The browser never decides payment success. It receives provider instructions, but only a signature-verified provider webhook or an authoritative provider verification call may capture a payment.
For the local adapter, createPayment produces a deterministic PENDING payment and provider reference. Authoritative verifyPayment moves it to CAPTURED after amount/currency checks. The signWebhookPayload method is a test/local helper, not an administrative capture bypass.
The simplest development path is an authenticated POST /api/v1/payments/{paymentId}/mock-capture by the customer who owns the payment. The operation is unavailable when NODE_ENV=production. It creates a deterministic payment.captured event, signs the exact bytes, and calls the same verification/deduplication service used by the public webhook.
To test webhook ingress directly, send POST /api/v1/webhooks/payments/mock with x-provider-signature and this DTO shape:
{
"id": "unique-provider-event-id",
"eventType": "payment.captured",
"providerPaymentId": "mock_pay_reference_from_checkout"
}The header is lowercase hexadecimal HMAC-SHA256 over the exact raw request bytes using WEBHOOK_MOCK_SECRET (the sha256= prefix is optional). Changing JSON whitespace after computing the signature invalidates it. The authenticated development helper avoids that manual byte-matching while retaining the same processing path.
sequenceDiagram
actor Customer
participant Checkout
participant Payments
participant Adapter as PaymentProvider
participant Webhook as Webhook endpoint
participant DB as PostgreSQL
participant Orders
Customer->>Checkout: initiate checkout + idempotency key
Checkout->>Payments: create payment for server total
Payments->>DB: persist payment/attempt as pending
Payments->>Adapter: createPayment(amountMinor, currency, reference)
Adapter-->>Payments: provider payment reference/instructions
Payments-->>Customer: provider-safe instructions
Customer->>Adapter: complete provider/mock payment
Adapter->>Webhook: signed payment event
Webhook->>Webhook: verify signature and event ID
Webhook->>DB: insert/deduplicate webhook event
Webhook->>Adapter: verifyPayment when required
Webhook->>Payments: capture once
Payments->>DB: transaction: payment + inventory + order effects
Payments->>Orders: PaymentCaptured / OrderConfirmed
Repeated delivery of the same provider event returns a successful acknowledgement after finding the stored event; it must not create another order, inventory movement, commission snapshot, or ledger entry.
The commission engine chooses the most specific active rule supported by the implementation (for example vendor override, then category override, then marketplace default). The selected rule, basis amount, basis points, commission amount, and vendor net are stored as an order-time snapshot. Future rule changes affect only new orders.
Ledger entries store a positive amount in minor units plus an explicit CREDIT or DEBIT direction and a stable business reference. Typical postings include ORDER_EARNING, MARKETPLACE_COMMISSION, PAYMENT_FEE, SHIPPING_CHARGE, REFUND, REVERSAL, MANUAL_ADJUSTMENT, and SETTLEMENT. Corrections append an offsetting entry; they do not overwrite history.
Conceptually:
available vendor balance
= sum(eligible credits)
- sum(debits)
- sum(amounts already reserved/paid in settlements)
The actual query must respect entry status and settlement linkage, not only sum every row indiscriminately.
flowchart LR
Captured[Payment captured]
Ledger["Commission snapshot +<br/>ledger postings"]
Delivered[Vendor order delivered]
Eligible[ELIGIBLE]
Processing[PROCESSING]
Provider[SettlementProvider]
Settled[SETTLED]
Failed[FAILED]
Reconcile[Reconciliation record]
Captured --> Ledger
Ledger --> Delivered
Delivered --> Eligible
Eligible --> Processing
Processing --> Provider
Provider -- accepted --> Settled
Provider -- retryable/permanent error --> Failed
Settled --> Reconcile
Failed --> Reconcile
Settlement creation and provider submission use stable idempotency references. A settlement item points to the ledger value it consumes so the same earning cannot be paid twice. The default/local provider simulates payout state; integrating an actual bank/payout rail requires provider onboarding, credentials, webhook verification, and reconciliation operations.
The MVP marks the vendor order's positive net ledger amount eligible immediately on delivery. A configurable return/dispute hold window is an intended production extension and should be added before real payouts where marketplace policy requires it.
Authorized finance/admin users can inspect eligible records at GET /api/v1/admin/settlements and initiate the mock payout at POST /api/v1/admin/settlements/{settlementId}/process. In non-production environments, POST /api/v1/admin/settlements/{settlementId}/mock-complete moves the provider payout to SETTLED and appends the settlement debit to the vendor ledger.
A refund stores the requested amount, provider reference, status, and affected order items. Before calling the provider, related not-yet-processing settlement snapshots (PENDING, ELIGIBLE, or FAILED) are compare-and-set invalidated; processing/settled payouts block the refund. On authoritative provider completion, one transaction adds refund and commission-reversal ledger entries, adjusts payment/order totals, and rebuilds each invalidated settlement's items and net amount from the resulting ledger (or leaves it cancelled when nothing remains payable). Request idempotency, transactional claims, stable ledger references, and audited settlement changes prevent a retry from applying the financial adjustment twice. A future asynchronous refund webhook must enter through the same claim/finalization path.
Reconciliation basics persist finance-scoped runs, submitted provider comparison results, mismatches, and audited resolutions for payments, refunds, or settlements. The current MVP does not automatically ingest provider or bank reports; a future reconciliation job/adapter must obtain that data and submit normalized results to this workflow.