Skip to main content

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

TopicTypePartitioning attrMin subscribers
melmastoon.billing.foliofolio.*.v1tenantidanalytics, reporting, audit, sync, bff-backoffice
melmastoon.billing.invoiceinvoice.*.v1, credit_note.*.v1tenantidnotification, analytics, reporting, audit
melmastoon.billing.cash_drawercash_drawer.*.v1tenantidaudit, reporting, bff-backoffice, notification (discrepancy)
melmastoon.billing.subscriptionsubscription.*.v1tenantidtenant-service, notification, analytics, reporting, audit
melmastoon.billing.usageusage.recorded.v1tenantidanalytics, 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

SubjectInbox keyEffect
melmastoon.reservation.confirmed.v1inbox: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 .v2 topic + payload alongside .v1; both are emitted for one quarter; .v1 is then retired with a deprecation notice in notification-service to internal consumers.
  • The folio closed.v1 settlement.perCurrencyTotals array 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.