Разработка портала для благотворительного фонда

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка портала для благотворительного фонда
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка портала для благотворительного фонда

Портал благотворительного фонда — это не просто сайт с кнопкой «Пожертвовать». Он должен вызывать доверие, обеспечивать прозрачность расходования средств и технически правильно работать с регулярными платежами. Отказ платёжного шлюза, потерянное пожертвование или отсутствие отчётности — прямой удар по репутации фонда.

Ключевые функциональные блоки

Типичный портал фонда включает: каталог программ и проектов с прогрессом сбора, формы разового и регулярного пожертвования, личный кабинет жертвователя с историей платежей, отчёты о расходовании средств, CMS для редакторов.

Схема данных

CREATE TABLE programs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    slug            VARCHAR(200) UNIQUE NOT NULL,
    title           VARCHAR(300) NOT NULL,
    description     TEXT,
    goal_amount     NUMERIC(15,2),      -- NULL = сбор без цели
    collected       NUMERIC(15,2) NOT NULL DEFAULT 0,
    status          VARCHAR(20) NOT NULL DEFAULT 'active'
                    CHECK (status IN ('active','completed','paused','archived')),
    is_featured     BOOLEAN NOT NULL DEFAULT FALSE,
    image_url       VARCHAR(500),
    ends_at         DATE,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE donations (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    program_id      UUID REFERENCES programs(id),  -- NULL = на уставную деятельность
    donor_id        UUID REFERENCES users(id),      -- NULL = анонимное пожертвование
    amount          NUMERIC(15,2) NOT NULL,
    currency        CHAR(3) NOT NULL DEFAULT 'RUB',
    is_anonymous    BOOLEAN NOT NULL DEFAULT FALSE,
    is_recurring    BOOLEAN NOT NULL DEFAULT FALSE,
    subscription_id UUID REFERENCES recurring_subscriptions(id),
    payment_method  VARCHAR(50),       -- 'card','sbp','qiwi','yoomoney'
    payment_id      VARCHAR(200),      -- ID транзакции в платёжной системе
    status          VARCHAR(20) NOT NULL DEFAULT 'pending'
                    CHECK (status IN ('pending','completed','failed','refunded')),
    donor_message   TEXT,
    receipt_sent_at TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE recurring_subscriptions (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    donor_id        UUID NOT NULL REFERENCES users(id),
    program_id      UUID REFERENCES programs(id),
    amount          NUMERIC(15,2) NOT NULL,
    currency        CHAR(3) NOT NULL DEFAULT 'RUB',
    payment_token   VARCHAR(200) NOT NULL,  -- сохранённый токен карты
    interval        VARCHAR(20) NOT NULL DEFAULT 'monthly'
                    CHECK (interval IN ('weekly','monthly','quarterly')),
    next_charge_at  DATE NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'active'
                    CHECK (status IN ('active','paused','cancelled','failed')),
    failed_count    INTEGER NOT NULL DEFAULT 0,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Форма пожертвования

Минимальная форма — сумма, выбор программы, тип (разовое/регулярное). Регулярное требует сохранения карты.

// DonationForm.tsx
interface DonationFormData {
  amount: number
  programId: string | null
  isRecurring: boolean
  isAnonymous: boolean
  message?: string
  donorName?: string
  donorEmail?: string
}

const PRESET_AMOUNTS = [100, 300, 500, 1000, 3000]

export function DonationForm({ program }: { program?: Program }) {
  const [isRecurring, setIsRecurring] = useState(false)
  const [amount, setAmount] = useState<number>(500)

  const handleSubmit = async (data: DonationFormData) => {
    const response = await fetch('/api/donations/initiate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    })
    const { payment_url } = await response.json()
    window.location.href = payment_url
  }

  return (
    <form onSubmit={handleSubmit}>
      <div className="preset-amounts">
        {PRESET_AMOUNTS.map(preset => (
          <button
            key={preset}
            type="button"
            className={amount === preset ? 'active' : ''}
            onClick={() => setAmount(preset)}
          >
            {preset} ₽
          </button>
        ))}
        <input
          type="number"
          min={50}
          value={amount}
          onChange={e => setAmount(Number(e.target.value))}
          placeholder="Другая сумма"
        />
      </div>
      {/* ... остальные поля */}
    </form>
  )
}

Интеграция ЮKassa с рекуррентными платежами

import yookassa
from yookassa import Payment, Configuration

Configuration.configure(
    account_id=settings.YUKASSA_SHOP_ID,
    secret_key=settings.YUKASSA_SECRET_KEY,
)


def initiate_donation(donation_data: dict) -> str:
    """Создаём платёж, возвращаем URL для редиректа"""
    metadata = {
        'donation_id': str(donation_data['id']),
        'program_id': str(donation_data.get('program_id', '')),
        'is_recurring': donation_data['is_recurring'],
    }

    payment = Payment.create({
        'amount': {
            'value': str(donation_data['amount']),
            'currency': 'RUB',
        },
        'payment_method_data': {'type': 'bank_card'},
        'save_payment_method': donation_data['is_recurring'],
        'confirmation': {
            'type': 'redirect',
            'return_url': f'{settings.BASE_URL}/donation/thank-you?id={donation_data["id"]}',
        },
        'description': f'Пожертвование в фонд «{settings.FOUNDATION_NAME}»',
        'metadata': metadata,
        'receipt': {
            'customer': {'email': donation_data['email']},
            'items': [{
                'description': 'Благотворительное пожертвование',
                'quantity': '1.00',
                'amount': {'value': str(donation_data['amount']), 'currency': 'RUB'},
                'vat_code': 1,  # Без НДС
                'payment_mode': 'full_payment',
                'payment_subject': 'another',  # Для некоммерческих платежей
            }],
        },
    })

    Donation.objects.filter(id=donation_data['id']).update(
        payment_id=payment.id
    )

    return payment.confirmation.confirmation_url

Рекуррентные списания

@shared_task
def charge_recurring_subscriptions():
    """Celery beat: ежедневно в 10:00"""
    today = date.today()
    subs = RecurringSubscription.objects.filter(
        status='active',
        next_charge_at__lte=today
    )

    for sub in subs:
        try:
            payment = Payment.create({
                'amount': {'value': str(sub.amount), 'currency': 'RUB'},
                'payment_method_id': sub.payment_token,
                'capture': True,
                'description': f'Регулярное пожертвование — {sub.get_interval_display()}',
                'metadata': {
                    'subscription_id': str(sub.id),
                    'is_recurring': True,
                },
            })

            Donation.objects.create(
                program=sub.program,
                donor=sub.donor,
                amount=sub.amount,
                is_recurring=True,
                subscription=sub,
                payment_id=payment.id,
                status='pending',
            )

            sub.failed_count = 0
            sub.next_charge_at = get_next_charge_date(today, sub.interval)
            sub.save()

        except Exception as e:
            sub.failed_count += 1
            if sub.failed_count >= 3:
                sub.status = 'failed'
                notify_subscription_failed.delay(str(sub.id))
            sub.save()
            logger.error(f'Recurring charge failed for sub {sub.id}: {e}')

Прозрачность: отчёты о расходовании

Ключевое для доверия — публичные отчёты с разбивкой по программам:

def get_program_report(program_id: str) -> dict:
    program = Program.objects.get(id=program_id)
    donations = Donation.objects.filter(
        program=program,
        status='completed'
    )
    expenses = Expense.objects.filter(program=program)

    return {
        'collected': donations.aggregate(total=Sum('amount'))['total'] or 0,
        'donors_count': donations.values('donor').distinct().count(),
        'avg_donation': donations.aggregate(avg=Avg('amount'))['avg'] or 0,
        'expenses': expenses.values('category').annotate(
            total=Sum('amount'),
            count=Count('id')
        ).order_by('-total'),
        'utilization_rate': (
            expenses.aggregate(s=Sum('amount'))['s'] or 0
        ) / program.collected * 100 if program.collected else 0,
        'monthly_dynamics': donations.annotate(
            month=TruncMonth('created_at')
        ).values('month').annotate(total=Sum('amount')).order_by('month'),
    }

Электронные квитанции

После пожертвования жертвователь получает квитанцию — важно для тех, кто оформляет налоговый вычет:

def send_donation_receipt(donation):
    """Письмо с квитанцией после успешного платежа"""
    if not donation.donor or donation.is_anonymous:
        return

    context = {
        'donation': donation,
        'foundation_name': settings.FOUNDATION_NAME,
        'foundation_inn': settings.FOUNDATION_INN,
        'donation_date': donation.created_at.strftime('%d.%m.%Y'),
    }

    send_mail(
        subject=f'Квитанция о пожертвовании на сумму {donation.amount} ₽',
        message=render_to_string('emails/receipt.txt', context),
        html_message=render_to_string('emails/receipt.html', context),
        from_email=settings.FOUNDATION_EMAIL,
        recipient_list=[donation.donor.email],
    )

    donation.receipt_sent_at = timezone.now()
    donation.save()

Сроки

Базовый портал с программами, формой пожертвования, ЮKassa и личным кабинетом: 4–5 недель. С рекуррентными платежами, отчётами по расходованию, CMS для контента и интеграцией СБП: 2–3 месяца.