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
| Aggregate | Offline read | Offline write | Conflict policy | Notes |
|---|---|---|---|---|
Account | yes (snapshot) | no | server_authoritative | Balance and aging are derived server-side |
LedgerEntry | yes (read-only) | no | append_only | Append-only server-side; never authored on device |
Charge | yes (list by encounter) | yes (draft) | append_only with server post | Device captures drafts while offline during a facility outage; on reconnect, drafts sync to server which assigns final IDs and posts to ledger |
Invoice | yes (snapshot) | no | server_authoritative | Only server issues invoices |
Payment | yes (snapshot) | yes (cash receipt) | server_authoritative with client idempotency | Device captures cash receipt with Idempotency-Key + deterministic client ULID; server is authoritative on posted time and ledger linkage |
Refund | yes | no | server_authoritative | Approval workflow requires online |
Adjustment | yes | no | server_authoritative | Requires supervisor; always online |
StatementRun | yes (status + artifacts) | no | server_authoritative | Server-side batch |
PriceList | yes (current + effective) | no | server_authoritative | Cached for charge capture |
TaxRule | yes | no | server_authoritative | Cached for charge capture |
3. Offline charge capture flow
4. Offline cash payment flow
5. Conflict semantics
| Conflict | Detection | Resolution |
|---|---|---|
| Duplicate payment (same IK replay) | Unique index (tenant_id, idempotency_key) | Server returns original 201 body |
| Same IK with different body | Hash compare on stored request_hash | 409 IDEMPOTENCY_CONFLICT |
| Offline draft charge posted after online charge for same encounter+code | Server rejects if exact duplicate detected | 409 DUPLICATE_CHARGE |
| Price list drift (device cached older prices) | Server re-resolves price at post time | Server wins; device must re-render line on response |
| Tax rule drift | Server-side snapshot at invoice issue | Always server-authoritative |
| Account suspended between device queue and replay | Server rejects | 403 ACCOUNT_SUSPENDED — device queues for ops review |
6. Cache lifetimes
| Cache | TTL | Invalidation |
|---|---|---|
PriceList (published) | 24 h | billing.price_list.published.v1 pushes invalidation to edge caches |
TaxRule | 24 h | Event-driven invalidation |
Account balance snapshot | 5 min | Refresh on re-open or on pull |
Invoice detail | on view | Re-fetch on patient open |
7. Determinism rules
- Client-generated ULIDs are allowed only for
ChargeandPaymentdrafts; server reserves the right to re-assign by returning the canonical id. - Idempotency-Key must be a UUIDv4 or ULID; device persists the key until the response is observed.
- No client-side ledger writes — the device never computes running balance; it renders what the server reports.
- Money arithmetic uses the same integer minor-unit representation as the server.
8. Observability
billing.sync.idempotency_replay_totalcounter (device-origin vs server-origin distinguishable byX-Client-Originheader).billing.sync.offline_charge_latency_secondshistogram — time between local draft capture and server post.billing.sync.conflict_total{type="duplicate|idempotency|price_drift"}counter.