Skip to main content

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:

  • SettlementAcceptanceAnchor rows 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 quoteId only
  • request B can arrive with reservationId only 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:

  1. Add a canonical booking identifier to the partner/CRM settlement payload.
  2. Perform a synchronous upstream CRM lookup in the settlement hot path.
  3. 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_TRUSTED and INVALIDATED.
  • 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 exact quoteId.
  • reservationId-only requests match only exact reservationId.
  • 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 canonicalBookingId field. Partners on the new contract supply it alongside (or instead of) the legacy quoteId/reservationId pair.
  • When supplied, it is the primary admission surface: SettlementAcceptanceAnchor rows are created with anchorType = BOOKING and anchorValue = canonicalBookingId. Two requests for the same booking — one carrying only quoteId plus canonicalBookingId, one carrying only reservationId plus canonicalBookingId — 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 #14009 lands.

Alternatives Considered

AlternativeProsConsWhy not chosen
Canonical booking identifier in payloadZero hot-path lookup latency, no runtime CRM dependency, strongest admission-time identityRequires CRM and partner contract changeChosen and shipped as an optional contract field in #14050 (2026-04-26)
Synchronous CRM lookup before admissionPrevents one-sided double-admission once CRM can answer both directionsAdds CRM latency and availability coupling to payment flow, harder failure modesNot chosen as the default long-term design
Continue anchor backfill from ingressBlocks some duplicate-anchor cases without upstream changesPersists unverified secondary identifiers and poisons future admissionRejected as unsafe
Infer trust from durable anchorsReuses existing data already in the ledgerHistorical anchors contain backfilled data with unverifiable provenanceRejected as unsafe
Exact-only admission plus reconciliation catch-upPreserves trust boundary, no new upstream dependencyLeaves explicit double-debit window for one-sided requestsChosen as the current production-safe posture until canonical identity exists

Follow-ups

  • Keep SettlementAlias as 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 optional canonicalBookingId. When supplied, settlement admission, replay matching, and reconciliation grouping use it as the primary dedupe surface (anchorType = BOOKING). Legacy one-sided quoteId/reservationId payloads 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.