payment-gateway-service
Bounded Context: Payments (Generic) · Owner: Finance Platform · Phase: 0–1 · Storage: Cloud SQL Postgres schema-per-tenant (ADR-0002) + central control schema · Bundle: services/payment-gateway-service/
payment-gateway-service is the single internal abstraction for all money movement on Ghasi Melmastoon. Behind a stable PaymentPort interface (authorize, capture, refund, void, getTransaction, reconcileBatch, tokenize, describeAdapter) it hides every vendor SDK: PayPal, Stripe, HesabPay (Afghan MFS), in-process Cash adapter, and the future Adyen / Razorpay / M-Pesa / Apple Pay / Google Pay / stablecoin slots. It owns tokenized payment methods (per guest, per tenant), transactions, settlement state, refunds, voids, chargebacks, FX context, webhook ingestion, and per-vendor health.
It does not own the folio, charges, taxes or invoices (that is billing-service); it does not decide refund policy (caller reservation-service evaluates the rate plan and tells us what and how much to refund).
Purpose
- Be the only place in the platform where a vendor SDK is imported. Every other service speaks
PaymentPortand never sees aStripe.PaymentIntentorpaypal.checkout.OrdersCreateobject. - Treat cash-on-arrival as a first-class payment method with full transaction record, guarantee policies, no-show automation, and reconciliation against the desktop's cash-drawer events (10 §7).
- Minimize PCI scope to SAQ A by routing all card capture through processor-hosted UI surfaces (Stripe Elements, PayPal hosted fields), keeping PAN/CVV out of our network entirely, and isolating tokens via schema-per-tenant Postgres.
- Provide idempotent primitives so guest double-clicks, retried sagas, and replayed webhooks never double-charge.
- Provide deterministic reconciliation between platform records and processor settlement reports, with mismatches as alerts (never silent).
Key responsibilities
- Adapter routing — choose the correct vendor adapter from
(tenantId, propertyId, method.kind, currency, amount)against the tenant's configured adapter precedence; fall back per circuit-breaker health. - Authorize → capture lifecycle — supports both
manual(auth at booking, capture later) andautomatic(capture-immediately) flows; supports partial / multi-capture per adapter capability. - Tokenization — per-guest, per-tenant payment methods stored encrypted at rest with column-level KMS envelope; tokens are processor references, never card data.
- Refunds & voids — full or partial; void within the adapter's void window, refund after; idempotency-keyed so retries are safe.
- Webhook ingestion — signed (HMAC, mTLS, Stripe
t=scheme, PayPal cert) per-vendor receivers under/webhooks/v1/<vendor>; idempotent dedupe byexternalEventIdin the centralwebhook_inbox. - Daily reconciliation jobs — per processor, fetch settlement report, join against platform transactions, emit
reconciliation.completed.v1anddiscrepancy_found.v1. - Cash-on-arrival recording — no vendor call, but a full
Transactionrecord with syntheticprocessor: 'cash', reconciled againstbilling.cash_drawer.closed.v1events frombilling-serviceand the desktop. - FX context capture — at authorize time, snapshot
{ rate, source, quotedAt, provider }and persist on the transaction; never re-quote. - Per-vendor health & circuit-breaker — sliding-window error / latency tracking, automatic open / half-open / closed transitions,
adapter.health_changed.v1events for the booking funnel. - PCI-safe logging — mask PAN to last-4, never log CVV, never log raw cardholder data; logs scanned in CI for accidental exposure patterns.
- Chargeback workflow — webhook intake, evidence pack collection (booking record, lock access logs, folio history, guest comms), evidence submission, dispute lifecycle events.
Aggregates owned
| Aggregate | Cardinality | Purpose | Identity prefix |
|---|---|---|---|
Transaction | 1 per attempted payment movement (root) | Authorize → capture → refund / void lifecycle, FX, processor reference, status machine | pay_ |
Authorization | 0..N per transaction | Pre-auth holds; expiry; capture references | (composite, auth_) |
Capture | 0..N per transaction | Each capture against an authorization (multi-capture supported per adapter) | (composite, cap_) |
Refund | 0..N per transaction | Full or partial refund; references original capture | rfd_ |
Void | 0..1 per authorization | Releases an unused authorization | (composite) |
PaymentMethod | 0..N per (tenant, guest) | Tokenized, encrypted-at-rest payment instrument | pm_ |
Webhook | 1 per processor event | Ingested webhook, signed, deduped | whk_ |
Reconciliation | 1 per (processor, date) | Daily settlement report ingestion + match | rec_ |
Chargeback | 0..N per transaction | Dispute lifecycle, evidence, outcome | cbk_ |
FxContext | 1 per transaction (when cross-currency) | Rate, source, quotedAt; immutable once persisted | (composite) |
AdapterHealth | 1 per (vendor, env) | Sliding-window health snapshot, circuit state | (composite) |
Key APIs (REST, /api/v1/payments)
| Method | Path | Purpose |
|---|---|---|
POST | /intents | Create a payment intent (authorize); body picks method.kind |
POST | /intents/:id/capture | Capture (full or partial) against an authorization |
POST | /intents/:id/void | Void an authorization within the void window |
POST | /intents/:id/refunds | Create a refund (full or partial) |
GET | /intents/:id | Read a transaction with full event timeline |
POST | /payment-methods/sessions | Create a tokenization session (returns hosted-fields client secret) |
POST | /payment-methods | Attach a tokenized method to a guest (server-confirmed after hosted capture) |
DELETE | /payment-methods/:id | Detach (delete server-side reference; processor token deleted async) |
POST | /cash/receipts | Record a cash receipt (front-desk; audited) |
POST | /cash/refunds | Record a cash refund (front-desk; dual sign-off) |
GET | /transactions/:id | Alias of /intents/:id for billing-side callers |
POST | /reconciliations/run | Trigger an ad-hoc reconciliation job (ops only) |
GET | /reconciliations/:id | Fetch a reconciliation report |
POST | /chargebacks/:id/evidence | Submit chargeback evidence pack |
POST | /webhooks/v1/stripe | Stripe webhook receiver (signed) |
POST | /webhooks/v1/paypal | PayPal webhook receiver (signed) |
POST | /webhooks/v1/hesabpay | HesabPay webhook receiver (HMAC) |
GET | /adapters/health | Per-tenant adapter health snapshot |
Consumed by bff-tenant-booking-service (guest funnel — tokenization sessions, intent creation), bff-backoffice-service (front-desk — cash, refunds, chargebacks), and only by reservation-service for saga-driven authorize / capture / refund. billing-service consumes our events but never calls our APIs.
Key events published
| Event | Trigger |
|---|---|
melmastoon.payment.transaction.created.v1 | New payment intent persisted (pre-authorize) |
melmastoon.payment.transaction.authorized.v1 | Authorization succeeded at processor |
melmastoon.payment.transaction.captured.v1 | Capture succeeded (full or partial) |
melmastoon.payment.transaction.refunded.v1 | Refund succeeded (full or partial) |
melmastoon.payment.transaction.voided.v1 | Authorization voided before capture |
melmastoon.payment.transaction.failed.v1 | Authorize / capture / refund failed (non-retriable) |
melmastoon.payment.method.tokenized.v1 | Hosted tokenization completed; method attached |
melmastoon.payment.method.detached.v1 | Method detached / processor token deleted |
melmastoon.payment.webhook.received.v1 | Webhook signed + queued |
melmastoon.payment.webhook.processed.v1 | Webhook applied to platform state |
melmastoon.payment.webhook.duplicate_dropped.v1 | Webhook deduped via externalEventId |
melmastoon.payment.reconciliation.completed.v1 | Daily reconciliation job ran cleanly |
melmastoon.payment.reconciliation.discrepancy_found.v1 | Mismatch detected (platform-only / processor-only) |
melmastoon.payment.chargeback.received.v1 | Dispute opened by issuer |
melmastoon.payment.chargeback.evidence_submitted.v1 | Evidence pack submitted to processor |
melmastoon.payment.chargeback.won.v1 | Dispute resolved in tenant's favor |
melmastoon.payment.chargeback.lost.v1 | Dispute resolved against tenant |
melmastoon.payment.adapter.health_changed.v1 | Circuit-breaker opened / half-opened / closed |
Key events consumed
| Event | Effect |
|---|---|
melmastoon.reservation.held.v1 | Saga step: create payment intent (authorize per rate plan) and reply |
melmastoon.reservation.confirmed.v1 | Saga step: capture (or stay pending_cash for cash-on-arrival) |
melmastoon.reservation.cancelled.v1 | Saga step: refund per refund payload (caller computed policy) or void if not yet captured |
melmastoon.reservation.no_show.v1 | If card_guarantee: capture first-night charge from held card |
melmastoon.tenant.config_updated.v1 | Refresh adapter precedence, vendor credentials pointer |
melmastoon.tenant.finance_schemas.provisioned.v1 | Activate the new tenant (start serving requests for tenant_<uuid>_payments) |
melmastoon.tenant.finance_schemas.dropped.v1 | Stop serving; flush per-tenant connection pool |
melmastoon.billing.cash_drawer.closed.v1 | Match cash drawer close to recorded cash receipts; emit discrepancy_found.v1 if mismatch |
Upstream / downstream
Upstream (we consume): reservation-service (saga driver), tenant-service (provisioning), billing-service (cash drawer).
Downstream (we publish for): reservation-service (saga reactions), billing-service (folio postings driven by capture/refund), notification-service (guest receipts, chargeback alerts), analytics-service (revenue & cost), audit-service (regulated events), ai-orchestrator-service (anomaly fraud detection), bff-backoffice-service (cash + dispute UX), sync-service (cash drawer projection to desktop).
Non-functional requirements
| NFR | Target |
|---|---|
authorize p99 (network adapters) | < 1.5 s including processor RTT |
capture p99 (network adapters) | < 1.5 s |
| Webhook processing latency p99 | < 5 s receive → state applied |
| Reconciliation success rate | > 99.9% monthly (matched / total) |
| Idempotency dedupe correctness | 100% — zero duplicate captures (CI-enforced chaos test) |
| Tenant isolation (PCI) | Schema-per-tenant; cross-schema access structurally impossible |
| API availability | 99.95% monthly |
| Replicas | Min 3 Cloud Run instances (hot path); separate webhook-receiver service; separate reconciliation cron worker |
| PCI scope | SAQ A target; annual review with QSA |
Where to go next
- Implementation-grade detail:
services/payment-gateway-service/SERVICE_OVERVIEW.mdand the rest of the 17-doc bundle. - Full payments thesis (cash-first, MFS, Sharia option, FX, refund DSL):
docs/10-payments-architecture.md. - Schema-per-tenant rationale:
docs/architecture/ADR-0002. - Booking saga orchestration end-to-end:
docs/04-event-driven-architecture.md. - Error catalog (PAYMENT, BILLING):
docs/standards/ERROR_CODES.md. - Desktop cash-drawer UX:
docs/frontend/desktop/06-desktop-app-specification.md.