2026-04-25

Module T (Transaction Pipeline) — Functional Decomposition

Artifact layer. Third of three Canary module artifact layers: 1. Canonical spec (vendor-neutral) — Canary-Retail-Brain/modules/T-transaction-pipeline.md 2. Code/schema crosswalk (Canary-specific) — Brain/wiki/canary-module-t-transaction-pipeline.md (planned for Q/T/R; exists for J/P/F/C) 3. Functional decomposition (Counterpoint-substrate-aware, L2/L3 + user stories) — this card

Governing thesis

T is the inbound edge of every spine module. Every detection (Q), every customer projection (R), every device telemetry signal (N), every margin computation (P-derived), every transfer reconciliation (D), every PO match (J) — all eventually trace back to a row T wrote. If T is wrong, every downstream answer is wrong.

For a Counterpoint-shaped tenant, T's functional surface is dominated by two architectural pivots that distinguish it from the Square baseline: (1) ingress is poll-only, not webhook-driven, and (2) the inbound payload is Document-shaped omnibus — sales, returns, voids, transfers, POs, RTVs, GFC transactions all arrive through one endpoint family discriminated by DOC_TYP. The decomposition must accommodate both shapes (Square = webhook + Payment-shaped; Counterpoint = poll + Document-shaped) because the substrate-decoupling work documented in the Apr 25 delivery spec demands it.

T is ● Full direct in every Counterpoint Solution Map cell, but the cell hides real architectural difficulty: poll cadence discipline, type-routing on DOC_TYP, watermark management per (tenant, entity), multi-company-per-tenant identity, and the Document-omnibus pattern that lights up D and J modules from the same poll loop that lights up T.

Counterpoint Endpoint Substrate

Counterpoint Endpoint CRDM Entity L2 Process Area
PS_DOC_HDR Transaction header T.1 (Document ingestion), T.2 (Document type routing)
PS_DOC_LIN Transaction line T.1 (Line-item ingestion), T.3 (Tender decomposition)
PS_DOC_LIN_PRICE Line price T.3 (Price-realized capture — sourced to P)
PS_DOC_PMT Payment records T.4 (Tender capture — sourced to F)
Events.transactions Transaction event stream T.1 (EJ spine ingestion — see canary-ej-spine-and-sales-audit)
Store/{StoreId}/Transactions Store transaction list T.1 (Polling substrate)

Executive summary

Dimension Count Source
L2 process areas 6 This card
L3 functional processes 41 This card
Counterpoint endpoints in steady-state path 4 (Document × GET, Customer/Items/Stores deltas) Endpoint spine map
Document types T must type-route on (DOC_TYP) 8+ Document model wiki
Downstream consumer modules 7 (R, N, S, F, D, J, Q) This card §T.4
Substrate contracts T owes downstream 12 This card §T.6
Assumptions requiring real-customer validation 9 Tagged ASSUMPTION-T-NN
User stories enumerated 64 Observer-perspective; actor ∈

Posture: archetype-shaped against Counterpoint specifically, with explicit accommodation for the Square baseline staying live. No real Counterpoint customer engaged. The Q card's L3s drove most of T.6 (substrate contracts to downstream); see Q's §Q.1 for the consumer-side perspective.

L1 → L2 → L3 framework

L1 (Solution Map cell)         T = ● Full direct (Counterpoint Document family)
                                 │
L2 (Process areas)               ├── T.1  Adapter ingress (poll OR webhook, per source)
                                 ├── T.2  Sealing & integrity (raw-bytes hash chain)
                                 ├── T.3  Provider-keyed parsing (Document → CanonicalEvent)
                                 ├── T.4  Canonical event publication (downstream dispatch)
                                 ├── T.5  Merkle anchoring (batch tree, anchor cadence)
                                 ├── T.6  Replay, backfill & idempotency
                                 └── T.7  Cross-module substrate contracts
                                 │
L3 (Functional processes)       (41 — enumerated per L2 below)
                                 │
L4 (Implementation detail)      Lives in SDDs + module specs
                                  (Canary-Retail-Brain/modules/T-transaction-pipeline.md,
                                   docs/sdds/canary/ncr-counterpoint-retail-spine-integration.md,
                                   2026-04-25-canary-for-rapidpos-delivery-spec.md cycles 7-8)

T.1 — Adapter ingress

Purpose. Receive vendor-shaped transactional events. Two ingress shapes must coexist on the same downstream pipeline: webhook push (Square — works today) and poll pull (Counterpoint — to be built). The substrate-decoupling pass defined in the delivery spec ensures the downstream consumers (T.2 onward) can't tell them apart.

