EVENT_SCHEMAS — billing-service
Conforms to 04 Event-Driven Architecture. All events are CloudEvents 1.0 envelopes carried over GCP Pub/Sub. Subjects use
melmastoon.<service>.<aggregate>.<verb-past-tense>.v<n>per standards/NAMING.md. Payloads are versioned (.v1,.v2); the wire schema for a major version is frozen.
1. Envelope
{
"specversion": "1.0",
"id": "evt_01HW...",
"source": "service:billing",
"type": "melmastoon.billing.folio.opened.v1",
"time": "2026-04-22T08:14:11.123Z",
"datacontenttype": "application/json",
"subject": "fol_01HW...",
"tenantid": "t_01HW...",
"traceparent": "00-...-...-01",
"data": { ... }
}
Pub/Sub message attributes mirror tenantid, type, subject, and traceparent so subscribers can filter without parsing JSON.
2. Topics published by billing-service
| Topic | Type | Partitioning attr | Min subscribers |
|---|---|---|---|
melmastoon.billing.folio | folio.*.v1 | tenantid | analytics, reporting, audit, sync, bff-backoffice |
melmastoon.billing.invoice | invoice.*.v1, credit_note.*.v1 | tenantid | notification, analytics, reporting, audit |
melmastoon.billing.cash_drawer | cash_drawer.*.v1 | tenantid | audit, reporting, bff-backoffice, notification (discrepancy) |
melmastoon.billing.subscription | subscription.*.v1 | tenantid | tenant-service, notification, analytics, reporting, audit |
melmastoon.billing.usage | usage.recorded.v1 | tenantid | analytics, reporting, billing (cycle worker) |
DLQs follow <topic>.dlq with a 14-day retention.
3. Folio events
melmastoon.billing.folio.opened.v1
{
"folioId": "fol_01HW...",
"tenantId": "t_01HW...",
"propertyId": "prop_01HW...",
"reservationId": "res_01HW...",
"currency": "AFN",
"fxSnapshot": {
"baseCurrency": "AFN",
"rates": { "USD": "70000000", "EUR": "76000000" },
"takenAt": "2026-04-15T10:00:00Z",
"source": "pricing-service:rate-plan-snapshot:rps_01HW..."
},
"openedAt": "2026-04-22T08:14:11Z",
"openedReason": "reservation.confirmed.v1"
}
JSON-Schema (excerpt):
type: object
required: [folioId, tenantId, propertyId, reservationId, currency, openedAt]
properties:
folioId: { type: string, pattern: "^fol_" }
tenantId: { type: string, pattern: "^t_" }
currency: { type: string, enum: [AFN,USD,EUR,PKR,SAR,AED,TJS,IRR,GBP,TRY] }
openedAt: { type: string, format: date-time }
openedReason: { type: string, enum: [reservation.confirmed.v1, reservation.checked_in.v1, manual] }
melmastoon.billing.folio.charge_added.v1
{
"folioId": "fol_01HW...",
"chargeId": "chg_01HW...",
"tenantId": "t_01HW...",
"kind": "mini_bar",
"description": { "default": "Mini-bar — Coca-Cola 330ml ×2" },
"quantity": 2,
"gross": { "amountMicro": "150000000", "currency": "AFN" },
"tax": { "code": "VAT_STANDARD", "amount": { "amountMicro": "15000000", "currency": "AFN" }, "rateNumerator": "10", "rateDenominator": "100", "jurisdiction": "AF-KBL" },
"postedAt": "2026-04-22T08:30:00Z",
"postedBy": "actor_01HW...",
"source": { "kind": "pos", "ref": "pos_ticket_482" }
}
melmastoon.billing.folio.payment_recorded.v1
{
"folioId": "fol_01HW...",
"paymentId": "fpm_01HW...",
"tenantId": "t_01HW...",
"method": "cash",
"amount": { "amountMicro": "200000000", "currency": "AFN" },
"externalPaymentId": null,
"cashSessionId": "cds_01HW...",
"recordedAt": "2026-04-22T11:01:00Z",
"recordedBy": "actor_01HW...",
"metadata": { "location": "front_desk" }
}
melmastoon.billing.folio.refund_recorded.v1
{
"folioId": "fol_01HW...",
"refundId": "frd_01HW...",
"tenantId": "t_01HW...",
"amount": { "amountMicro": "50000000", "currency": "AFN" },
"reason": "Mini-bar item double-charged",
"externalRefundId": null,
"cashSessionId": "cds_01HW...",
"refundedAt": "2026-04-22T11:30:00Z",
"refundedBy": "actor_01HW..."
}
melmastoon.billing.folio.closed.v1
{
"folioId": "fol_01HW...",
"tenantId": "t_01HW...",
"settlement": {
"id": "set_01HW...",
"perCurrencyTotals": [
{ "currency": "AFN", "charges": { "amountMicro": "350000000", "currency": "AFN" }, "payments": { "amountMicro": "350000000", "currency": "AFN" }, "refunds": { "amountMicro": "0", "currency": "AFN" } }
],
"residual": { "amountMicro": "0", "currency": "AFN" }
},
"closedAt": "2026-04-22T11:02:33Z",
"closedBy": "actor_01HW..."
}
melmastoon.billing.folio.balance_due.v1
Emitted on a failed close attempt where balance > 0.
{
"folioId": "fol_01HW...",
"tenantId": "t_01HW...",
"balance": { "amountMicro": "100000000", "currency": "AFN" },
"attemptedAt": "2026-04-22T11:02:33Z",
"attemptedBy": "actor_01HW..."
}
melmastoon.billing.folio.reopened.v1
{
"folioId": "fol_01HW...",
"tenantId": "t_01HW...",
"by": "actor_01HW...",
"reason": "Restaurant charge added 30 min after checkout",
"voidedInvoiceId": "inv_doc_01HW...",
"at": "2026-04-22T11:35:00Z"
}
4. Invoice & credit-note events
melmastoon.billing.invoice.generated.v1
{
"invoiceId": "inv_doc_01HW...",
"tenantId": "t_01HW...",
"folioId": "fol_01HW...",
"number": "INV-AF-2026-000142",
"currency": "AFN",
"subtotal": { "amountMicro": "318181818", "currency": "AFN" },
"taxTotal": { "amountMicro": "31818182", "currency": "AFN" },
"grandTotal": { "amountMicro": "350000000", "currency": "AFN" },
"locale": "ps-AF",
"template": "standard",
"customer": { "class": "individual", "name": "Asma Rashid", "vatNumber": null },
"pdfUri": "gs://billing-invoices-t_01HW.../2026/04/INV-AF-2026-000142.pdf",
"issuedAt": "2026-04-22T11:02:33Z"
}
melmastoon.billing.invoice.sent.v1
{
"invoiceId": "inv_doc_01HW...",
"tenantId": "t_01HW...",
"channel": "email",
"to": "asma@example.com",
"notificationId": "ntf_01HW...",
"sentAt": "2026-04-22T11:03:14Z"
}
melmastoon.billing.invoice.voided.v1
{ "invoiceId": "inv_doc_01HW...", "tenantId": "t_01HW...", "reason": "Folio re-opened", "voidedAt": "2026-04-22T11:35:00Z" }
melmastoon.billing.credit_note.generated.v1
{
"creditNoteId": "cnt_01HW...",
"tenantId": "t_01HW...",
"invoiceId": "inv_doc_01HW...",
"number": "CN-AF-2026-000017",
"currency": "AFN",
"total": { "amountMicro": "15000000", "currency": "AFN" },
"lines": [ { "originalLineId": "ln_01HW...", "amount": { "amountMicro": "15000000", "currency": "AFN" }, "reason": "Mini-bar double-charge" } ],
"issuedAt": "2026-04-22T13:14:00Z"
}
5. Cash drawer events
melmastoon.billing.cash_drawer.opened.v1
{
"sessionId": "cds_01HW...",
"drawerId": "cdr_01HW...",
"tenantId": "t_01HW...",
"propertyId": "prop_01HW...",
"openingFloat": { "amountMicro": "5000000000", "currency": "AFN" },
"openedBy": "actor_01HW...",
"openedAt": "2026-04-22T06:00:00Z",
"shiftLabel": "Day"
}
melmastoon.billing.cash_drawer.closed.v1
{
"sessionId": "cds_01HW...",
"drawerId": "cdr_01HW...",
"tenantId": "t_01HW...",
"expectedClosingFloat": { "amountMicro": "8500000000", "currency": "AFN" },
"countedClosingFloat": { "amountMicro": "8500000000", "currency": "AFN" },
"variance": { "amountMicro": "0", "currency": "AFN" },
"closedBy": "actor_01HW...",
"coSigner": "actor_01HW...",
"closedAt": "2026-04-22T22:14:00Z",
"stepUpEvidence": { "stepUpTokenId": "stp_01HW...", "method": "totp" }
}
melmastoon.billing.cash_drawer.discrepancy_found.v1
{
"sessionId": "cds_01HW...",
"drawerId": "cdr_01HW...",
"tenantId": "t_01HW...",
"variance": { "amountMicro": "-300000000", "currency": "AFN" },
"thresholdMicro": "100000000",
"detectedAt": "2026-04-22T22:14:00Z"
}
6. Subscription events
melmastoon.billing.subscription.created.v1
{
"subscriptionId": "sub_01HW...",
"tenantId": "t_01HW...",
"plan": { "code": "STARTER_PER_ROOM", "base": { "kind": "per_room_month", "perRoomAmount": { "amountMicro": "3000000000", "currency": "USD" } }, "meters": [] },
"currency": "USD",
"cycleAnchor": 1,
"createdAt": "2026-01-12T08:00:00Z"
}
melmastoon.billing.subscription.invoice_generated.v1
{
"subscriptionId": "sub_01HW...",
"tenantId": "t_01HW...",
"invoice": {
"id": "sin_01HW...",
"period": "2026-04",
"currency": "USD",
"subtotal": { "amountMicro": "30000000000", "currency": "USD" },
"taxTotal": { "amountMicro": "0", "currency": "USD" },
"grandTotal": { "amountMicro": "30000000000", "currency": "USD" },
"lines": [
{ "kind": "base_per_room", "rooms": 10, "amount": { "amountMicro": "30000000000", "currency": "USD" } },
{ "kind": "ai_overage", "units": "0", "amount": { "amountMicro": "0", "currency": "USD" } }
],
"issuedAt": "2026-05-01T00:05:00Z",
"dueAt": "2026-05-08T00:00:00Z"
}
}
melmastoon.billing.subscription.payment_failed.v1
{
"subscriptionId": "sub_01HW...",
"tenantId": "t_01HW...",
"subscriptionInvoiceId": "sin_01HW...",
"state": "grace",
"reason": "MELMASTOON.PAYMENT.PROCESSOR_DECLINED",
"attempt": 1,
"nextRetryAt": "2026-05-04T00:00:00Z",
"at": "2026-05-01T00:06:00Z"
}
melmastoon.billing.subscription.cancelled.v1
{
"subscriptionId": "sub_01HW...",
"tenantId": "t_01HW...",
"reason": "past_due",
"at": "2026-05-15T00:00:00Z"
}
melmastoon.billing.subscription.reactivated.v1
{
"subscriptionId": "sub_01HW...",
"tenantId": "t_01HW...",
"by": "platform_actor_01HW...",
"at": "2026-05-20T10:00:00Z"
}
7. Usage event
melmastoon.billing.usage.recorded.v1
{
"tenantId": "t_01HW...",
"period": "2026-04",
"meter": "ai_tokens",
"totalUnits": "152340",
"snapshotAt": "2026-04-30T23:55:00Z"
}
8. Events consumed
| Subject | Inbox key | Effect |
|---|---|---|
melmastoon.reservation.confirmed.v1 | inbox:billing:reservation.confirmed.v1:<eventId> | If tenant policy eager, run OpenFolioUseCase |
melmastoon.reservation.checked_in.v1 | …:checked_in.v1:<eventId> | OpenFolioUseCase if not yet; post arrival-day room-night charges |
melmastoon.reservation.checked_out.v1 | …:checked_out.v1:<eventId> | CloseFolioUseCase |
melmastoon.reservation.cancelled.v1 | …:cancelled.v1:<eventId> | Compute refund per policy; RecordRefundUseCase; IssueCreditNoteUseCase |
melmastoon.payment.transaction.captured.v1 | …:payment.captured.v1:<paymentId> | RecordExternalPaymentUseCase (folio) or RecordSubscriptionPaymentResultUseCase (sub) |
melmastoon.payment.transaction.refunded.v1 | …:payment.refunded.v1:<refundId> | Reconcile against FolioRefund |
melmastoon.tenant.created.v1 | …:tenant.created.v1:<tenantId> | Provision per-tenant billing schema; InitializeSubscriptionUseCase |
melmastoon.property.room.activated.v1 | …:room.activated.v1:<roomId> | RecordUsageUseCase('rooms') |
melmastoon.ai_orchestrator.completion.recorded.v1 | …:ai.completion.v1:<eventId> | RecordUsageUseCase('ai_tokens') |
melmastoon.file_storage.bytes.measured.v1 | …:storage.bytes.v1:<eventId> | RecordUsageUseCase('storage_bytes') |
Inbox dedupe TTL is 7 days for inbound events; replayed messages outside that window log a warning and proceed (the underlying aggregate state is the source of truth and rejects re-application).
9. Versioning, deprecation, evolution
- Backward-compatible additions (new optional fields, new enum values where consumers fall back to
unknown) ship under the same.v1. - Breaking changes ship a
.v2topic + payload alongside.v1; both are emitted for one quarter;.v1is then retired with a deprecation notice innotification-serviceto internal consumers. - The folio
closed.v1settlement.perCurrencyTotalsarray is the canonical per-currency breakdown; downstream services must not assume a single-currency total.
10. Audit & retention
All published events are persisted in audit-service for 7 years. The outbox table itself is pruned after 30 days; the audit topic carries the long-term tail.