internal/commerce, internal/commercepg,
internal/paymentservice или в файле database/migrations, который затрагивает заказ, платёж или
книгу проводок.
Обеспечивается схемой
Это CHECK-ограничения и уникальные ограничения в PostgreSQL. Нарушение любого из них вызывает ошибку, а не производит неверное число.Обеспечивается внутри транзакции
Эти правила нельзя выразить ограничением, поэтому они реализованы как проверки внутри сериализуемой транзакции — и каждая написана там, где меняется состояние, а не в вызывающем коде, который можно обойти.Только добавление — точная формулировка
«Только добавление» стоит сформулировать точно, потому что три таблицы, к которым это относится, ведут себя по-разному:- Транзакции и проводки книги никогда не обновляются и никогда не удаляются. Ни в панели, ни в API, ни в воркерах нет пути кода, который делает то или другое. Корректировка — это новая компенсирующая пара.
- Платёжные события только вставляются. История намерения — это упорядоченный список всего, что когда-либо было по нему классифицировано, включая классификации, которые ничего не изменили.
- События вебхуков провайдеров вставляются один раз на
(provider, provider_event_id), и дальше у них только завершается статус обработки. Они несутretain_untilв 30 дней, после чего чистка по срокам хранения их удаляет: сырое тело — это диагностические данные, а не финансовая история.
Что намеренно не гарантируется
Описывать проверки, не описывая их границы, значило бы вводить в заблуждение.- Единого шлюза, через который проходит каждый переход заказа, нет. Домен объявляет таблицу
переходов, но в продакшене её никто не вызывает. Работает набор предикатов на отдельных запросах:
запрос отмены, чистка истёкших заказов, проверка на дубликат в классификации. Добавить новый код,
пишущий
orders.state, — значит добавить и его собственную проверку, а не рассчитывать на уже существующую. wallet_unavailableоставляет работу человеку. Провайдер взял деньги по заказу, который остаётся вpending, и ничто не разрешает это автоматически. См. Когда платёж не проводится.- Сгорание средств в кошельке сходится, а не применяется мгновенно. Баланс — это простая сумма
проводок; сгорание — компенсирующая проводка, которую раз в минуту пишет проход
commerce_maintenanceна воркере. Пока он не отработал, средства с истёкшим сроком по-прежнему можно потратить. - Кнопки возврата и сверки в панели фиксируют решение. Они записывают
panel.refund.issuedиpanel.payment.reconciledи не двигают деньги и не опрашивают провайдера. Возврат, который действительно возвращает деньги, — этоPOST /v1/admin/payments/{paymentID}/refunds. - Уникальное ограничение на
ledger_entriesне может дедуплицировать клиринговые строки. Оно покрывает(transaction_id, account_type, user_id, currency), а у обоих клиринговых типов счетовuser_idравен NULL, который PostgreSQL в уникальном ограничении считает различным значением. Клиринговые строки полностью полагаются на то, что их транзакция создаётся один раз под своим уникальным ключом идемпотентности, — поэтому каждый пишущий код сначала создаёт транзакцию и вставляет проводки только под неё. - Возврат за выдачу цифрового товара зачисляет расчётную цену, не вычитая скидку по акции, и
оставляет
orders.stateиorders.refunded_minorнетронутыми. Клиенту, который применил промокод к неудавшейся выдаче в магазине, зачисляется больше, чем он заплатил, а заказ по-прежнему читается как оплаченный.
Список, блокирующий релиз
Изменение, ломающее любой из этих пунктов, не должно попадать в релиз:- Деньги — это целая сумма в минорных единицах плюс одна валюта из трёх заглавных букв.
- Заказ удовлетворяет
wallet + external = subtotal - discount. - Скидка не может превышать подытог; возвращённая сумма не может превышать оплаченную.
- Транзакция книги проводок сходится в ноль независимо по каждой валюте.
- Баланс кошелька выводится из проводок, которые только добавляются, и ни один путь кода не пишет движение, которое увело бы его ниже нуля.
- Кошельковые резервы ожидающих оплаты заказов вычитаются из доступного баланса и перепроверяются перед проведением платежа.
- Каждое событие провайдера, каждое изменение заказа, каждый возврат, каждый референс книги проводок и каждая операция исполнения идемпотентны под устойчивым ключом.
- Дублирующие, несовпадающие, переплаченные и недоплаченные события провайдера никогда не дают права на услугу; точный опоздавший платёж может дать его не более одного раза.
- Успех платежа и надёжное намерение на исполнение фиксируются вместе.
- История книги проводок, платёжных событий, согласий, аудита и исполнения только пополняется; тела вебхуков хранятся 30 дней и затем удаляются.
- Секреты, ссылки на подписку, контактные данные в открытом виде и сырые заголовки авторизации никогда не попадают в логи или в операционную историю.
- Изменения возвратов сериализуются по каждому платёжному намерению, и ожидающие плюс успешные возвраты не могут превысить оплаченную провайдером сумму до обращения к провайдеру.
- Пополнение кошелька зачисляет ровно ту сумму, которую провёл провайдер, ровно один раз, с ключом по своему заказу. Пополнение никогда не тратит средства кошелька.
- Пропорциональное списание за дополнение никогда не превышает его каталожную цену, никогда не округляется до нуля и считается в 128 битах, чтобы длинный период не мог его переполнить.
- Ёмкость дополнения применяется к праву на услугу не более одного раза на заказ и версию дополнения, а лимит null остаётся null.
- Сохранённая корзина переоценивается по текущему каталогу перед любым списанием и списывается под одним ключом идемпотентности, сколько бы путей ни пытались это сделать одновременно.
- Лимиты одновременности вычисляются под рекомендательной блокировкой на клиента, поэтому одновременные покупки не могут пройти обе.
- Режим обслуживания блокирует только новые заказы и исходящую выдачу доступа. Он никогда не отменяет, не возвращает, не аннулирует и иным образом не меняет уже взятые деньги.
- Чистка по срокам хранения удаляет только расходуемые операционные данные. Финансовая история, журнал аудита, согласия, идентификационные данные и история прав на услуги никогда не удаляются чисткой.
Где они проверяются тестами
Тесты домена покрывают разделение суммы с приоритетом кошелька, повтор платежа, несовпадение валют, область действия акции, балансировку книги проводок, подписи вебхуков, проверки жизненного цикла клиента, лимиты и проведение пополнений, пропорциональный расчёт дополнений, вычисление корзины, политику одновременности и разрешение сквадов. Они выполняются в обычномgo test ./....
Набор на Testcontainers в internal/integrationtest доказывает транзакционные инварианты на
настоящем PostgreSQL: ровно однократное зачисление пополнения при повторах, разрешение переплаты и
недоплаты в книге проводок, однократное списание корзины при конкуренции, соблюдение лимитов
одновременности под нагрузкой, одно событие outbox и одно операторское уведомление на проведение,
блокировку новых заказов режимом обслуживания при том, что уже оплаченный заказ проводится, и
однократное применение дополнений. См. Стратегия тестирования.
Смежное чтение: Заказы, платежи и возвраты, Кошелёк и книга
проводок, Пополнение кошелька и сохранённые
корзины.