Bonum settles off-platform: the saga records the revenue-split plan, then a downstream
operator (typically MCredit) pays sub-merchants and reports back via the settlement API.
Architecture
- Three Prisma models:
BonumTransaction,MerchantPrefixRegistry,BonumConfig - NestJS module: dedicated
bonummodule on the backend - Payment strategy:
BonumPaymentStrategy— registered inPaymentFactoryunderprovider === 'BONUM' - Saga step: the revenue-distribution step forks for Bonum orders (records splits, no live transfer)
- Webhook receiver:
POST /bonum/webhook(IntegrationApiKeyGuard) - Settlement cron: two daily audit passes (
SettlementReconciliationCron)
API surface
| Route | Purpose |
|---|---|
POST /orders/v2/create-payment-intent (with provider=BONUM) | Creates BonumTransaction, returns invoice ID + Apple/Google Pay token |
POST /bonum/payment/confirm | Confirms payment via the integration service → Bonum PSP |
POST /bonum/webhook | Settlement event from Bonum; signature-verified, idempotent |
GET /settlement/summary | Settlements grouped by merchant prefix |
GET /settlement/transactions | Settlement-transaction listing |
PATCH /settlement/transactions/payout | Mark transactions as paid out to sub-merchants |
GET /merchants/:shopId/balance | Pending payable balance for a shop |
Configuration
| Variable | Default | Notes |
|---|---|---|
BONUM_API_BASE_URL | — | https://testpsp.bonum.mn for sandbox |
BONUM_MERCHANT_KEY | — | Issued by the Bonum / MCredit team |
SETTLEMENT_CRON_SCHEDULE | 0 2 * * * | Adjust for the operating timezone (Ulaanbaatar UTC+8 = 0 18 * * *) |
MERCHANT_PREFIX_DIGITS | 6 | Confirm with MCredit before go-live |
Payment flow
Create payment intent
POST /orders/v2/create-payment-intent with provider=BONUM. The PaymentFactory
routes to BonumPaymentStrategy.createPaymentIntent(), which upserts a
BonumTransaction and returns an invoice ID prefixed with the shop’s merchant prefix.Confirm payment
POST /bonum/payment/confirm calls the integration service → Bonum PSP. The verify
use case then confirms via /api/payment-log/read.Run the confirmation saga
The standard
ConfirmPaymentSaga runs; for Bonum the revenue-distribution step
records splits rather than executing live transfers.Receive settlement webhook
Bonum POSTs to
/bonum/webhook → HandleBonumWebhookUseCase sets settledAt.Testing
Sandbox setup
- Set
BONUM_API_BASE_URL=https://testpsp.bonum.mn - Obtain a sandbox
BONUM_MERCHANT_KEYfrom the MCredit team - Confirm
NODE_ENVis notproduction
Unit tests
Critical scenarios
| Scenario | Expected |
|---|---|
Shop with ACTIVE prefix creates intent | 24-char alphanumeric invoice ID |
| Shop with no prefix | BadRequestException |
Shop with SUSPENDED prefix | BadRequestException |
Two createOrGet calls with same invoice ID | Second returns existing record (no duplicate) |
| Revenue splits | droplinkedFeeAmount + merchantPayable + affiliateAmount + referralAmount === amountMnt; live transfer not called |
Webhook delivered twice with same _eventId | First sets settledAt; second is silently discarded |
Invalid payout status transition (e.g. skipping PROCESSING → PAID) | 400 with “Invalid payout status transition” |
Settlement API examples
Cron audit
The settlement-reconciliation cron runs two daily passes:- Pass 1 — warns on
BonumTransactionrows that are verified but unsettled after 48h - Pass 2 — warns on rows that remain unverified after 24h
E2E against sandbox
Requires a sandboxBONUM_MERCHANT_KEY and an active MerchantPrefixRegistry entry.
Create the intent
POST /orders/v2/create-payment-intent with provider=BONUM → confirm the returned
invoice ID starts with the shop’s prefix.Pay via sandbox
Use a real Apple/Google Pay token from
testpsp.bonum.mn, POST it with the invoice ID
to the integration service’s /bonum/payment/process endpoint.Related
- Order lifecycle — how Bonum slots into the confirmation saga.
- Checkout stability — the broader test matrix.