Skip to main content

Billing Service — Sync Contract

Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · Offline / sync

1. Scope

Most billing mutations are online-only — ledger integrity cannot be guaranteed from offline clients. A narrow subset supports offline capture with deterministic reconciliation when the device reconnects. This contract describes the per-aggregate policy.

2. Per-aggregate sync policy

AggregateOffline readOffline writeConflict policyNotes
Accountyes (snapshot)noserver_authoritativeBalance and aging are derived server-side
LedgerEntryyes (read-only)noappend_onlyAppend-only server-side; never authored on device
Chargeyes (list by encounter)yes (draft)append_only with server postDevice captures drafts while offline during a facility outage; on reconnect, drafts sync to server which assigns final IDs and posts to ledger
Invoiceyes (snapshot)noserver_authoritativeOnly server issues invoices
Paymentyes (snapshot)yes (cash receipt)server_authoritative with client idempotencyDevice captures cash receipt with Idempotency-Key + deterministic client ULID; server is authoritative on posted time and ledger linkage
Refundyesnoserver_authoritativeApproval workflow requires online
Adjustmentyesnoserver_authoritativeRequires supervisor; always online
StatementRunyes (status + artifacts)noserver_authoritativeServer-side batch
PriceListyes (current + effective)noserver_authoritativeCached for charge capture
TaxRuleyesnoserver_authoritativeCached for charge capture

3. Offline charge capture flow

4. Offline cash payment flow

5. Conflict semantics

ConflictDetectionResolution
Duplicate payment (same IK replay)Unique index (tenant_id, idempotency_key)Server returns original 201 body
Same IK with different bodyHash compare on stored request_hash409 IDEMPOTENCY_CONFLICT
Offline draft charge posted after online charge for same encounter+codeServer rejects if exact duplicate detected409 DUPLICATE_CHARGE
Price list drift (device cached older prices)Server re-resolves price at post timeServer wins; device must re-render line on response
Tax rule driftServer-side snapshot at invoice issueAlways server-authoritative
Account suspended between device queue and replayServer rejects403 ACCOUNT_SUSPENDED — device queues for ops review

6. Cache lifetimes

CacheTTLInvalidation
PriceList (published)24 hbilling.price_list.published.v1 pushes invalidation to edge caches
TaxRule24 hEvent-driven invalidation
Account balance snapshot5 minRefresh on re-open or on pull
Invoice detailon viewRe-fetch on patient open

7. Determinism rules

  1. Client-generated ULIDs are allowed only for Charge and Payment drafts; server reserves the right to re-assign by returning the canonical id.
  2. Idempotency-Key must be a UUIDv4 or ULID; device persists the key until the response is observed.
  3. No client-side ledger writes — the device never computes running balance; it renders what the server reports.
  4. Money arithmetic uses the same integer minor-unit representation as the server.

8. Observability

  • billing.sync.idempotency_replay_total counter (device-origin vs server-origin distinguishable by X-Client-Origin header).
  • billing.sync.offline_charge_latency_seconds histogram — time between local draft capture and server post.
  • billing.sync.conflict_total{type="duplicate|idempotency|price_drift"} counter.