Skip to main content

EVENT_SCHEMAS — payment-gateway-service

Sibling: APPLICATION_LOGIC · DOMAIN_MODEL · API_CONTRACTS · DATA_MODEL

Strategic anchors: 04 Event-Driven Architecture · standards/NAMING

All events follow melmastoon.<service>.<aggregate>.<verb-past-tense>.v<n>. Transport is Google Pub/Sub (durable topics) for inter-service events and Cloud Tasks for outbox flush. Every event embeds a CloudEvents 1.0 envelope; the bodies below are the data payload. Schemas are validated against JSON Schema Draft 2020-12 in CI (schemas/payments/*.schema.json).

0. Envelope (CloudEvents 1.0)

{
"specversion": "1.0",
"id": "01HZX…",
"type": "melmastoon.payment.transaction.captured.v1",
"source": "//melmastoon/payment-gateway-service",
"subject": "tnt_01H…/payments/pay_01HZX…",
"time": "2026-04-22T18:31:42.815Z",
"datacontenttype": "application/json",
"dataschema": "https://schemas.melmastoon.ghasi.io/payment/transaction.captured.v1.json",
"tenantid": "tnt_01H…",
"correlationid": "rsv_01H3Z…",
"causationid": "01HZX…parent_event_id",
"traceparent": "00-…-01",
"data": { "...": "..." }
}

tenantid, correlationid, causationid are CloudEvents extensions registered platform-wide (see 04 EDA §envelope). Money in payloads uses the same { amountMicro, currency } shape as the API, but amountMicro is a JSON string (bigint-safe).

1. Topic & subscription map

DirectionTopicSubscribers (initial)
outmelmastoon.payment.transactionbilling-service, reservation-service, analytics-service, notification-service
outmelmastoon.payment.methodbilling-service, audit-log-service
outmelmastoon.payment.webhookanalytics-service (volume), audit-log-service
outmelmastoon.payment.reconciliationbilling-service, analytics-service, notification-service (alerts)
outmelmastoon.payment.chargebackbilling-service, notification-service, audit-log-service, ai-orchestrator-service
outmelmastoon.payment.adapternotification-service (status pages), analytics-service
inmelmastoon.reservation (filtered)this service consumes held.v1, confirmed.v1, cancelled.v1
inmelmastoon.tenantthis service consumes config_updated.v1
inmelmastoon.billingthis service consumes cash_drawer.shift_closed.v1 for cash recon

Each subscriber owns a Pub/Sub subscription with retry policy min=10s, max=600s, dead-letter topic <topic>.dlq after 7 attempts.


2. Outbound events

2.1 melmastoon.payment.transaction.created.v1

Emitted on intent creation, before adapter call. Used by analytics + audit.

{
"paymentId": "pay_01HZX…",
"tenantId": "tnt_01H…",
"propertyId": "ppt_01H…",
"reservationId": "rsv_01H3Z…",
"guestId": "gst_01H3Z…",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"method": "card",
"processor": "stripe",
"fxContext": { "base": "USD", "quote": "AFN", "rate": 71.50, "source": "ecb", "quotedAt": "2026-04-22T18:31:00Z" },
"initiatedBy": { "type": "guest", "id": "gst_01H3Z…" },
"occurredAt": "2026-04-22T18:31:00.123Z"
}

2.2 melmastoon.payment.transaction.authorized.v1

Emitted after a successful authorize. expiresAt lets reservation-service decide capture timing.

{
"paymentId": "pay_01HZX…",
"authorizationId": "auth_01HZX…",
"tenantId": "tnt_01H…",
"reservationId": "rsv_01H3Z…",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"processor": "stripe",
"processorRef": "pi_3NXzABCdef",
"expiresAt": "2026-04-29T18:31:00Z",
"occurredAt": "2026-04-22T18:31:01.451Z"
}

2.3 melmastoon.payment.transaction.captured.v1

Emitted on capture (real adapter or cash). Drives billing-service folio posting.

{
"paymentId": "pay_01HZX…",
"captureId": "cap_01HZX…",
"tenantId": "tnt_01H…",
"reservationId": "rsv_01H3Z…",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"processor": "stripe",
"processorRef": "ch_3NXzABCdef",
"method": "card",
"capturedAt": "2026-04-22T18:31:42.815Z",
"occurredAt": "2026-04-22T18:31:42.815Z"
}

2.4 melmastoon.payment.transaction.refunded.v1

{
"paymentId": "pay_01HZX…",
"refundId": "rfd_01HZX…",
"tenantId": "tnt_01H…",
"reservationId": "rsv_01H3Z…",
"amount": { "amountMicro": "200000000", "currency": "USD" },
"reason": "cancellation_within_policy",
"processor": "stripe",
"processorRef": "re_3NXzABCdef",
"method": "card",
"refundedAt": "2026-04-22T18:32:10.001Z",
"occurredAt": "2026-04-22T18:32:10.001Z"
}

2.5 melmastoon.payment.transaction.voided.v1

{
"paymentId": "pay_01HZX…",
"voidId": "vd_01HZX…",
"tenantId": "tnt_01H…",
"reservationId":"rsv_01H3Z…",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"reason": "saga_compensation",
"processor": "stripe",
"processorRef":"pi_3NXzABCdef",
"voidedAt": "2026-04-22T18:31:55Z",
"occurredAt": "2026-04-22T18:31:55Z"
}

2.6 melmastoon.payment.transaction.failed.v1

Terminal failure (decline, network exhaustion, hard error). errorCode mirrors ERROR_CODES.

{
"paymentId": "pay_01HZX…",
"tenantId": "tnt_01H…",
"reservationId": "rsv_01H3Z…",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"processor": "stripe",
"errorCode": "MELMASTOON.PAYMENT.DECLINED",
"processorCode": "card_declined",
"retriable": false,
"failedAt": "2026-04-22T18:31:01Z",
"occurredAt": "2026-04-22T18:31:01Z"
}

2.7 melmastoon.payment.method.tokenized.v1

{
"paymentMethodId": "pm_01HZX…",
"tenantId": "tnt_01H…",
"guestId": "gst_01H3Z…",
"kind": "card",
"processor": "stripe",
"display": { "brand": "visa", "last4": "4242", "expMonth": 12, "expYear": 2028 },
"tokenizedAt": "2026-04-22T18:32:00Z",
"occurredAt": "2026-04-22T18:32:00Z"
}

2.8 melmastoon.payment.method.detached.v1

{
"paymentMethodId": "pm_01HZX…",
"tenantId": "tnt_01H…",
"guestId": "gst_01H3Z…",
"reason": "guest_request",
"detachedAt": "2026-04-22T18:33:00Z",
"occurredAt": "2026-04-22T18:33:00Z"
}

2.9 melmastoon.payment.webhook.received.v1

{
"webhookId": "whk_01HZX…",
"tenantId": "tnt_01H…",
"processor": "stripe",
"externalEventId": "evt_3NXzABCdef",
"eventType": "payment_intent.succeeded",
"receivedAt": "2026-04-22T18:31:50.001Z",
"occurredAt": "2026-04-22T18:31:50.001Z"
}

2.10 melmastoon.payment.webhook.processed.v1

{
"webhookId": "whk_01HZX…",
"tenantId": "tnt_01H…",
"processor": "stripe",
"externalEventId": "evt_3NXzABCdef",
"outcome": "applied",
"appliedTo": { "paymentId": "pay_01HZX…", "transition": "authorized→captured" },
"processedAt": "2026-04-22T18:31:50.412Z",
"occurredAt": "2026-04-22T18:31:50.412Z"
}

2.11 melmastoon.payment.webhook.duplicate_dropped.v1

{
"webhookId": "whk_01HZX…",
"tenantId": "tnt_01H…",
"processor": "stripe",
"externalEventId": "evt_3NXzABCdef",
"firstSeenAt": "2026-04-22T18:31:50.001Z",
"droppedAt": "2026-04-22T18:34:01.117Z",
"occurredAt": "2026-04-22T18:34:01.117Z"
}

2.12 melmastoon.payment.reconciliation.completed.v1

{
"reconciliationId": "rec_01HZX…",
"tenantId": "tnt_01H…",
"processor": "stripe",
"date": "2026-04-21",
"matched": { "count": 412, "total": { "amountMicro": "412000000000", "currency": "AFN" }},
"unmatched": { "count": 1, "total": { "amountMicro": "560000000", "currency": "AFN" }},
"fees": { "amountMicro": "12450000000", "currency": "AFN" },
"net": { "amountMicro": "399550000000", "currency": "AFN" },
"completedAt": "2026-04-22T02:05:43Z",
"occurredAt": "2026-04-22T02:05:43Z"
}

2.13 melmastoon.payment.reconciliation.discrepancy_found.v1

{
"reconciliationId": "rec_01HZX…",
"tenantId": "tnt_01H…",
"processor": "stripe",
"date": "2026-04-21",
"discrepancies": [
{ "side": "platform_only", "paymentId": "pay_01HZX…", "amount": { "amountMicro": "560000000", "currency": "AFN" }, "suspectedReason": "webhook_drop" },
{ "side": "vendor_only", "vendorRef": "ch_3NX…X1", "amount": { "amountMicro": "120000000", "currency": "AFN" }, "suspectedReason": "out_of_band_capture" }
],
"occurredAt": "2026-04-22T02:05:43Z"
}

2.14 melmastoon.payment.chargeback.received.v1

{
"chargebackId": "cbk_01HZX…",
"paymentId": "pay_01HZX…",
"tenantId": "tnt_01H…",
"reservationId": "rsv_01H3Z…",
"processor": "stripe",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"reason": "fraudulent",
"deadlineAt": "2026-05-06T23:59:59Z",
"fraudSignal": false,
"occurredAt": "2026-04-22T18:35:00Z"
}

For cash payments, fraudSignal: true and the body adds "impossible": true.

2.15 melmastoon.payment.chargeback.evidence_submitted.v1

{
"chargebackId": "cbk_01HZX…",
"tenantId": "tnt_01H…",
"submittedAt": "2026-04-23T11:00:00Z",
"bundleRef": "gs://gm-disputes/tnt_01H…/cbk_01HZX…/bundle.zip",
"aiAssisted": true,
"aiProvenanceId": "dec_01HZX…",
"occurredAt": "2026-04-23T11:00:00Z"
}

2.16 melmastoon.payment.chargeback.won.v1 / .lost.v1

{
"chargebackId": "cbk_01HZX…",
"tenantId": "tnt_01H…",
"outcome": "won", // or "lost"
"settledAmount": { "amountMicro": "0", "currency": "USD" },
"decidedAt": "2026-05-12T08:00:00Z",
"occurredAt": "2026-05-12T08:00:00Z"
}

2.17 melmastoon.payment.adapter.health_changed.v1

{
"tenantId": "tnt_01H…",
"processor": "hesabpay",
"previousState":"closed",
"currentState": "open",
"errorRate1m": 0.42,
"p99LatencyMs": 9100,
"openedAt": "2026-04-22T18:30:00Z",
"expectedHalfOpenAt": "2026-04-22T18:33:00Z",
"occurredAt": "2026-04-22T18:30:00Z"
}

3. Inbound events (consumed)

3.1 melmastoon.reservation.held.v1

Saga-trigger for AuthorizePayment. Required fields: reservationId, tenantId, propertyId, guestId, amount, fxContext, paymentMethodId.

3.2 melmastoon.reservation.confirmed.v1

Saga-trigger for CapturePayment (or no-op for cash_on_arrival).

3.3 melmastoon.reservation.cancelled.v1

Saga-trigger for RefundPayment or VoidPayment depending on transaction state. Body includes refundDirective: { amount, reason } already evaluated by reservation-service policy DSL — this service does not re-evaluate.

3.4 melmastoon.tenant.config_updated.v1

Filtered to config.section ∈ { 'payments.adapter_precedence', 'payments.cash_thresholds', 'payments.fx_provider' }.

3.5 melmastoon.billing.cash_drawer.shift_closed.v1

Used by RunDailyReconciliation to compare cash captures with drawer totals.


4. Versioning, evolution, compatibility

  • Additive only within vN. Removing or renaming a field requires vN+1.
  • A new event version coexists with the old until all consumers acknowledge the new schema (tracked in service-registry).
  • Consumers must ignore unknown fields.
  • The deprecation window is 90 days minimum; flagged in event-registry with deprecatedAt.

5. Idempotency & ordering

  • Each event carries a stable id; consumers dedupe on (tenantId, type, id).
  • Ordering within a single paymentId is guaranteed via Pub/Sub ordering keys (ordering_key = paymentId).
  • Cross-payment ordering is not guaranteed; consumers must not assume it.

6. Schema registry

JSON Schemas live in ghasi-melmastoon repo at /schemas/payments/<event>.<version>.schema.json. CI (pnpm event:check) fails any PR that:

  1. Changes a published schema in a non-additive way.
  2. Publishes a new event without a schema or without registry entry.
  3. Drops a dataschema URL not resolvable in the schema-registry bucket.