ADR-0001: CRM Settlement Identity Boundary
- Status: Accepted (canonical booking identity contract landed in #14050, 2026-04-26)
- Date: 2026-04-03 (canonical booking identity addendum 2026-04-26)
- Author: @digiwedge/engineering
Context
The POS partner settlement flow accepts CRM-linked requests that may contain either quoteId, reservationId, or both. The reconciliation expansion under #13319 and the identity resolver work under #14033 separated trusted cross-ID identity from settlement admission by introducing a first-class SettlementAlias model and a shared SettlementIdentityResolver.
That change exposed a hard boundary that was previously blurred by acceptance-anchor backfill:
SettlementAcceptanceAnchorrows are safe as workflow ownership and idempotency state.- They are not safe as an authority for quote-to-reservation identity inference.
- Later ingress requests can repeat a known identifier while supplying a mistyped secondary identifier.
- Persisting that secondary identifier onto an existing anchor poisons future admission and replay behavior.
We removed anchor backfill from conflicting ingress for exactly that reason. That removal restores the trust boundary, but it also leaves an explicit operational window:
- request A can arrive with
quoteIdonly - request B can arrive with
reservationIdonly for the same booking - without trusted alias evidence or an upstream canonical booking identifier, the system cannot prove that both requests refer to the same booking at admission time
- exact-only admission can therefore create two separate anchors and two separate settlement attempts until reconciliation later promotes a trusted alias from a durable outcome
This is not a local implementation bug. It is an information-boundary problem. Preventing that double-admission window requires authoritative upstream identity proof.
The design options considered are:
- Add a canonical booking identifier to the partner/CRM settlement payload.
- Perform a synchronous upstream CRM lookup in the settlement hot path.
- Keep exact-only admission until a trusted alias exists, then let reconciliation catch up and normalize future traffic.
The current runtime also keeps reconciliation sync inside the API process. That is acceptable only as a temporary single-replica bridge and is already tracked separately in #14009.
Decision
We will treat CRM settlement identity as a two-tier model with an explicit trust boundary.
1. Trusted identity authority
SettlementAlias is the only trusted quote-to-reservation identity surface.
- Cross-ID enrichment must read only from
SettlementAlias. - Trust lifecycle remains explicit:
ACTIVE_TRUSTEDandINVALIDATED. - Reconciliation and admission must share the same resolver semantics through
SettlementIdentityResolver.
2. Acceptance-anchor role
SettlementAcceptanceAnchor is an ownership and idempotency surface only.
- It may store identifiers that were present on the request that created the anchor.
- It must not be mutated later with secondary identifiers copied from subsequent ingress.
- It must not be treated as trusted alias evidence.
- It must not perform cross-column identity inference.
3. Admission behavior before trust exists
Until authoritative upstream booking identity exists, CRM-linked settlement admission remains exact-only.
quoteId-only requests match only exactquoteId.reservationId-only requests match only exactreservationId.- Cross-ID normalization happens only when a trusted alias already exists.
- Reconciliation remains responsible for catch-up after a durable dual-ID outcome proves the mapping.
This means the double-debit window for one-sided requests is explicit and accepted as the current production-safe posture.
4. Long-term target — canonical booking identity (shipped #14050, 2026-04-26)
The preferred long-term design is a canonical booking identifier in the payload, and as of #14050 it is now part of the contract.
- The settlement DTO carries an optional
canonicalBookingIdfield. Partners on the new contract supply it alongside (or instead of) the legacyquoteId/reservationIdpair. - When supplied, it is the primary admission surface:
SettlementAcceptanceAnchorrows are created withanchorType = BOOKINGandanchorValue = canonicalBookingId. Two requests for the same booking — one carrying onlyquoteIdpluscanonicalBookingId, one carrying onlyreservationIdpluscanonicalBookingId— converge on the same anchor row and the second admission is dedupe-rejected. - Replay matching now requires the canonical booking id to match in addition to the existing replay signature (
partnerTransactionId, member, total, currency, payment method, terminal, table, charge type, quote id, reservation id). This prevents accidental replay across logically distinct bookings. - Reconciliation grouping (
buildCanonicalAnchor,SettlementCanonicalAnchorResolver,DuplicateOutcomeClassifier) prefers the BOOKING anchor when present, so duplicate-outcome detection groups by the strongest identifier the partner could provide. - This avoids adding CRM availability to the payment hot path. It removes the need for local heuristic identity inference. It lets admission dedupe the same booking without mutating anchors or depending on later reconciliation.
Backward compatibility is preserved during the transition. Legacy payloads without canonicalBookingId continue to admit through the existing RESERVATION/QUOTE anchor surfaces and the exact-only admission posture described in section 3 still applies to those payloads. Trusted alias promotion still requires both quoteId AND reservationId because that is what proves the QUOTE↔RESERVATION mapping; the canonical booking identity is an independent admission key, not a substitute for the trusted alias graph.
We do not choose synchronous CRM lookup as the long-term default because it adds a hard runtime dependency to the money-moving path. It remains a fallback option only if the payload contract cannot be evolved.
5. Evidence versus authority
If the business needs observability for unresolved or one-sided CRM settlements, we will add a separate evidence surface rather than reusing anchors as a trust store.
- Observed pairs and one-sided admissions may be recorded for analysis.
- That evidence must not directly drive admission, alias promotion, or replay normalization.
6. Reconciliation runtime follow-up
The current in-process reconciliation scheduler remains a bridge only.
- Same-process coalescing reduces local races.
- Cross-pod safety still requires externalizing or leasing the writer.
- That work stays tracked under
#14009.
Consequences
Positive
- The trust boundary is explicit and defensible.
- Later malformed requests cannot poison trusted identity by mutating existing anchors.
- Admission, replay matching, and reconciliation all converge on one trusted alias model.
- The architecture is honest about what the system can and cannot prove locally.
- The long-term path is clear: canonical booking identity in the payload.
Negative
- One-sided CRM admissions still have a real double-debit window for partner payloads that do not yet supply
canonicalBookingId. Once a partner adopts the new contract the window closes for that partner; hub-side trust boundaries are unchanged. - Some duplicate prevention still moves from admission time to reconciliation time for legacy payloads.
- Operators need visibility into unresolved identity cases and alias invalidation history.
- The platform still carries a single-writer reconciliation runtime constraint until
#14009lands.
Alternatives Considered
| Alternative | Pros | Cons | Why not chosen |
|---|---|---|---|
| Canonical booking identifier in payload | Zero hot-path lookup latency, no runtime CRM dependency, strongest admission-time identity | Requires CRM and partner contract change | Chosen and shipped as an optional contract field in #14050 (2026-04-26) |
| Synchronous CRM lookup before admission | Prevents one-sided double-admission once CRM can answer both directions | Adds CRM latency and availability coupling to payment flow, harder failure modes | Not chosen as the default long-term design |
| Continue anchor backfill from ingress | Blocks some duplicate-anchor cases without upstream changes | Persists unverified secondary identifiers and poisons future admission | Rejected as unsafe |
| Infer trust from durable anchors | Reuses existing data already in the ledger | Historical anchors contain backfilled data with unverifiable provenance | Rejected as unsafe |
| Exact-only admission plus reconciliation catch-up | Preserves trust boundary, no new upstream dependency | Leaves explicit double-debit window for one-sided requests | Chosen as the current production-safe posture until canonical identity exists |
Follow-ups
- Keep
SettlementAliasas the only trusted cross-ID authority in#14033. - Remove ingress-driven anchor backfill that mutates existing anchors with unverified secondary identifiers in
#13993. - Externalize reconciliation sync out of the API process in
#14009. - Add one-sided CRM settlement metrics and dashboards to measure the real size of the double-debit window in
#14049. - Add operator-grade alias review, invalidation, and audit tooling in
#14051(shipped in#14360). - Define and land the canonical booking identity contract change for CRM-linked settlements in
#14050(shipped 2026-04-26). The settlement DTO now accepts an optionalcanonicalBookingId. When supplied, settlement admission, replay matching, and reconciliation grouping use it as the primary dedupe surface (anchorType = BOOKING). Legacy one-sidedquoteId/reservationIdpayloads continue to behave exactly as before during the partner rollout. - Add unresolved-identity reporting that is observational only and does not change admission semantics in
#14052.