Skip to main content

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 PaymentPort and never sees a Stripe.PaymentIntent or paypal.checkout.OrdersCreate object.
  • 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

  1. 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.
  2. Authorize → capture lifecycle — supports both manual (auth at booking, capture later) and automatic (capture-immediately) flows; supports partial / multi-capture per adapter capability.
  3. Tokenization — per-guest, per-tenant payment methods stored encrypted at rest with column-level KMS envelope; tokens are processor references, never card data.
  4. Refunds & voids — full or partial; void within the adapter's void window, refund after; idempotency-keyed so retries are safe.
  5. Webhook ingestion — signed (HMAC, mTLS, Stripe t= scheme, PayPal cert) per-vendor receivers under /webhooks/v1/<vendor>; idempotent dedupe by externalEventId in the central webhook_inbox.
  6. Daily reconciliation jobs — per processor, fetch settlement report, join against platform transactions, emit reconciliation.completed.v1 and discrepancy_found.v1.
  7. Cash-on-arrival recording — no vendor call, but a full Transaction record with synthetic processor: 'cash', reconciled against billing.cash_drawer.closed.v1 events from billing-service and the desktop.
  8. FX context capture — at authorize time, snapshot { rate, source, quotedAt, provider } and persist on the transaction; never re-quote.
  9. Per-vendor health & circuit-breaker — sliding-window error / latency tracking, automatic open / half-open / closed transitions, adapter.health_changed.v1 events for the booking funnel.
  10. PCI-safe logging — mask PAN to last-4, never log CVV, never log raw cardholder data; logs scanned in CI for accidental exposure patterns.
  11. Chargeback workflow — webhook intake, evidence pack collection (booking record, lock access logs, folio history, guest comms), evidence submission, dispute lifecycle events.

Aggregates owned

AggregateCardinalityPurposeIdentity prefix
Transaction1 per attempted payment movement (root)Authorize → capture → refund / void lifecycle, FX, processor reference, status machinepay_
Authorization0..N per transactionPre-auth holds; expiry; capture references(composite, auth_)
Capture0..N per transactionEach capture against an authorization (multi-capture supported per adapter)(composite, cap_)
Refund0..N per transactionFull or partial refund; references original capturerfd_
Void0..1 per authorizationReleases an unused authorization(composite)
PaymentMethod0..N per (tenant, guest)Tokenized, encrypted-at-rest payment instrumentpm_
Webhook1 per processor eventIngested webhook, signed, dedupedwhk_
Reconciliation1 per (processor, date)Daily settlement report ingestion + matchrec_
Chargeback0..N per transactionDispute lifecycle, evidence, outcomecbk_
FxContext1 per transaction (when cross-currency)Rate, source, quotedAt; immutable once persisted(composite)
AdapterHealth1 per (vendor, env)Sliding-window health snapshot, circuit state(composite)

Key APIs (REST, /api/v1/payments)

MethodPathPurpose
POST/intentsCreate a payment intent (authorize); body picks method.kind
POST/intents/:id/captureCapture (full or partial) against an authorization
POST/intents/:id/voidVoid an authorization within the void window
POST/intents/:id/refundsCreate a refund (full or partial)
GET/intents/:idRead a transaction with full event timeline
POST/payment-methods/sessionsCreate a tokenization session (returns hosted-fields client secret)
POST/payment-methodsAttach a tokenized method to a guest (server-confirmed after hosted capture)
DELETE/payment-methods/:idDetach (delete server-side reference; processor token deleted async)
POST/cash/receiptsRecord a cash receipt (front-desk; audited)
POST/cash/refundsRecord a cash refund (front-desk; dual sign-off)
GET/transactions/:idAlias of /intents/:id for billing-side callers
POST/reconciliations/runTrigger an ad-hoc reconciliation job (ops only)
GET/reconciliations/:idFetch a reconciliation report
POST/chargebacks/:id/evidenceSubmit chargeback evidence pack
POST/webhooks/v1/stripeStripe webhook receiver (signed)
POST/webhooks/v1/paypalPayPal webhook receiver (signed)
POST/webhooks/v1/hesabpayHesabPay webhook receiver (HMAC)
GET/adapters/healthPer-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

EventTrigger
melmastoon.payment.transaction.created.v1New payment intent persisted (pre-authorize)
melmastoon.payment.transaction.authorized.v1Authorization succeeded at processor
melmastoon.payment.transaction.captured.v1Capture succeeded (full or partial)
melmastoon.payment.transaction.refunded.v1Refund succeeded (full or partial)
melmastoon.payment.transaction.voided.v1Authorization voided before capture
melmastoon.payment.transaction.failed.v1Authorize / capture / refund failed (non-retriable)
melmastoon.payment.method.tokenized.v1Hosted tokenization completed; method attached
melmastoon.payment.method.detached.v1Method detached / processor token deleted
melmastoon.payment.webhook.received.v1Webhook signed + queued
melmastoon.payment.webhook.processed.v1Webhook applied to platform state
melmastoon.payment.webhook.duplicate_dropped.v1Webhook deduped via externalEventId
melmastoon.payment.reconciliation.completed.v1Daily reconciliation job ran cleanly
melmastoon.payment.reconciliation.discrepancy_found.v1Mismatch detected (platform-only / processor-only)
melmastoon.payment.chargeback.received.v1Dispute opened by issuer
melmastoon.payment.chargeback.evidence_submitted.v1Evidence pack submitted to processor
melmastoon.payment.chargeback.won.v1Dispute resolved in tenant's favor
melmastoon.payment.chargeback.lost.v1Dispute resolved against tenant
melmastoon.payment.adapter.health_changed.v1Circuit-breaker opened / half-opened / closed

Key events consumed

EventEffect
melmastoon.reservation.held.v1Saga step: create payment intent (authorize per rate plan) and reply
melmastoon.reservation.confirmed.v1Saga step: capture (or stay pending_cash for cash-on-arrival)
melmastoon.reservation.cancelled.v1Saga step: refund per refund payload (caller computed policy) or void if not yet captured
melmastoon.reservation.no_show.v1If card_guarantee: capture first-night charge from held card
melmastoon.tenant.config_updated.v1Refresh adapter precedence, vendor credentials pointer
melmastoon.tenant.finance_schemas.provisioned.v1Activate the new tenant (start serving requests for tenant_<uuid>_payments)
melmastoon.tenant.finance_schemas.dropped.v1Stop serving; flush per-tenant connection pool
melmastoon.billing.cash_drawer.closed.v1Match 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

NFRTarget
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 correctness100% — zero duplicate captures (CI-enforced chaos test)
Tenant isolation (PCI)Schema-per-tenant; cross-schema access structurally impossible
API availability99.95% monthly
ReplicasMin 3 Cloud Run instances (hot path); separate webhook-receiver service; separate reconciliation cron worker
PCI scopeSAQ A target; annual review with QSA

Where to go next