Skip to main content

TESTING_STRATEGY — billing-service

Test pyramid is enforced in CI: unit (domain) is the widest layer, application wraps domain, integration verifies infrastructure adapters and DB, contract verifies API + event payload schemas, E2E exercises top user journeys, chaos verifies failure-mode SLOs.

1. Layers

LayerToolingScopeCoverage gate
Unit (domain)vitest (no I/O)aggregates, value objects, invariantsline ≥ 95%, branch ≥ 90%
Applicationvitest + in-memory portsuse cases against fake repositories, fake clock, fake outboxline ≥ 90%
Integrationvitest + Testcontainers (Postgres, Pub/Sub emulator, GCS fake)repositories, outbox drainer, event handlers, tax engine, PDF renderline ≥ 80%
Contract — APIvitest + supertest + the spec from API_CONTRACTSrequest/response shapes vs. OpenAPI100% endpoints with at least 1 happy + 1 sad test
Contract — eventsvitest + JSON schema validationpublished payloads vs. schemas in EVENT_SCHEMAS100% event types
E2Eplaywrightdesktop (Electron) journeys + cloud RESTtop 10 journeys
Chaostoxiproxy, k6network / DB partition, slow Pub/Sub, processor brownoutsSLO assertions
Performancek6hot endpointslatency targets in OBSERVABILITY §3

2. Domain unit tests (the heart)

For every aggregate the suite exercises:

  • happy path,
  • every domain error in the matrix,
  • every state transition (legal + illegal),
  • OCC version increment,
  • event emission shape (events as data, asserted whole).

Example shape:

describe('Folio.recordRefund', () => {
it('rejects when refund > net captured', () => {
const folio = openFolioFixture();
folio.postCharge(chargeFixture({ grossMicro: 100_000_000n }));
folio.recordPayment(paymentFixture({ amountMicro: 50_000_000n }));
expect(() => folio.recordRefund(refundFixture({ amountMicro: 80_000_000n })))
.toThrowDomainError('MELMASTOON.BILLING.REFUND_EXCEEDS_BALANCE');
});
});

Property-based tests (fast-check) cover:

  • Money arithmetic (associativity, identity, currency-mismatch surfacing),
  • FXSnapshot.convert round-trip within ε,
  • Folio.balanceInFolioCurrency() algebraic identity Σchg + Σtax − Σpay + Σref for any random sequence of mutations.

3. Application use-case tests

Use in-memory FolioRepository, OutboxWriter, Clock, IdGenerator doubles. Patterns covered:

  • idempotency: same Idempotency-Key ⇒ identical output, no double-emit, single side-effect;
  • inbox dedupe for events;
  • OCC retry path (3 attempts, then surfaced);
  • error → HTTP mapping table assertions for the controller layer above;
  • compensating actions (e.g., refund → gateway failure → folio refund rolled back, no orphan credit note).

4. Integration tests

Single Testcontainers stack per CI job:

  • Postgres 16 (per-tenant schemas created by the templated DDL bundle);
  • gcr.io/google.com/cloudsdktool/google-cloud-cli Pub/Sub emulator;
  • fsouza/fake-gcs-server for invoice PDFs.

Suites:

  • TenantProvisioning — create three tenants; verify schemas, RLS policies, GUC default, triggers.
  • FolioRepository — open / save / load with version OCC; cross-tenant access blocked.
  • OutboxDrainer — write event in tx, drainer publishes to emulator topic, sets published_at; failure path leaves row.
  • InvoicePipeline — close folio of fixture types: standard EN, government AR (RTL, bilingual numerals), corporate USD, agent EUR, sharia PKR (no interest line). Snapshot the rendered PDF metadata and assert sha256(payload) matches the embedded hash.
  • CashDrawerLifecycle — open, post receipts, initiate close, finalize close (mock iam-service step-up), variance scenarios (within / over threshold), prior-session-open guard.
  • SubscriptionCycle — provision tenant, ingest usage events, run cycle worker, assert invoice rows + outbox events; failure → dunning state advance.
  • TaxEngine — fixture-driven matrix per jurisdiction (AF-KBL, AF-HER, PK-KHI, SA-RUH, AE-DXB, TJ-DYU, IR-THR) × charge kind × customer class; 100% match required.
  • MultiCurrencyFolio — folio AFN with USD payment; assert per-currency totals and FX rounding to half-up banker's at minor unit.
  • Reconcile — daily reconciliation finds mismatch between cash sessions and folio cash payments; emits the right event.

