Files
baya-monorepo/archive/docs/integration/domains/payment.md
T
2026-08-02 20:01:31 +03:30

5.7 KiB

payment — card checkout, the PSP webhook, invoices

Client seam client/src/services/payment/ · USE_PAYMENT_MOCK = false (real) · 3 server ops Last verified: 2026-07-30 against commit d3ec723 and swagger.v1.json (2026-07-29).

The card money path. Three endpoints on the wire; the domain reads three more that belong to neighbours. Installment checkout is bnpl.md; reversals are refunds.md.

Endpoints

Method Path Auth Verdict
POST /api/v1/bookings/{bookingRequestId}/payments [Authorize] · sensitive 20/min wired · Idempotency-Key
GET /api/v1/invoices/{bookingId} [Authorize] wired
POST /api/v1/webhooks/payments/{provider} anonymous · webhook 120/min server-only — the PSP calls it

Also read by this domain, documented with their owners: GET booking_requests/checkout_summary/{id} and GET booking_requests/get/{id} (booking-requests.md).

Phantom — 1

Client call REQ Live?
GET /api/v1/bookings/payment_history REQ-047 Yes — this domain's mock is off. The wallet «پرداخت‌ها» tab calls it, gets a 404, and renders its empty state. Guarded, but a real 404 on every visit

The flow, and why it is shaped this way

accepted request ──initiate──▸ PSP hosted page ──customer pays──▸ PSP webhook
     (money-free)              (redirectUrl)                          │
                                                                      ▼
                                              server re-verifies, then creates + confirms the booking

Four rules that follow, and they are the whole design:

  1. Payment is initiated against the accepted request, not a booking. The bookings row does not exist yet. POST bookings/{bookingRequestId}/payments takes a request id despite the bookings/ prefix — the route is misleading and the parameter name is the truth.
  2. There is no client verify endpoint. The server re-verifies with the acquirer inside the webhook handler. A client-reported "success" is never trusted.
  3. The client learns the outcome by polling. getPaymentOutcome maps the request status (converted → succeeded) with backoff. A first-class transaction-status read is REQ-017's remaining half.
  4. One Idempotency-Key per attempt, reused across retries of that attempt; a new attempt takes a new key. A 409 on initiate means "already in progress / already captured" — a benign convergence, and the client must not surface it as an error.

Webhook idempotency does not use the header. The handler upserts the provider event first, keyed on external_event_id, and no-ops on a duplicate; bookings.booking_request_id is UNIQUE so a replay cannot create a second booking; a unique-violation on confirm is treated as idempotent success. The DB constraint is the backstop, not the handler's if.

POST bookings/convert (bookings.md) is the Development-only capture simulator that stands in for the webhook locally. It is fail-closed outside Development/Testing.

Shape rules the JSON does not express

  • CheckoutSummaryDto serves the money breakdown so the client never derives it (REQ-016, delivered): serviceCostIrr, commissionIrr, vatIrr, vatRate, totalIrr and the three-amount split grossPriceIrr / balinyaarCommissionIrr / nursePayoutAmount. All digit strings.
  • VAT is on Balinyaar's commission only — the platform's taxable supply — never on the nurse payout.
  • InitiatePaymentResult is { transactionId, redirectUrl, gatewayReferenceCode }. The client hands off to redirectUrl and keeps transactionId to poll.
  • InvoiceDto carries totalIrr (REQ-033, partial) = platform commission + BNPL commission + VAT, and moadianStatus/moadianReferenceNumber for the سامانه مودیان e-invoicing submission. issuingEntityType distinguishes a platform-issued invoice from a partner-center one — see partner-center.md.
  • The acquirer is a seam. Seams:Payments:Provider = mock (default) / zarinpal / sadad / vandar / jibit; IPaymentProvider, ISettlementSplitProvider and IWebhookVerifier swap together. Webhook signature secrets are per-provider (Seams:Payments:WebhookSigningSecrets), read from the X-Signature header by default. A provider with no signature falls back to the mandatory server-side re-verify.

Enums

Vocabulary Values
PaymentTransactionStatus pending succeeded failed
MoadianStatus pending submitted registered failed
GatewayReturnOutcome (client-side, from the return URL) success failure

Verified against Entities/Payments/PaymentTransactionStatus.cs and Entities/Invoices/MoadianStatus.cs. GatewayReturnOutcome is a client reading of the acquirer's redirect and is advisory only — the authoritative outcome is the polled request status.

Open REQs

REQ Status Effect
REQ-017 delivered (partial in practice) bookingId is on the converted request; the first-class transaction-status read is not, so the client polls the request status instead
REQ-046 open No nurse identity on the checkout summary and no client-readable payment reference. The real receipt hides the identity avatar/badge and the tracking line
REQ-047 open No customer payment-transactions list. Live 404 on bookings/payment_history; the wallet tab renders empty
REQ-049 open InvoiceDto has no payment method, transaction reference, or seller fiscal identity. The invoice renders the money breakdown and مودیان status unconditionally