billing-service
Bounded Context: Billing & Folio (Core) · Owner: Finance · Phase: 0 · Storage: Cloud SQL Postgres schema-per-tenant for guest folios + central schema for tenant subscription billing + transactional outbox · Bundle: services/billing-service/
billing-service owns two distinct sub-domains under one service: (a) the Guest Folio — the financial ledger attached to every reservation, with charges, taxes, payments, refunds, multi-currency settlement, and a per-stay invoice; and (b) Tenant Subscription Billing — the platform's own billing of its tenants for using Ghasi Melmastoon (per-room-month or flat plan, plus AI / storage usage overage). The two sub-domains share storage tooling, the outbox, the tax engine, and the invoice renderer, but they live in different schemas, different RBAC scopes, and different RLS modes because they protect different things.
The service does not capture money (payment-gateway-service) and does not compute base prices (pricing-service). It owns the ledger truth: every monetary fact about a stay or about a tenant's platform bill, in the order it occurred, with the audit chain regulators expect.
Purpose
- Be the single authoritative ledger for money owed and money paid against a reservation, and for money owed and money paid by a tenant for the platform.
- Generate tax-correct invoices in the right locale (Pashto / Dari / Arabic RTL / EN / FR) with jurisdiction-specific numbering, multi-currency display, and Sharia-compliant variants.
- Provide first-class cash workflows on the Electron desktop: cash drawer open / record / close with two-staff sign-off, online-only close, variance detection, daily reconciliation against the folio.
- Provide automated subscription billing for tenants: usage metering (rooms, AI tokens, storage), monthly invoice generation, dunning for unpaid invoices, automatic suspension past hard caps.
Key responsibilities
- Folio lifecycle —
pending → open → balance_due → settled → closedwith re-open under supervisor override; first-class cash, multi-currency, partial-refund, and credit-note paths. - Open folio at
melmastoon.reservation.confirmed.v1(eager) ormelmastoon.reservation.checked_in.v1(deferred, per tenant policy); apply room-night charges frompricing-service's rate-plan snapshot. - Add charges — room nights, taxes, fees, mini-bar, restaurant, laundry, additional services. Each charge carries a
taxCoderesolved through the per-tenant tax engine; tax computed at post-time, not invoice-time. - Record payments — cash payments captured via the desktop cash-drawer flow; card / PayPal / MFS payments linked to
payment-gateway-servicepaymentIds via consumed events. Cash-on-arrival is first-class, not a degraded path. - Process refunds — partial and full; compute refund amount per the rate plan's refund policy; emit credit notes; release tax appropriately; reconcile with
payment-gateway-servicefor the wire transfer. - Close folio at checkout — settle balance, emit
folio.closed.v1, generate guest invoice (PDF stored infile-storage-service), publishinvoice.generated.v1. - Multi-currency settlement — folios may carry charges in tenant currency and payments in guest currency; the FX snapshot pinned on the reservation governs settlement; rounding is half-up banker's at currency minor-unit precision.
- Cash drawer session management — desktop opens a session with a counted opening float; every cash receipt and refund posts to the session; close requires two operators authenticated against
iam-serviceplus a counted closing float; variance over the tenant-configured threshold flagscash_drawer.discrepancy_found.v1and blocks the next session start until reconciled. - Government / corporate / agent invoices — different invoice templates with the correct VAT registration display, customer-class fields, and signature blocks; bilingual layout (Latin + Arabic numerals on the same Saudi/UAE document).
- Tenant subscription billing —
Subscriptionaggregate per tenant with plan (per-room-month or flat) plus metered usage overage; monthly cycle generatesSubscriptionInvoice; dunning state machine (current → grace → past_due → suspended) with automatic platform-suspension past hard cap. - Usage metering — consume
melmastoon.property.room.activated.v1,melmastoon.ai_orchestrator.completion.recorded.v1,melmastoon.file_storage.bytes.measured.v1and aggregate per(tenantId, billingPeriod, meter)in a partitionedusage_recordstable.
Aggregates owned
| Aggregate | Cardinality | Purpose | ID prefix |
|---|---|---|---|
Folio | 1 per reservation | Ledger root for one stay | fol_ |
FolioCharge | 0..N per folio | One posted charge with tax | chg_ |
FolioPayment | 0..N per folio | One recorded payment (cash or external) | fpm_ (new) |
FolioRefund | 0..N per folio | One processed refund | frd_ (new) |
Invoice | 0..N per folio (typically 1) | Issued invoice document | inv_doc_ |
InvoiceLine | 1..N per invoice | Line items with tax | (composite) |
CreditNote | 0..N per folio | Compensating doc for refunds / corrections | cnt_ (new) |
Subscription | 1 per tenant | Platform subscription state | sub_ (new) |
SubscriptionInvoice | 0..N per subscription | Monthly platform invoice | sin_ (new) |
UsageRecord | M per (tenantId, period, meter) | Aggregated metered usage | usg_ (new) |
CashDrawer | 1 per property | Drawer registry | cdr_ (new) |
CashDrawerSession | 0..N per drawer | One shift/session with float | cds_ (new) |
Settlement | 1 per closed folio | Settlement summary | set_ (new) |
New ID prefixes are declared in the service DATA_MODEL.md and added to standards/NAMING.md in the same PR.
Key APIs (REST, /api/v1)
| Method | Path | Purpose |
|---|---|---|
POST | /folios | Open a folio (idempotent via Idempotency-Key) |
GET | /folios/:id | Read folio + balance |
POST | /folios/:id/charges | Post a charge (server computes tax) |
POST | /folios/:id/payments | Record a payment (cash via desktop or link a paymentId) |
POST | /folios/:id/refunds | Issue a refund (policy-checked) |
POST | /folios/:id/close | Close folio + generate invoice |
POST | /folios/:id/reopen | Reopen (supervisor override; audited) |
POST | /invoices/:id/credit-notes | Issue a credit note |
GET | /invoices/:id.pdf | Download invoice PDF (signed URL via file-storage) |
POST | /cash-drawers/:id/sessions | Open a session (counted opening float) |
POST | /cash-sessions/:id/close | Close session (two-staff sign-off; online only) |
GET | /cash-sessions/:id/reconciliation | Cash reconciliation view |
GET | /subscriptions/:tenantId | Read subscription + usage |
POST | /subscriptions/:tenantId/cycle | (Platform-admin) trigger billing cycle |
POST | /subscriptions/:tenantId/reactivate | Reactivate suspended subscription |
Consumed by bff-backoffice-service for the desktop, by bff-tenant-booking-service for the post-booking receipt, and by an internal platform-admin BFF for subscription operations.
Key events published
| Event | Trigger |
|---|---|
melmastoon.billing.folio.opened.v1 | Folio created on reservation.confirmed.v1 or reservation.checked_in.v1 |
melmastoon.billing.folio.charge_added.v1 | Any charge posted |
melmastoon.billing.folio.payment_recorded.v1 | Any payment recorded |
melmastoon.billing.folio.refund_recorded.v1 | Refund processed |
melmastoon.billing.folio.closed.v1 | Folio settled and closed |
melmastoon.billing.folio.balance_due.v1 | Folio still owes at close attempt |
melmastoon.billing.invoice.generated.v1 | Invoice document persisted |
melmastoon.billing.invoice.sent.v1 | Invoice delivered (via notification-service) |
melmastoon.billing.credit_note.generated.v1 | Credit note issued |
melmastoon.billing.cash_drawer.opened.v1 | Cash drawer session opened |
melmastoon.billing.cash_drawer.closed.v1 | Cash drawer session closed |
melmastoon.billing.cash_drawer.discrepancy_found.v1 | Variance > tenant threshold |
melmastoon.billing.subscription.created.v1 | Subscription created on tenant.created.v1 |
melmastoon.billing.subscription.invoice_generated.v1 | Monthly subscription invoice created |
melmastoon.billing.subscription.payment_failed.v1 | Subscription invoice payment failed |
melmastoon.billing.subscription.cancelled.v1 | Subscription cancelled |
melmastoon.billing.subscription.reactivated.v1 | Subscription resumed |
melmastoon.billing.usage.recorded.v1 | Usage aggregation snapshot persisted |
Key events consumed
| Event | Effect |
|---|---|
melmastoon.reservation.confirmed.v1 | Open folio (eager mode) or store eligibility (deferred) |
melmastoon.reservation.checked_in.v1 | Open folio if not yet; post arrival-day charges per rate plan |
melmastoon.reservation.checked_out.v1 | Close folio + generate invoice |
melmastoon.reservation.cancelled.v1 | Refund per policy; emit credit note where applicable |
melmastoon.payment.transaction.captured.v1 | Record FolioPayment linked to paymentId |
melmastoon.payment.transaction.refunded.v1 | Reconcile refund against folio refund record |
melmastoon.tenant.created.v1 | Initialize Subscription (default plan); provision per-tenant billing schema |
melmastoon.property.room.activated.v1 | Increment room_count usage meter |
melmastoon.ai_orchestrator.completion.recorded.v1 | Increment AI-token usage meter |
melmastoon.file_storage.bytes.measured.v1 | Increment storage usage meter |
Upstream / downstream
Upstream (we consume): reservation-service, payment-gateway-service, tenant-service, pricing-service, property-service, ai-orchestrator-service, file-storage-service.
Downstream (we publish for): notification-service (invoice send, dunning), analytics-service, reporting-service, audit-service, search-aggregation-service (none — no PII / financial detail leaves to that service), bff-backoffice-service (desktop folio, cash drawer), bff-tenant-booking-service (post-booking receipt), sync-service (folio replication for active stays).
Non-functional requirements
| NFR | Target |
|---|---|
| Invoice generation latency p95 | < 2 s end-to-end (charge tally → tax computation → PDF render → file-storage URL) |
| Cash drawer close latency p95 | < 5 s (counted close → variance compute → two-staff sign-off → event publish) |
| Folio balance read p99 | < 200 ms |
| Tax computation correctness | 100% match against jurisdictional reference tables (CI gated) |
| Cash variance threshold | tenant-configured (default 0.5% or AFN 100, whichever is greater) |
| Tenant isolation | schema-per-tenant (per ADR-0002); RLS enabled inside each schema as defense in depth |
| API availability | 99.95% monthly |
| Subscription dunning correctness | 100% — no double-charge, no missed dunning step |
| Replicas | Min 3 Cloud Run instances (hot path); separate per-tenant schema migration job + monthly subscription cycle worker |
| Audit retention | 7 years for financial events (per 07-security §11.2) |
Where to go next
- Implementation-grade detail:
services/billing-service/SERVICE_OVERVIEW.mdand the rest of the 17-doc bundle. - Multi-tenancy rationale (schema-per-tenant): ADR-0002.
- Payments deep dive (cash drawer, FX, refunds):
docs/10-payments-architecture.md. - Tax engine & invoice locales:
docs/10-payments-architecture.md§10. - Booking saga interactions (folio open / close points):
docs/04-event-driven-architecture.md§7. - Standards & error codes:
standards/NAMING.md,standards/ERROR_CODES.md(BILLING / PAYMENT).