Provider key. Every event entering T.1 carries (provider, tenant_id) from the moment it enters. Provider is derived from request signature (webhook) or known at construction time (poll loop). No event reaches T.2 without provider attribution.

L3 processes

ID L3 process Provider scope Notes
T.1.1 Webhook receipt + HMAC validation Square Existing; signature key per merchant rotated at onboarding → TBD: L4 implementation detail pending
T.1.2 Webhook idempotency check Square Valkey DB 3 dedupe by (merchant_id, source_event_id), 24h TTL → TBD: L4 implementation detail pending
T.1.3 Poll worker per (tenant, entity_type) Counterpoint (today); future Aloha Reads adapter's poll_intervals(); respects per-entity cadence → TBD: L4 implementation detail pending
T.1.4 Watermark management per poll cursor Counterpoint Stored as (tenant_id, company_alias, entity_type) → last_RS_UTC_DT; advances on successful page consume → TBD: L4 implementation detail pending
T.1.5 Poll authentication injection Counterpoint HTTP Basic <CompanyAlias>.<UserName>:password + APIKey header per request; credentials read from pos_tenant_credentials → TBD: L4 implementation detail pending
T.1.6 Multi-company-per-tenant routing Counterpoint Single Canary tenant may have N Counterpoint companies; poll loop iterates over all company_alias per tenant → TBD: L4 implementation detail pending
T.1.7 Server-cache discipline Counterpoint ServerCache: no-cache for first poll of each cycle on cached entities; never on Documents (transactional, uncached server-side) → TBD: L4 implementation detail pending
T.1.8 Rate-limit / 429 backoff Counterpoint Exponential backoff; per-tenant poll budget → TBD: L4 implementation detail pending
T.1.9 On-prem reachability handling Counterpoint Customer's API server may be on-prem with no public IP; fall back to customer-side agent or document the constraint at onboarding → TBD: L4 implementation detail pending
T.1.10 Backfill ingress (one-shot) All At tenant install, walk the relevant entity surface from epoch via paginated GET; same downstream pipeline; tag rows source_type=BATCH → TBD: L4 implementation detail pending

User stories

T.2 — Sealing & integrity

Purpose. Hash every received event over its raw bytes before any business logic runs, then chain those hashes per tenant to produce an append-only evidence ledger. The chain survives every downstream operation including parser changes — because the hash is over bytes-as-arrived, not over a parsed shape.

Stage ordering is patent-critical. Hash before parse. Parse is fallible; hash is not. Every later integrity claim depends on this ordering.

L3 processes

ID L3 process Notes
T.2.1 Raw-bytes hash SHA-256 over the unparsed payload as it crossed the ingress boundary → TBD: L4 implementation detail pending
T.2.2 Per-tenant chain hash SHA-256(previous_chain_hash || event_hash); serialized via pg_advisory_xact_lock(merchant_id) → TBD: L4 implementation detail pending
T.2.3 Evidence record write evidence_records row with provider, tenant, raw bytes, hash, chain hash, ingest timestamp → TBD: L4 implementation detail pending
T.2.4 Per-event ingestion-log lifecycle ingestion_log tracks received → sealed → parsed → published → completed/failed transitions → TBD: L4 implementation detail pending
T.2.5 Source-type marker Every evidence row carries source_type ∈ {WEBHOOK, POLLING, BATCH} so downstream can disambiguate without inspecting bytes → TBD: L4 implementation detail pending
T.2.6 Provider marker Every evidence row carries provider ∈ {square, counterpoint, ...} — the substrate-decoupling load-bearing field → TBD: L4 implementation detail pending

User stories

T.3 — Provider-keyed parsing

Purpose. Convert vendor-shaped sealed bytes into CanonicalEvent rows. This is the L2 where the Document-omnibus pattern lives. For Counterpoint, one polled Document fans out into multiple CanonicalEvents — transaction header, lines flattened, payments flattened, taxes flattened, audit log entries flattened, pricing decisions flattened, original-doc references resolved.

Dispatch shape. Parser registry keyed by (provider, event_type) for webhooks, (provider, entity_type) for poll. Adding a third provider requires no edits to dispatch — just registry entry.

L3 processes

