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
| Direction | Topic | Subscribers (initial) |
|---|---|---|
| out | melmastoon.payment.transaction | billing-service, reservation-service, analytics-service, notification-service |
| out | melmastoon.payment.method | billing-service, audit-log-service |
| out | melmastoon.payment.webhook | analytics-service (volume), audit-log-service |
| out | melmastoon.payment.reconciliation | billing-service, analytics-service, notification-service (alerts) |
| out | melmastoon.payment.chargeback | billing-service, notification-service, audit-log-service, ai-orchestrator-service |
| out | melmastoon.payment.adapter | notification-service (status pages), analytics-service |
| in | melmastoon.reservation (filtered) | this service consumes held.v1, confirmed.v1, cancelled.v1 |
| in | melmastoon.tenant | this service consumes config_updated.v1 |
| in | melmastoon.billing | this 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 requiresvN+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-registrywithdeprecatedAt.
5. Idempotency & ordering
- Each event carries a stable
id; consumers dedupe on(tenantId, type, id). - Ordering within a single
paymentIdis 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:
- Changes a published schema in a non-additive way.
- Publishes a new event without a schema or without registry entry.
- Drops a
dataschemaURL not resolvable in the schema-registry bucket.