← Все кейсы

Некоммерческий сектор

Платформа пожертвований для НКО

Год
2025
Роль
Технический руководитель, два инженера
Результаты
Бета: 11 организаций подключено

Контекст

Небольшим некоммерческим организациям негде собирать пожертвования так, чтобы жертвователь видел, куда ушли деньги. Задача не техническая, а доверительная: платформа существует ради публичной отчётности, приём платежей — лишь способ её обеспечить.

Ограничения

  • Один сервер и ноль бюджета на облако
  • Платёжный провайдер без sandbox-окружения
  • Публичная отчётность обязана сходиться до копейки

Решение

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

src/payments/ledger.ts
export async function recordIntake(
  tx: Transaction,
  event: ProviderEvent,
): Promise<LedgerEntry> {
  const existing = await tx.ledger.findByIdempotencyKey(event.id);
  if (existing !== null) {
    return existing; // повтор вебхука — не второе поступление
  }

  return tx.ledger.insert({
    idempotencyKey: event.id,
    campaignId: event.campaignId,
    amountDiram: BigInt(event.amountDiram),
    receivedAt: event.occurredAt,
  });
}

Верификацию НКО мы вели вручную, в переписке. Это правильно для одиннадцати организаций и нерабочо для пятидесяти.

Архитектура

Деньги входят один раз: повтор вебхука не создаёт второго поступления. жертвователь → web; web → api; api → платёжный провайдер; платёжный провайдер → api (вебхук); api → ledger; ledger → postgres; postgres → отчётностьвебхукжертвовательwebплатёжный провайдерapiledgerpostgresотчётность
Деньги входят один раз: повтор вебхука не создаёт второго поступления

Ключевые решения

  • Проверка интеграции с платёжным провайдером

    Что выбрал

    Свой стенд-двойник провайдера, собранный по документации и по логам реальных ответов. Тесты гоняются против него, прод — против настоящего API.

    Что отверг

    Тестировать на боевом провайдере мелкими платежами. Дёшево на старте, но каждая проверка требует возврата и ручной сверки.

    Почему

    Двойник врёт ровно там, где расходится с документацией, и это находится один раз. Боевые микроплатежи врут каждый раз по-новому и оставляют след в отчётности, которая обязана быть чистой.

  • Как хранить суммы

    Что выбрал

    Целые числа в дирамах — минимальной единице валюты.

    Что отверг

    Числа с плавающей точкой или decimal средствами приложения.

    Почему

    Публичная отчётность обязана сходиться до копейки. Любое округление на границе типов однажды даёт расхождение в одну единицу, и объяснить его жертвователю невозможно.

Что было сложно

  • Проблема

    Провайдер повторял вебхук при любой задержке ответа, и первые сборы получили дублирующиеся поступления.

    Решение

    Каждая операция идемпотентна по идентификатору события провайдера: повтор возвращает уже записанное поступление вместо создания второго.

  • Проблема

    Организациям почти сразу понадобился CSV для собственных аудиторов, а отчётность была сделана страницей, а не выгрузкой.

    Решение

    Выгрузку пришлось добавлять поверх готовой вёрстки. Правильнее было бы с самого начала считать отчёт данными, а страницу — одним из его представлений.

Результаты

организаций
11
сборов проведено
38
расхождений в отчётности
0

Стек