ID L3 process Provider scope Notes
T.3.1 Provider/source-code dispatch All webhook_dispatch.py and tsp/consumers/sub2_parse.py lookup by (provider, event_type/entity_type) → TBD: L4 implementation detail pending
T.3.2 Document type-routing on DOC_TYP Counterpoint T-routes to transaction.created; XFER → transfer.created (D); PO/PREQ/RECVR/RTV → J events; GFC → gift_card.activity (F) → TBD: L4 implementation detail pending
T.3.3 Document line flattening Counterpoint PS_DOC_LIN[]Events.transaction_lines rows; per-line LIN_TYP (S/R/O/B/L/etc.) preserved → TBD: L4 implementation detail pending
T.3.4 Document payment flattening Counterpoint PS_DOC_PMT[]Events.payments; PII-sensitive fields (SIG_IMG, SIG_IMG_VECTOR) redacted at parser, never persisted → TBD: L4 implementation detail pending
T.3.5 Document tax flattening Counterpoint PS_DOC_TAX[]Events.transaction_taxes; multi-authority preserved (city + county + state stack) → TBD: L4 implementation detail pending
T.3.6 Document audit-log flattening Counterpoint PS_DOC_AUDIT_LOG[]Events.audit_log_entries; ACTIV and LOG_ENTRY preserved verbatim for Q rule pattern-matching → TBD: L4 implementation detail pending
T.3.7 Pricing-decision flattening Counterpoint PS_DOC_LIN_PRICE[]Events.pricing_decisions; per-line pricing rule applied (Q + P-derived substrate) → TBD: L4 implementation detail pending
T.3.8 Original-document reference resolution Counterpoint PS_DOC_HDR_ORIG_DOC[]Events.original_doc_refs; links return → original sale, void → original sale → TBD: L4 implementation detail pending
T.3.9 Square-shaped legacy parsing Square 16 existing parser modules; refactored into services/pos/square/parsers/ per Cycle 7; behavior unchanged → TBD: L4 implementation detail pending
T.3.10 Idempotency on parse All Keyed on (provider, tenant, external_id); parsing the same sealed event twice produces the same canonical rows → TBD: L4 implementation detail pending
T.3.11 Parse-failure quarantine All Failed parses do NOT block the seal chain; logged + dead-lettered for inspection; re-runnable via Replay-tool → TBD: L4 implementation detail pending
T.3.12 Schema-drift fingerprinting All Per-source SchemaFingerprint; Counterpoint payloads compute against Counterpoint baseline, never Square — fixes the L3B-04 cross-provider drift-alert bug → TBD: L4 implementation detail pending

User stories

T.4 — Canonical event publication

Purpose. Hand sealed-and-parsed CanonicalEvents to downstream consumers. Every consumer reads from the same canonical surface; none reach into provider-specific shapes. This L2 owns the publication contract and the per-consumer dispatch.

L3 processes

ID L3 process Consumer module Notes
T.4.1 Publish to canary:detection stream Q Every transaction-related CanonicalEvent; Q's chirp engine subscribes → TBD: L4 implementation detail pending
T.4.2 Publish to R-relevant subscribers R New customer references trigger upserts; transaction velocity feeds R's projections → TBD: L4 implementation detail pending
T.4.3 Publish device telemetry to N N Per-STR_ID/STA_ID/DRW_ID telemetry; drawer-session correlation substrate → TBD: L4 implementation detail pending
T.4.4 Publish line items to S consumers S Item identity references; sales velocity feeds S projections → TBD: L4 implementation detail pending
T.4.5 Publish tender + tax to F consumers F Per-Document tender mix and per-authority tax detail → TBD: L4 implementation detail pending
T.4.6 Publish transfer events to D D DOC_TYP=XFER routes here; source/dest store reconciliation → TBD: L4 implementation detail pending
T.4.7 Publish PO/RECVR/RTV events to J J DOC_TYP=PO/PREQ/RECVR/RTV routes here; vendor reconciliation substrate → TBD: L4 implementation detail pending
T.4.8 Per-consumer subscription ack tracking All Confirm each consumer processed; expose lag per consumer → TBD: L4 implementation detail pending

User stories

T.5 — Merkle anchoring

Purpose. Batch sealed event hashes into a Merkle tree, anchor the root, expose per-event inclusion proofs. Today the anchor is mocked; v2 anchors as a Bitcoin ordinal inscription. This L2 is least urgent for Counterpoint cutover but most differentiating for the platform pitch.

L3 processes

ID L3 process Notes
T.5.1 Inscription pool accumulation Sealed event hashes accumulate in inscription_pool per tenant per cadence window → TBD: L4 implementation detail pending
T.5.2 Merkle tree construction Deterministic tree over the pool window; root is the anchor commitment → TBD: L4 implementation detail pending
T.5.3 Per-event inclusion proof event_inscriptions rows carry the path from event hash to root for each batched event → TBD: L4 implementation detail pending
T.5.4 Anchor handoff (mock today) v1: mock anchor with deterministic-but-fake commitment; v2: Bitcoin ordinal inscription → TBD: L4 implementation detail pending
T.5.5 Cadence policy Per-tenant or platform-wide; needs commitment (per-batch / per-day / per-100k-events) → TBD: L4 implementation detail pending
T.5.6 Proof retrieval surface Operator can fetch per-event inclusion proof and verify against anchor → TBD: L4 implementation detail pending

