Features / supported
Customers and contracts
Purpose
Team-scoped customer master data as immutable versioned snapshots, ERP customer mappings, and contracts as the commercial frame subscriptions and charges live inside.
User outcomes
- A billing admin upserts a customer by external ID; every material change appends an immutable version (canonical SHA-256 content hash) while historical versions stay intact and are provably immutable at the database (UPDATE/DELETE rejected).
- An unchanged upsert is an idempotent no-op — no version churn.
- A finance operator connects the customer to an ERP customer number; the
mapping is
pendinguntil adapter validation confirms it. - Contracts are created with an immutable version 1 pinning the customer’s current version.
Actors and permissions
Mutations: billing_admin or team_admin (upsert_customer_erp_mapping:
finance_operator or team_admin). Reads additionally admit
finance_operator, auditor, integration_client. All queries are
team-constrained.
Domain terminology
- Customer version — immutable snapshot of the full customer facts
(legal name, address, country, email, VAT number, currency preference,
status) with
content_hash. - Customer ERP mapping —
(team, customer, erp_connection)→external_customer_numberwith avalidation_status. - Contract version — immutable snapshot pinning customer version, currency, and the effective date window.
Workflows
Contracts.upsert_customer/2— locks by(team, external_id); inserts customer + version 1, or appends the next version when the canonical snapshot changed; emitscustomer.version_created.v1. Invalid facts are rejected as a whole — no partial customer.Contracts.upsert_customer_erp_mapping/3— insert-or-update per(team, customer, connection), default status"pending".Contracts.create_contract/2— requires an existing team customer; writes contract +contract_versionsrow (version 1, canonical hash).
State transitions
Customers carry a status field (default active) stored inside each
version snapshot; contracts are draft/active. No state machine.
Business rules / invariants
- Customer facts are immutable versioned snapshots; the same external ID is unique per team but reusable across teams (BC-US-030).
- Idempotence is content-based: an upsert whose canonical snapshot
(including status) is unchanged returns
version: niland writes nothing. - Only an
invalidERP mapping blocks synchronization (INV-011);pendingdoes not. - Contract currency is fixed at creation; charge instances must match it.
- Subscriptions must start inside the contract’s date window (see subscriptions doc).
GraphQL contract
customer, customers (bounded cursor connection), contract;
upsertCustomer (idempotencyKey + optional expectedVersion optimistic
concurrency → VersionConflict), mapCustomerToErp, createContract.
CLI surface
Not yet implemented (BC-US-157 planned).
MCP surface
Not yet implemented (BC-US-158 planned).
UI behavior
LiveView surfaces under construction; domain commands available via GraphQL.
Accounting / ERP effects
The frozen customer version snapshot — not the live head — supplies the invoice recipient during ERP synchronization, so later customer-master edits cannot rewrite invoice identity (SPEC §17.4).
Async / failure / recovery behavior
Synchronous transactions only. ERP mapping validation itself happens in the ERP context asynchronously.
Observability
Audit: contracts.customer.version_created,
contracts.customer.erp_mapping_upserted, contracts.contract.created.
Outbox: customer.version_created.v1.
Tests
test/workflows/customer_management_test.exs — version append semantics,
database-level immutability of versions, idempotent no-op, per-team
uniqueness, authorization matrix.
Security / privacy
Customer PII lives in team-scoped rows and version snapshots; access requires a team-resolved scope with a read role. Lists are capped (100).
Limitations
- No customer archival/merge workflow beyond the
statusfield. - Contract versioning beyond version 1 (amendments) has no command yet.
- ERP mapping adapter validation (
pending → valid/invalid) is not yet automated end to end.