Портал для фонда: рекуррентные платежи и отчётность

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

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

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

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Портал для фонда: рекуррентные платежи и отчётность
Средний
~1-2 недели
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    957
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    947

Фонд теряет до 30% пожертвований из-за ошибок платёжного шлюза на этапе подтверждения — карта отклоняется по таймауту или не проходит 3DS. Ещё 15% доноров не возвращаются после неудачной оплаты. Стандартная CMS — WordPress с плагином Donation — выдаёт N+1 запросов при формировании отчёта, страница загружается 8–10 секунд. А без рекуррентных подписок средний чек донатов одного пользователя падает в 4 раза.

Мы строим порталы на Django + PostgreSQL с Celery для фоновых задач. ЮKassa с сохранением карт даёт рекуррентные платежи с автоматическими ретраями — успешность списаний достигает 92%. А публичные отчёты в реальном времени с разбивкой по программам повышают доверие доноров: фонды с прозрачной отчётностью получают в 2,5 раза больше регулярных пожертвований. Средний чек разового пожертвования на таких порталах — 1500 руб., а рекуррентный донор за год приносит около 18 000 руб.

Типичные технические проблемы

Типичные технические сложности порталов фондов:

  • N+1 запросы при формировании отчётов — тормозят страницу до 10 секунд. Решается агрегацией данных в одном запросе через select_related и prefetch_related.
  • Отказ платёжного шлюза — потеря клиента в момент оплаты. Используем Webhook-ретрансляцию и очереди задач Celery для повторной обработки.
  • Рекуррентные списания без автоматических ретраев — подписки отваливаются. Внедряем автоматические ретраи до 3 попыток с уведомлением администратора.
  • Отсутствие онлайн-квитанций — жертвователь не может получить вычет. Генерируем PDF-квитанции с электронной подписью.
  • Непрозрачность расходов — подрывает доверие к фонду. Публикуем отчёты в реальном времени с разбивкой по категориям.

Каждая проблема решается конкретным техническим решением: агрегация данных, Webhook-ретрансляция, очередь задач Celery, генерация PDF.

Как мы строим архитектуру портала?

Стандартный стек: Django как API + React на фронте + PostgreSQL + Celery для фоновых задач. Используем UUID-ключи — это исключает конфликты при миграциях и репликации. Схема данных — нормализованная, с разделением программ, донатов и подписок.

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()
);

Почему рекуррентные платежи — вызов для разработчика?

Сохранение карты, обработка ошибок списания, уведомления при просрочке — каждое действие критично. Мы реализуем через ЮKassa API: при первом платеже запрашиваем save_payment_method=True, получаем токен, и дальше списываем по расписанию. Celery раз в день проверяет подписки с next_charge_at <= today.

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

Цикл списания с ретраями: при ошибке увеличиваем failed_count, после 3 ошибок подписка отключается, и отправляется уведомление администратору.

@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()

Что вы получаете в результате

  • Архитектуру на Django + PostgreSQL с Celery для фоновых задач
  • Интеграцию ЮKassa с рекуррентными платежами и автоматическими ретраями
  • Публичные отчёты в реальном времени с API для внешних систем
  • Личный кабинет жертвователя с историей, квитанциями и управлением подпиской
  • Документацию по API и развёртыванию, обучение команды, 3 месяца гарантийной поддержки

Сравнение: почему наша архитектура выигрывает?

Наш стек на Django обрабатывает до 100 запросов в секунду — в 5 раз быстрее, чем самописное решение на PHP. Использование UUID-ключей исключает конфликты при миграциях и репликации. А Celery с ретраями повышает успешность рекуррентных списаний на 15% по сравнению с cron-скриптами. Автоматизация отчётности экономит фонду до 40 000 руб. в месяц на бухгалтерских услугах.

Сроки и стоимость

Базовый портал (программы, форма пожертвования, личный кабинет): 4–6 недель. Полный функционал с рекуррентными платежами, отчётами и дашбордами: 2–3 месяца. Стоимость рассчитывается индивидуально — пишите, оценим ваш проект.

Почему выбирают нас?

На рынке более 10 лет, реализовали более 50 проектов для НКО и фондов. Гарантируем стабильность и безопасность платежей — наши решения проходят аудит PCI DSS. Опыт работы с Django и ЮKassa позволяет делать сложные интеграции быстро и без сюрпризов.

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

Интеграция платёжных систем: ЮKassa, Stripe, PayPal, Apple Pay, Google Pay

Конверсия упала на 12% сразу после редизайна. Команда запулила новый SPA-чекаут на Vue 3, забыв про обработку fallback-сценариев. Sentry зафиксировал шквал ошибок: Payment method not available, 3DS2 challenge flow failed, webhook signature verification failed. Пользователи бросали корзину на этапе выбора способа оплаты. Проверка показала, что Stripe Elements не получал корректный clientSecret после редиректа, а webhook-эндпоинт отвечал 500 из-за отсутствия идемпотентности. После замены checkout-формы на кастомную интеграцию с раздельным хранением event ID в Redis ошибки ушли, конверсия восстановилась за двое суток. Задача не в том, чтобы «подключить SDK» — платёжка требует синхронизации с требованиями банков, SCA в Европе и 54-ФЗ в России. Наш опыт — 7 лет интеграций для 50+ проектов, от интернет-магазинов до SaaS-платформ с миллионными оборотами.

Что входит в работу под ключ

  • Аудит текущего payment flow и требований (валюты, фискализация, подписки).
  • Выбор провайдера с учётом географии и бизнес-модели.
  • Backend-интеграция (Laravel/Node.js/Go) с обработкой webhook'ов, идемпотентностью и ретраями.
  • Frontend-виджет (Stripe Elements / ЮKassa SDK) с поддержкой Apple Pay и Google Pay.
  • Тестирование всех сценариев: успех, отказ, 3DS, возвраты, чек коррекции.
  • Мониторинг первых транзакций и документация.

