Skip to content

Latest commit

 

History

History
109 lines (81 loc) · 6.42 KB

payment-flow.md

File metadata and controls

109 lines (81 loc) · 6.42 KB

Payment, Ledger, and Settlement Flow

Payment trust boundary

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.

Local capture workflow

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
Loading

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.

Commission precedence

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.

Append-oriented ledger

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.

Settlement lifecycle

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
Loading

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.

Refund accounting

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

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.