Некоммерческий сектор
Платформа пожертвований для НКО
- Год
- 2025
- Роль
- Технический руководитель, два инженера
- Результаты
- Бета: 11 организаций подключено
Контекст
Небольшим некоммерческим организациям негде собирать пожертвования так, чтобы жертвователь видел, куда ушли деньги. Задача не техническая, а доверительная: платформа существует ради публичной отчётности, приём платежей — лишь способ её обеспечить.
Ограничения
- Один сервер и ноль бюджета на облако
- Платёжный провайдер без sandbox-окружения
- Публичная отчётность обязана сходиться до копейки
Решение
Публичная отчётность обязана сходиться до копейки, поэтому деньги в базе живут целыми числами в дирамах, а не числами с плавающей точкой, и каждая операция идемпотентна по ключу от провайдера.
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,
});
}Верификацию НКО мы вели вручную, в переписке. Это правильно для одиннадцати организаций и нерабочо для пятидесяти.
Архитектура
Ключевые решения
Проверка интеграции с платёжным провайдером
Что выбрал
Свой стенд-двойник провайдера, собранный по документации и по логам реальных ответов. Тесты гоняются против него, прод — против настоящего API.
Что отверг
Тестировать на боевом провайдере мелкими платежами. Дёшево на старте, но каждая проверка требует возврата и ручной сверки.
Почему
Двойник врёт ровно там, где расходится с документацией, и это находится один раз. Боевые микроплатежи врут каждый раз по-новому и оставляют след в отчётности, которая обязана быть чистой.
Как хранить суммы
Что выбрал
Целые числа в дирамах — минимальной единице валюты.
Что отверг
Числа с плавающей точкой или decimal средствами приложения.
Почему
Публичная отчётность обязана сходиться до копейки. Любое округление на границе типов однажды даёт расхождение в одну единицу, и объяснить его жертвователю невозможно.
Что было сложно
Проблема
Провайдер повторял вебхук при любой задержке ответа, и первые сборы получили дублирующиеся поступления.
Решение
Каждая операция идемпотентна по идентификатору события провайдера: повтор возвращает уже записанное поступление вместо создания второго.
Проблема
Организациям почти сразу понадобился CSV для собственных аудиторов, а отчётность была сделана страницей, а не выгрузкой.
Решение
Выгрузку пришлось добавлять поверх готовой вёрстки. Правильнее было бы с самого начала считать отчёт данными, а страницу — одним из его представлений.
Результаты
- организаций
- 11
- сборов проведено
- 38
- расхождений в отчётности
- 0