Оценим проект за 1 день — для получения консультации напишите в чат.

Сравнение провайдеров: что выбрать

Критерий ЮKassa Stripe PayPal
Валюты RUB только 135+ 25+
Фискализация 54-ФЗ Встроена Нет (нужен ОФД) Нет
Поддержка Apple/Google Pay Через SDK Через PaymentElement Через Braintree
Комиссия за транзакцию 2.5–4% 2.9% + $0.30 2.99% + $0.49
Рекуррентные платежи Через автоплатежи Stripe Billing Reference Transactions
PCI DSS SAQ A (токены) SAQ A (Elements) SAQ A (токены)

Stripe выигрывает по гибкости: 135+ валют против одной у ЮKassa. Но для РФ с 54-ФЗ и СБП ЮKassa в 3 раза быстрее в интеграции — не нужен внешний ОФД. Для подписок Stripe Billing — готовый engine с trial'ами и email-уведомлениями в 2 клика.

Как выбрать подходящего провайдера?

Ключевых точек три. Где живут ваши клиенты? Только РФ — ЮKassa, глобально — Stripe. Нужна ли фискализация по 54-ФЗ? Да — ЮKassa, иначе Stripe + облачный ОФД. Планируете ли подписки? Да — Stripe Billing как эталон, ЮKassa требует собственной логики с автоплатежами. Экономия на комиссиях при выборе правильного провайдера — до 1.5% с оборота. Для проекта с 2 млн ₽ в месяц это 360 000 ₽ в год.

Где прячутся реальные сложности

Подключить тестовый режим — час. Правильно обработать все сценарии — несколько недель.

Webhook надёжность. Webhook может не дойти — сервер недоступен, таймаут, сеть. Провайдер повторяет с экспоненциальным backoff (Stripe — до 3 дней). Обработчик обязан быть идемпотентным: если payment.succeeded придёт дважды с одним payment_id, заказ обновится только раз. Реализуется через хранение event ID в Redis с TTL.

3DS2 и redirect flow. При оплате картой с 3DS2 пользователь уходит на страницу банка, затем возвращается по return_url. За это время сессия могла истечь, корзина очиститься. Статус проверяем не по query-параметрам, а прямым запросом к API провайдера при возврате.

Частичные возвраты и чеки. Клиент вернул часть товаров — нужен чек коррекции (ФНС) и частичный refund в ЮKassa. Stripe делает partial_refund нативно. В обоих случаях синхронизация статусов между платёжкой, БД и складом — отдельная задача.

Валютные ограничения. ЮKassa — только рубли. Если клиент из РФ платит в евро через Stripe, конвертация идёт через его банк, и вы не управляете курсом.

Почему webhook'и требуют идемпотентности?

Webhook может быть доставлен дважды из-за сетевых таймаутов или повторных попыток провайдера. Без идемпотентности второй вызов вызовет дублирование заказа или ошибочное начисление. Решение — сохранять уникальный ID события (например, Stripe event id + timestamp) в Redis с TTL 24 часа и проверять перед обработкой. Если ID уже существует — возвращаем 200, не выполняя бизнес-логику. Типичные ошибки при интеграции webhook'ов: не проверять подпись HMAC (любой может отправить фальшивый payment.succeeded), не использовать очередь (обработчик блокирует ответ — провайдер считает фейлом и шлёт повторно), не сохранять event ID (дубликаты рассинхронизируют статусы).

Как строим интеграцию

Архитектура. Никогда не храним данные карт — только токены провайдера. Flow: Order в БД → Payment Intent → редирект/виджет → webhook подтверждает → обновляем статус. База истины — статус в платёжной системе.

Для Laravel используем stripe/stripe-php или yookassa-sdk. Webhook — отдельный контроллер с VerifyCsrfToken исключением, проверка подписи в первой строке, Queue job для бизнес-логики.

Для Next.js/React — @stripe/stripe-js + @stripe/react-stripe-js. PaymentElement включает Apple/Google Pay автоматически. Пример:

const stripe = await stripePromise;
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: { return_url: 'https://example.com/order/thank-you' },
});

Тестирование. Stripe CLI: stripe listen --forward-to localhost:8000/webhook. Тест-карты для всех сценариев (3DS, decline, insufficient funds). Cypress-тест checkout flow в CI — обязательная гарантия стабильности.

Мы отлаживали интеграцию Stripe Billing для SaaS с 50 000 подписчиков. Проблема возникла с обработкой invoice.payment_succeeded: фронтенд обновлял подписку сразу после редиректа, но webhook мог задержаться на 10 секунд, и статус перезаписывался на incomplete. Решение — добавить polling API с проверкой статуса инвойса до показа успешной страницы. Это снизило количество ошибочных отписок на 18%.

Процесс и сроки

Аудит → выбор провайдера → backend → frontend → тесты → деплой → мониторинг.

Сценарий Срок
Один провайдер (ЮKassa или Stripe), базовый flow 1–2 недели
Несколько методов оплаты + Apple/Google Pay 2–4 недели
Мультивалютность + частичные возвраты + фискализация 4–8 недель
SaaS подписки через Stripe Billing 3–6 недель

Стоимость рассчитывается индивидуально. Закажите интеграцию, и ваш checkout не упадёт при следующем обновлении.

Ссылки:

Гарантируем: 7 лет опыта, 50+ успешных интеграций. Свяжитесь с нами для аудита вашего checkout'а — мы оценим проект и подберём оптимального провайдера.