User stories

T.6 — Replay, backfill & idempotency

Purpose. Operational discipline. Three concerns intersect: replay (re-process sealed bytes through a fixed parser), backfill (initial historical pull at tenant install), idempotency (same input produces same output every time). All three converge on the principle: the seal is the source of truth, not the parsed rows.

L3 processes

ID L3 process Notes
T.6.1 Parse-only replay Re-run T.3 against sealed bytes; does NOT re-seal, does NOT re-anchor → TBD: L4 implementation detail pending
T.6.2 Per-tenant backfill at install One-shot historical pull from epoch (or tenant-specified start); same downstream pipeline; source_type=BATCH marker → TBD: L4 implementation detail pending
T.6.3 Backfill SLA per tenant Published completion target per entity type; 1d for Documents-from-90d-ago is reasonable; longer history = longer SLA → TBD: L4 implementation detail pending
T.6.4 Idempotency at every stage T.1 ingress (dedup), T.2 seal (per-event hash idempotent), T.3 parse (per-canonical-row idempotent) → TBD: L4 implementation detail pending
T.6.5 Retroactive parser-change reclassification handling When a parser change retroactively changes event type (e.g., PAID_OUTPOST_VOID), downstream Q rules may double-fire — needs explicit policy → TBD: L4 implementation detail pending
T.6.6 Per-event reprocess-on-demand Operator can target a specific (provider, tenant, external_id) and force re-parse; useful for one-off bugfix verification → TBD: L4 implementation detail pending
T.6.7 Backfill progress observability Per-tenant completion %, per-entity-type, with rate estimate → TBD: L4 implementation detail pending

User stories

T.7 — Cross-module substrate contracts

Purpose. T owes downstream modules specific guarantees about the canonical surface. This L2 is the contract registry: which fields, which preservation rules, which freshness commitments. Q's §Q.1 is the consumer view of these contracts; T.7 is the producer view.

Why it matters. A downstream module's L3 process can quietly fail if T silently changes a field's preservation. Naming the contracts here makes silent breakage impossible without explicit T-side change.

L3 contracts (registry)

ID Contract Owner downstream What T promises
T.7.1 Audit-log verbatim preservation Q (Q.1.5) ACTIV and LOG_ENTRY strings preserved exactly — no normalization, no trimming → TBD: L4 implementation detail pending
T.7.2 Original-doc-ref array always present Q (Q.2.2) Empty array means "no link"; missing array means "T didn't populate" — distinguishable → TBD: L4 implementation detail pending
T.7.3 Pricing-decision per-line snapshot Q (Q.1.6), P (derived) PS_DOC_LIN_PRICE flattened with PRC_JUST_STR, PRC_RUL_SEQ_NO, UNIT_PRC per line → TBD: L4 implementation detail pending
T.7.4 Multi-authority tax preservation Q (Q.2.8), F PS_DOC_TAX[] rows preserved per authority; not summed → TBD: L4 implementation detail pending
T.7.5 Drawer-session linkage Q (Q.2.4), N Every transaction carries DRW_ID + DRW_SESSION_ID + USR_ID for drawer-shrinkage rule chains → TBD: L4 implementation detail pending
T.7.6 DOC_TYP type-routing completeness D, J, F Every documented DOC_TYP routes to a CanonicalEvent type; unknown DOC_TYP routes to a quarantine event with full payload → TBD: L4 implementation detail pending
T.7.7 Customer reference upsert R Every CUST_NO reference triggers an R upsert before the transaction event publishes (or at the same time, with R catching up async per its T-card-defined posture) → TBD: L4 implementation detail pending
T.7.8 Item reference resolution S, P-derived Every ITEM_NO on a line carries enough context (category, mix-match group, vendor) for downstream joins without re-hitting Counterpoint → TBD: L4 implementation detail pending
T.7.9 Substrate freshness signal Q, every consumer Every CanonicalEvent carries polled_at so consumers can suppress stale-substrate alerts → TBD: L4 implementation detail pending
T.7.10 PII redaction at parse All SIG_IMG, SIG_IMG_VECTOR, raw PAN, raw CVV — never reach evidence_records past the parser; redacted in-flight → TBD: L4 implementation detail pending
T.7.11 Provider attribution All Every CanonicalEvent carries provider so consumer-side rule applicability (e.g., Square-only rules vs Counterpoint-only) is enforceable → TBD: L4 implementation detail pending
T.7.12 Tenant + company alias attribution All Every CanonicalEvent carries (tenant_id, company_alias) so multi-company-per-tenant Counterpoint deployments don't bleed events across companies → TBD: L4 implementation detail pending