5. Contract tests

  • OpenAPIredocly lint + openapi-diff against the previous main; breaking changes blocked unless the PR is labeled api-breaking-v2.
  • Event schemasajv validates fixtures from each emitter against the JSON schemas in EVENT_SCHEMAS.
  • Consumer-drivenpact-broker interactions with bff-backoffice-service, bff-tenant-booking-service, notification-service, analytics-service; PR fails if a deployed consumer's contract no longer matches.

6. E2E (Playwright)

Top 10 journeys:

  1. Walk-in: open folio → post charges → record cash payment → close → print invoice.
  2. Booking → check-in → mini-bar charges → external card payment → checkout → invoice (multi-currency).
  3. Reservation cancellation with policy-based partial refund + credit note.
  4. Cash drawer open → posted shifts of receipts → initiate close → online finalize close (two staff via mock step-up).
  5. Cash drawer close with variance > threshold → reconciliation_blocked → next session blocked → supervisor acknowledges → next session opens.
  6. Folio re-open after checkout → add restaurant charge → close → new invoice + voided original.
  7. Subscription billing cycle: usage ingested over month → cycle runs → invoice generated → payment captured → state stays current.
  8. Subscription payment failed → grace → past_due → suspended → reactivation by platform admin.
  9. RTL Arabic invoice render with bilingual numerals (visual regression baseline).
  10. Sharia-compliant tenant attempts to add interest late fee → rejected; correct invoice template applied with no finance-charge wording.

E2E runs in a containerized Electron with a virtual display; recorded videos and screenshots upload to the CI artifact store on failure.

7. Chaos & resilience

ScenarioToolAssertion
Cloud SQL primary failovergcloud sql instances failover in stagefolio mutations resume in ≤ 30 s; no double-write
Pub/Sub publish 5 s latencytoxiproxyoutbox lag rises but drainer recovers; no event loss
payment-gateway-service 5xxmock proxy 503refund use case rolls back domain state; no orphan credit note
iam-service step-up rejectedmock returns 401cash session close fails clean; session stays pending_close
Network partition during cash closetoxiproxy cut mid-requestdesktop surfaces "queued"; on reconnect, replay finalizes idempotently
Tax-engine cache cold-starttenantSettings proxy 500mscharge post latency budget held by retry+circuit
AI orchestrator timeouttoxiproxy 100% timeoutsfallback triggers; no impact on user request latency

8. Performance / load

k6 scenarios run nightly against staging:

  • 200 rps POST /folios/:id/charges per tenant for 10 minutes → p95 ≤ 350 ms;
  • 50 rps POST /folios/:id/close per tenant for 5 minutes → p95 ≤ 2 s including PDF render;
  • 100 concurrent active cash sessions per tenant, mixed open/post/close.

Regressions ≥ 10% over the rolling baseline block the PR.

9. Security tests

  • PCI-DSS DLP scan — pre-commit and CI rule rejects strings matching Luhn-passing 13–19 digit sequences in repo, in _outbox payloads (sample), and in logs (sample).
  • AuthZ matrix tests — every endpoint × every role; 403 expected for un-scoped roles; step-up enforced where required.
  • OCC tampering — replay attack with stale If-Match; expect 412.
  • Cross-tenant — request signed with tenant A token attempting to read tenant B folioId; expect 404 (not 403, to avoid leaking existence).

10. Test data & fixtures

  • Fixture builders in test/fixtures/ produce Folio, Charge, Payment, Refund, Subscription deterministically from a seed.
  • Currency / tax / locale matrix fixture lives at test/fixtures/tax-matrix.json and is the source of truth for the TaxEngine integration suite.
  • PDF golden snapshots live at test/fixtures/invoice-snapshots/ keyed by (template, locale) — diffs reviewed visually in PR.

11. Coverage & CI gating

Coverage thresholds enforced by vitest --coverage:

  • domain: line 95% / branch 90% / function 100%
  • application: line 90% / branch 85%
  • combined: line 85% / branch 80%

npm run check runs: lint → typecheck → unit → application → integration → contract. PRs fail if any threshold or contract / OpenAPI gate fails. E2E + chaos + performance run on the merge queue and nightly.

12. Cross-references