User stories

Canary Detection Hooks

T Process → Detection Surface Signal Description
T.2 (Document type routing) All downstream modules T is the spine — every detection rule in Q, D, F, P, R depends on T's type-routing being correct. T does not itself fire Chirp rules; it is the ingest substrate
T.3 (Tender decomposition) Q-TM rule family Tender-mix anomalies detected during T.3 decomposition (e.g., unusual split-tender patterns, voided tenders) are published as Q-TM-01/02 signals
T.4.7 (XFER routing to D) D detection pipeline T.4.7 routes XFER-type documents to D's transfer-loss detection pipeline. T is the upstream source; D.3 is the consumer
T.5 (Return/void processing) Q-IS rule family Returns and voids routed through T.5 are accumulation inputs for Q-IS-02 (return-fraud pattern) and Q-IS-04 (void accumulation)

Additional User Stories

Assumptions requiring real-customer validation

These markers exist because the answer requires either a Counterpoint sandbox database, a real customer's Document corpus, or both.

ID Assumption What it blocks Resolution path
ASSUMPTION-T-01 Full DOC_TYP code list — sample only confirmed T for ticket; XFER/RECVR/PO/PREQ/RTV/GFC inferred from Workgroup numbering T.3.2 type-routing completeness; T.7.6 contract Sandbox DB inspection; multiple sample payloads needed
ASSUMPTION-T-02 Void DOC_TYP code and whether voids reference original via PS_DOC_HDR_ORIG_DOC T.3.2 + T.7.2 (original-doc contract) Sandbox DB workflow test — execute a void and inspect
ASSUMPTION-T-03 Other LIN_TYP codes beyond S, R, O, B, L, D — full taxonomy from PS_STR_CFG_PS.LIN_TYP_* T.3.3 line flattening completeness Sandbox config inspection
ASSUMPTION-T-04 IS_DOC_COMMITTED = "N" semantics — do uncommitted Documents appear in GET / should we ingest them T.1.3 poll-loop scope (skip uncommitted? ingest as draft?) Sandbox workflow test
ASSUMPTION-T-05 IS_OFFLINE Document handling — what changes at sync time when offline-processed events catch up T.6 backfill + idempotency edge case Sandbox DB or customer with offline-mode stores
ASSUMPTION-T-06 Counterpoint sandbox / FTP credentials — published path exists per Brain/wiki/ncr-counterpoint-sandbox-setup-checklist.md, but does the team actually have them T.1 entire ingress build path can't smoke-test without these Operator action — request from NCR partner contact
ASSUMPTION-T-07 On-prem reachability for production tenants — customer Counterpoint API server may have no public IP T.1.9 (on-prem reachability) — may force customer-side agent or tunneled architecture Per-customer at onboarding
ASSUMPTION-T-08 API option enablement rate across the installed base — Counterpoint REST is a paid registration.ini add-on If <50% of base has API enabled, T may need direct-DB fallback as primary path Customer-side discovery; possibly direct-DB SDD addition
ASSUMPTION-T-09 Rapid POS proprietary tables on top of standard Counterpoint schema Whether T must extend its parser surface for Rapid-specific data Direct conversation with Rapid POS / customer

Highest-leverage gaps: T-01 / T-02 (full DOC_TYP + void semantics) — these are the load-bearing structural assumptions for type-routing. Sandbox access (T-06) unblocks all of T-01 / T-02 / T-03 / T-04 / T-05 in one move.

Customer-specific overrides

Empty until a real customer engagement starts. Format reserved:

Customer: <name>
Counterpoint deployment shape: <on-prem | hosted | hybrid>
API hosting: <self-hosted Windows port 52000 | other>
Multi-company: <yes — N companies | no — single company>
Company aliases: [<alias1>, <alias2>, ...]
Backfill window: <epoch | <date> | <N years>>
Per-tenant poll budget: <reqs/min cap>
On-prem reachability path: <direct | customer-agent | tunnel>
Rapid POS extensions in scope: <yes — extension list | no>
Disabled DOC_TYPs (with reason):
  <type>: <reason>
  ...
ASSUMPTION resolutions:
  ASSUMPTION-T-NN: resolved as <answer>; source: <evidence>
  ...

Operating notes