purchasing — Module Spec

1. Purpose

purchasing owns the tenant's inbound supply chain — vendor management, purchase orders, three-way match (invoice line ↔ receipt line ↔ PO line), vendor invoices, credits, and returns — module #16 in the v2 build order, the buy-side counterpart to orders/pos. It completes the supply loop: orders/pos sell stock OUT (a stock_movement decrement); the receiving step brings stock IN (a stock_movement receipt), updating weighted-average cost. purchasing is deliberately bounded: it owns the vendor master, the PO, and the invoice DOCUMENT + 3-way match, but not payment (Billing owns A/P — vendor_invoice.status has no 'paid'), not the physical receiving step (Receiving owns it — see the 2026-07-10 update immediately below), not stock truth (InventoryService owns the movement; Receiving calls it), and not pricing (it snapshots vendor cost, frozen at PO time).

2026-07-10 update: Purchasing's own purchase_receipt/purchase_receipt_line tables — the physical receiving step — MOVED OUT into a brand-new receiving schema (renamed goods_receipt/goods_receipt_line). This is the 2nd time in this codebase a table set has left an already-locked module for one that didn't exist yet — the 1st was approval_workflow/approval_routing_rule/approval_request leaving Admin for the new approvals schema (PROJECT_DECISIONS #44). Boundary justification: Purchasing's own original lock (PROJECT_DECISIONS #30) already framed receiving as one process step among several in this module's own purpose sentence ("vendor management, purchase orders, receiving with three-way match, vendor invoices, credits, and returns") — never as an intrinsic, Purchasing-only concern the way the vendor master or the PO-send money gate are (§7 DR-C, §4 D7). It sat in Purchasing because that's simply where it was first built, next to the PO it fulfills, not because it structurally belonged there — the same realization Approvals made explicit about a workflow engine that had been built inside Admin first and only later recognized as its own concern (PROJECT_DECISIONS #44, reversing #34 Section 5's own Option B). Full delta accounting, rationale, and the bidirectional Purchasing↔Receiving seam: §7 DR-S. New module spec: docs/modules/module_spec/receiving.md.

Second module built under the Design-Phase Integrity rules; the full retained 4-block v1→v2 delta-accounting record lives in PROJECT_DECISIONS #30 (that entry's table/column counts reflect the module's shape AT LOCK TIME, 2026-07-07 — see §7 DR-S for the 2026-07-10 post-lock Receiving-extraction delta).

2. Ownership

Owns — 15 tables, 353 columns (down from 17/410 after the 2026-07-10 Receiving extraction; −2 tables/−57 cols — purchase_receipt/purchase_receipt_line MOVED OUT to the new receiving schema, see §7 DR-S), in 4 groups: Vendor Master (vendor, vendor_contact, vendor_address, vendor_item); Purchase Orders (purchase_order, purchase_order_line, purchase_order_template, purchase_order_template_line); Invoice + 3-Way Match (vendor_invoice, vendor_invoice_line, vendor_invoice_match); Credit + Return (vendor_credit, vendor_credit_line, vendor_return, vendor_return_line). Sum reconciles exactly: 410 (2026-07-10 Header/Line Remediation end state, §7 DR-Q/DR-R) − 30 (purchase_receipt) − 27 (purchase_receipt_line) = 353 (15 tables) — the 2 subtracted tables did not disappear, they moved to receiving (via ALTER TABLE ... SET SCHEMA, renamed, and extended +3/+2 cols respectively); see docs/modules/module_spec/receiving.md.

Does NOT own: payment (Billing — A/P write-backs into vendor_invoice/purchase_order); the physical receiving step (Receiving — receiving.goods_receipt/goods_receipt_line, moved out 2026-07-10; Purchasing still owns the PO the receipt is checked against and the invoice/3-way-match + vendor-return that read the receipt line back — see §9); stock truth/decrement (InventoryService.receive(), called from Receiving, writes the movement + avg-cost); the sale price (Pricing/orders/pos); shipping execution (tracking_number/carrier are plain text — these moved WITH the tables to receiving.goods_receipt; a future Delivery module); document storage (shipment_photo_ref → future Files, also now on receiving.goods_receipt rather than a Purchasing table).

3. Layer & Dependencies

Tenant-scoped operational layer, consuming multi_loc/shared/crm/identity as master data; downstream of orders (special-order source) and — as of 2026-07-10 — upstream of the new receiving module, which resolves the physical receipt against a purchase_order/purchase_order_line. Depends on: platform (tenant, set_updated_at, and — as of Remediation Phase 4 — legal_entity), multi_loc (site), shared (currency/country/administrative_region/unit_of_measure, and — as of Remediation Phase 4 — payment_terms_catalog), inventory (item_variant — catalog refs on vendor_item/purchase_order_line/vendor_invoice_line; stock_movement/lot — the return seam on vendor_return_line, direct and unaffected by the Receiving extraction), receiving (new, 2026-07-10goods_receipt_line, the 3-way-match/vendor-return line target; see §7 DR-S), crm (customer — the vendor↔customer link), orders (order_header — special orders + the draft_po_id closure), identity (actor/agent_duty_grant). Depended on by: Billing (A/P write-backs), Search (vendor.search_vector), Reporting, orders (whose draft_po_id seam this module closes), and receiving (new, 2026-07-10vendor/vendor_address/purchase_order/purchase_order_line, the PO context every goods_receipt is checked against; see §7 DR-S). Note: the older framing "downstream of inventory (the receiving target)" no longer describes Purchasing itself — inventory is now Receiving's target, not Purchasing's own (§7 DR-S).

4. Capabilities — honest Part D framing

Purchasing has the richest legitimate agent surface of any module so far — v1's own module spec dedicated 3 feature groups to AI (Group 6 invoice OCR, Group 7 cost-trend/performance alerts, Group 8 reorder forecasting). But it is also where real money first meets autonomy: a PO is a financial commitment. The load-bearing rule: agents DRAFT and FLAG; humans COMMIT.

The agent surface: a reorder agent drafts a PO from a low-stock signal (the flagship); an OCR agent drafts a vendor invoice from a scanned document; a cost-trend agent flags a vendor cost increase on vendor_item; a performance agent derives vendor lead-time/reliability. All route through draft_only/observational authority + the review seam. No table supports autonomous money commitment.

No PurchasingService exists yet — schema-only build.

D7 — Autonomy boundary, per action

Action Authority Rationale
Draft a reorder PO draft_only, automation_source='agent', review_status='pending' The flagship buy-side loop; commits nothing.
SEND a PO (commit money) needs_approval ALWAYS — never may_act_alone The C8 financial-autonomy boundary. No agent_duty_grant authority permits autonomous PO-send; a human approves every send (approval_status/approved_by).
Draft a vendor invoice from OCR draft_only, review_status='pending' Agent extracts; human confirms.
Flag a vendor cost increase may_act_alone to flag (observational) Writes a review flag on vendor_item; resolving is human.
Record a receipt (now receiving.goods_receipt — moved 2026-07-10, see §7 DR-S) human/system (physical event) Not an agent decision — unchanged by the move. receiving's own new over-receipt-tolerance trigger (tenant-configurable flag/block) is a DB-level guard, not an agent authority grant.

The spend-ceiling decision (resolved decision 2, PROJECT_DECISIONS #30): the cumulative agent_duty_grant spend-ceiling is DEFERRED, not reopened now. It protects against volume-based autonomous spend, but purchasing structurally forbids autonomous money commitment (PO-send is needs_approval always). Per-PO spend_limit_cents (bounding the draft) + ai.agent_usage_period (aggregation) suffice for v1; the human approval gate is the real money control. Trigger to reopen identity: when a may_act_alone PO-send-under-budget capability is first contemplated — cross-referenced to the existing identity/ai/pricing rows, not duplicated.

5. Service Contract — PurchasingService

Not built this pass — schema + migration + tests only, matching every product module's first-pass precedent. Binding build requirements (DR-1…DR-3 below) constrain PurchasingService/InventoryService when built.

6. Build Requirements (service-layer, binding)

DR-1 — (MOVED to receiving, 2026-07-10 — see §7 DR-S) receiving goes through InventoryService.receive() with an idempotency key, never a direct inventory.stock write

This requirement now binds a future ReceivingService, not PurchasingServicepurchase_receipt/purchase_receipt_line (and their idempotency_key) moved to receiving.goods_receipt/goods_receipt_line. Retained verbatim below as the historical record of what this module's own first lock required, not as a live Purchasing obligation: receiving a purchase_receipt_line MUST call InventoryService.receive(idempotency_key, ...), which writes an inventory.stock_movement (movement_type='received', source_module='purchasing' — kept as-is post-move, a disclosed naming asymmetry, see §7 DR-S) AND updates inventory.item_variant.avg_cost_cents (weighted-average from stock_unit_cost_cents). The idempotency key MUST be deterministic and passed to inventory.stock_movement.idempotency_key (its own unique index) so a re-run never double-receives. Receiving NEVER writes inventory.stock directly (v1 guard, preserved through the move). This binding contract's live copy belongs in docs/modules/module_spec/receiving.md §6 going forward.

DR-2 — PO-send is the money-commit gate; agents draft only

purchase_order.status='sent' (transmitting to the vendor) MUST require human approval — no agent may set it may_act_alone. The approval_status/approved_by_actor_id/approved_at workflow is that gate (DB CHECK: approved requires actor + timestamp).

DR-3 — the reorder loop reads inventory.stock, drafts a PO

The reorder-detection service reads inventory.stock.reorder_point/reorder_qty/available_qty and drafts a purchase_order (automation_source='agent', review_status='pending'). The signal lives on inventory.stock, NOT stock_adjustment_request (the adjustment gate). Not built yet.

7. Design Rationale (DR-A…S)

  • DR-A — v1→v2 delta (retained record). v1 16 tables / 341 cols → v2 16 / 398 (+57), zero consolidation. Full 4 blocks in PROJECT_DECISIONS #30.
  • DR-B — adversarial verification caught 3 BLOCKER + 2 disclosure issues in the proposal (a vendor off-by-one, conflicting totals, a crm LIGHT-tier mis-citation, a missing DR-E tier justification, an undisclosed column rename) — all resolved before build. See #30.
  • DR-C — vendor master in purchasing, NOT crm. Vendors and customers are fundamentally different relationships (we pay vs. sell; payment_terms/1099/purchase-cost-catalog vs. credit/tax-certs/segments). v1's explicit guard, preserved. A grower who also buys retail gets two linked records via the new vendor.linked_customer_id → crm.customer (nullable, real FK) — link, don't merge.
  • DR-D — vendor_invoice has no 'paid' status. Purchasing owns the document; Billing owns payment (single owner per fact). billing_ap_ref/payment_status_ref/paid_at are Billing write-backs.
  • DR-E — LIGHT tier carries automation_source uniformly. A deliberate, disclosed deviation from crm's created_by-only LIGHT tier — knowing human/agent/system provenance is useful on every purchasing row given the module's rich agent surface. purchase_receipt_line is LIGHT (not FULL) because the accept/reject inspection is a human physical-dock decision; the receiving-anomaly review seam lives on the FULL purchase_receipt header — matching orders' order_fulfillment[FULL] / order_fulfillment_line[ZERO] split (this line gets automation_source because it writes to inventory). (2026-07-10: purchase_receipt/purchase_receipt_line themselves moved to receiving.goods_receipt/goods_receipt_line — the LIGHT/FULL tier split and its rationale carried over unchanged with the tables, now describing Receiving's own schema, not Purchasing's. See DR-S.)
  • DR-F — vendor_return/vendor_credit are LIGHT (resolved decision 3). Human/mechanical AP processes (a human authorizes an RMA; a credit memo is applied), not agent-proposal surfaces. No review seam.
  • DR-G — dual-UOM cost snapshot frozen at PO time. purchase_unit_cost_cents/purchase_to_stock_factor/stock_unit_cost_cents on purchase_order_line, preserved verbatim — the 3-way-match inputs; back-calculating the conversion later is error-prone.
  • DR-H — 3-way match at line grain (N:M). vendor_invoice_match links invoice line to receipt line; a single invoice may cover lines from multiple POs/receipts. Mutable (Audit owns immutable history). (2026-07-10: the receipt-line target retargeted to receiving.goods_receipt_line when that table moved out — the N:M/mutable shape itself is unchanged. See DR-S.)
  • DR-I — the 3 forward-ref FKs are REAL now. purchase_receipt_line.inventory_movement_id, vendor_return_line.inventory_movement_id, purchase_order.source_order_id — v1 deferred them (targets unbuilt); all exist live now, so all enforced. v1's column name inventory_movement_id KEPT on both (no rename). (2026-07-10: the first of these 3 moved with its table — it's now receiving.goods_receipt_line.stock_movement_id, renamed there since it left Purchasing's own v1-naming scope. vendor_return_line.inventory_movement_id stays in Purchasing, unchanged — still a bare, not composite, FK into inventory.stock_movement, a pre-existing gap out of the Receiving extraction's own named scope. See DR-S.)
  • DR-J — draft_po_id closure. This module closes the forward-ref orders locked with, via one ALTER (order_header_draft_po_id_fkey) — not an orders reopen.
  • DR-K — global-first vendor_address. Reshaped to the shared-FK pattern (region/country natural keys, no 'US'/'USD' defaults, +region/country CHECK), mirroring crm.address incl. its 2026-07-07 NOT-NULL erosion fix.
  • DR-L — receiving idempotency_key added. v1's rationale referenced it but the schema omitted it; added (NULL-safe unique) for the InventoryService.receive() dedup. (2026-07-10: this column now lives on receiving.goods_receipt.idempotency_key, not on a Purchasing table — moved with purchase_receipt. See DR-S.)
  • DR-M — spend-ceiling deferred (see §4). DR-N — no PurchasingService yet (schema-only).
  • DR-O — Remediation Phase 1 (2026-07-08). Cross-cutting senior-architect-review remediation added chk_purchase_order_sent_requires_approval (a PO can't reach status IN ('sent','partial','received') without approval_status='approved' recorded — the C8 boundary) and chk_purchase_order_no_self_approval (approved_by_actor_idcreated_by_actor_id when both set) to purchase_order; and chk_vendor_credit_fully_applied_requires_actor_at/chk_vendor_credit_void_requires_actor_at to vendor_credit. Same-day corrective migration 20260708160000 fixed a live NULL-bypass bug in the first CHECK (a bare approval_status = 'approved' comparison passes when NULL) by adding an explicit approval_status IS NOT NULL guard. No column/table count change. Full cross-module record: PROJECT_DECISIONS #37.
  • DR-P — Remediation Phase 4 (2026-07-08), Items 15 + 17b. Futureproofing pass, 398 → 402 cols (+4), 16 tables unchanged. Item 15 (Legal Entity): purchase_order.entity_id and vendor_invoice.entity_id (both nullable uuid FK → new platform.legal_entity) — 2 of the 10 tables independently derived as plausibly differing per legal entity within one tenant. Header-only by disclosed rule (no entity_id on line items or on vendor itself — a line item's entity is always inherited via its header's FK). Item 17b (Payment Terms enum→catalog, additive-interim): vendor.payment_terms_id and purchase_order.payment_terms_id (both nullable uuid FK → new shared.payment_terms_catalog, 10 seeded rows incl. 2_10_net_30, representing complex terms like "2/10 net 30" a bare CHECK-enum can't). Purely additive — vendor.payment_terms's existing CHECK-enum is completely unchanged; no reconciliation, no backfill. Disclosed interim gap: the new FK is not yet constrained to stay in sync with the legacy enum; the eventual cutover needs a real mapping decision for 4 catalog codes (cod/prepaid/net_7/2_10_net_30) with no clean vendor.payment_terms equivalent — deferred, logged to OPEN_ITEMS. All 4 columns confirmed nullable, FK-valid, and zero-backfill-risk (both source tables 0 rows at build time). Migrations: packages/db/migrations/20260709010000_phase4_item15_legal_entity.sql, packages/db/migrations/20260709030000_phase4_item17_enum_to_catalog.sql (part b). Independently live-reproduced and adversarially verified (2 separate lenses, CLEAN). Full cross-module record: PROJECT_DECISIONS #40.
  • DR-Q — Header/Line Remediation reopen (2026-07-10), fixes #10/#12/#4 — +1 table/+8 cols (402→410, 16→17 tables). Second of 3 coordinated reopens (POS → Purchasing → Platform, design doc vrida-header-line-remediation-design-2026-07-10.md §4) bundled per the user's own request into one pass. Fix #10 (must-fix): chk_purchase_order_line_quantity_rollup on purchase_order_line — the one qty-counter-without-status table missing its own rollup CHECK; mandatory pre-migration audit found zero violating rows live (table was empty), added directly. Fix #12 (guardrail): line_number partial-unique indexes on purchase_receipt_line/vendor_invoice_line/vendor_return_line/purchase_order_template_line, matching purchase_order_line's own pre-existing shape; mandatory dedup audit found zero duplicates on all 4 (all zero-row at the time). Fix #4 (structural): new table vendor_credit_line (8 cols, ZERO tier, write-once) — per-line decomposition of vendor_credit, 3 composite FKs (col, tenant_id) → (id, tenant_id) into vendor_credit/vendor_invoice_line/vendor_return_line (all 3 needed a new prerequisite UNIQUE(id, tenant_id), confirmed missing on all 3), chk_vendor_credit_line_target (exactly one of invoice-line/return-line, or neither — never both), and a HEADER-IS-TRUTH reconciliation trigger trg_vendor_credit_line_validate_against_credit rejecting any line-sum that would exceed vendor_credit.credit_amount_cents. Fully additive, zero backfill. Migration: packages/db/migrations/20260710010000_headerline_purchasing_fixes.sql. Live-reproduced and test-confirmed (purchasing-schema.spec.ts sections L/M/N, 58/58). See PROJECT_DECISIONS #47.
  • DR-R — Header/Line Remediation batch 2, closing bare-FK fix (2026-07-10) — constraint-only, no table/column count change. vendor_invoice_match.vendor_invoice_line_id upgraded from a bare FK to composite vendor_invoice_match_vendor_invoice_line_id_tenant_fkey (vendor_invoice_line_id, tenant_id) → vendor_invoice_line(id, tenant_id), using DR-Q's own vendor_invoice_line_id_tenant_id_unique prerequisite (added by fix #4 above, not by this later batch). purchase_receipt_line_id deliberately stays bare (purchase_receipt_line has no UNIQUE(id, tenant_id), out of scope) — logged to OPEN_ITEMS. This is the LAST item in the entire 2-batch Header/Line Remediation effort. Full detail: docs/database/schema_docs/purchasing.md's own "Header/Line Remediation Batch 2" subsection. See PROJECT_DECISIONS #53. (Superseded 2026-07-10, same day, by a separate later reopen: the Receiving extraction closed this exact "deliberately stays bare" gap too — renamed goods_receipt_line_id, now composite — when purchase_receipt_line itself moved out and gained its own UNIQUE(id, tenant_id) as part of that move. Not credited to this fix; see DR-S.)
  • DR-S — Receiving extraction reopen (2026-07-10) — 3-module coordinated reopen (Inventory + Purchasing + new receiving), −2 tables/−57 cols (410→353, 17→15 tables). Design doc vrida-cc-task-2026-07-10-orphan-fix-and-receiving-design.md §B.1–B.18, independently verified before build (2 BLOCKERs found and fixed at design time, both live-reproduced during the build itself — see below). The 2nd time in this codebase a table set has moved out of an already-locked module into a brand-new one — the 1st was approval_workflow/approval_routing_rule/approval_request leaving Admin for the new approvals schema (PROJECT_DECISIONS #44). purchase_receipt/purchase_receipt_line MOVED + RENAMED to receiving.goods_receipt/goods_receipt_line (33/29 cols there, +3/+2 new columns — void-attribution on the header, movement-line linkage + reversal linkage on the line; full detail in docs/modules/module_spec/receiving.md). Boundary justification (see also §1's own 2026-07-10 update): this module's original lock (PROJECT_DECISIONS #30) already listed receiving as one process step among several in the purpose sentence ("vendor management, purchase orders, receiving with three-way match, vendor invoices, credits, and returns") — never as an intrinsic, Purchasing-only concern the way the vendor master (DR-C) or the PO-send money gate (§4 D7, §6 DR-2) are. It sat in Purchasing because that's where it was first built, next to the PO it fulfills, not because it structurally belonged there — the same realization Approvals made explicit about a workflow engine that had simply been built inside Admin first (PROJECT_DECISIONS #44, reversing #34 Section 5's own Option B). Physical receiving has its own lifecycle, its own review seam, and (as of this reopen) its own tenant-configurable over-receipt-tolerance policy — none of which are PO or vendor-master concerns. Purchasing's own share of this reopen: 4 new UNIQUE(id, tenant_id) prerequisites (vendor, vendor_address, purchase_order, purchase_order_line) — needed by receiving.goods_receipt's own new composite FKs back into Purchasing (the dependency now runs BOTH ways: Receiving reads Purchasing's PO/vendor context; Purchasing's 3-way-match and vendor-return read Receiving's line back — see §3, §9). 2 FKs retargeted at the new schema: vendor_invoice_match.purchase_receipt_line_id and vendor_return_line.purchase_receipt_line_id both RENAMED goods_receipt_line_id and upgraded to composite (goods_receipt_line_id, tenant_id) → receiving.goods_receipt_line(id, tenant_id) — the 2nd of these two was a dependency this extraction's own design-phase independent verification caught that the first design draft had missed (the first draft named only vendor_invoice_match). This closes 2 previously-open items: the vendor_invoice_match.purchase_receipt_line_id bare-FK gap DR-R itself disclosed as deliberately out of scope is now resolved by this LATER, separate reopen, not by DR-R's own fix (see DR-R's own annotation above); and the fix #7 (movement-line linkage) / fix #11 (over-receipt tolerance) cross-reference that had been tracking both as "deferred to the upcoming Receiving extraction" — both are now built, as receiving's own capabilities, not PurchasingService's (see §6 DR-1's own MOVED annotation). 3 real bugs found and fixed during this build's own live-reproduction pass (distinct from, and in addition to, 2 BLOCKERs the pre-build design-phase independent verification already caught and fixed before any schema was touched): (1) receiving.goods_receipt was initially missing its own UNIQUE(id, tenant_id), needed because goods_receipt_line references its header via a composite FK — the first migration attempt failed outright on this; (2) the admin.custom_field_definition backfill for the retired entity_type CHECK value ('purchasing.purchase_receipt''receiving.goods_receipt', 11 live test-fixture rows) hit a genuine chicken-and-egg constraint-migration ordering bug, resolved by dropping the CHECK, backfilling unconstrained, then re-adding the CHECK; (3) the new over-receipt-tolerance trigger's ::numeric cast on a tenant/site setting value had no defensive handling — a corrupted catalog value would have crashed the trigger (blocking all future receiving for that tenant) instead of failing safe; fixed via BEGIN/EXCEPTION WHEN invalid_text_representation falling back to the existing COALESCE(...,0) path. All 13 composite (col, tenant_id) → parent(id, tenant_id) FKs touched by this extraction (across both Purchasing and Receiving) were individually live-reproduced for cross-tenant rejection, isolating exactly one mismatched FK column per test. Migration: packages/db/migrations/20260710090000_receiving_extraction.sql. Full record: PROJECT_DECISIONS #55; new module spec: docs/modules/module_spec/receiving.md; new schema docs: docs/database/schema_docs/receiving.md.

8. Agent Authority Mapping

No new authority mechanism — a pure consumer of identity.agent_duty_grant. Permission codes: purchasing:po:draft_reorder, purchasing:invoice:draft_from_ocr, purchasing:vendor_item:flag_cost_change — all draft_only/observational, gated by spend_limit_cents where money is involved. Agent actions log to ai.agent_execution (target_module='purchasing'); a later human approval is a second row linked via resolves_execution_id. decision_provenance.memory_refsai.agent_memory.

9. Cross-Module Seams

Cataloged in CROSS_MODULE_CONTRACTS.md. Key: purchasing → inventory (item_variant catalog refs, reorder-signal read on inventory.stock, and vendor_return_line's own direct inventory_movement_id — the receiving/avg-cost seam itself moved to receiving → inventory, 2026-07-10, §7 DR-S), purchasing ↔ receiving (new, 2026-07-10receiving.goods_receipt_line is the 3-way-match/vendor-return line target; receiving.goods_receipt reads purchasing.vendor/vendor_address/purchase_order/purchase_order_line as its own PO context — a genuinely bidirectional seam, see §7 DR-S), purchasing ↔ orders (source_order_id + draft_po_id closure), purchasing → crm (linked_customer_id), purchasing → shared (currency/country/region/uom, and — as of Remediation Phase 4 — payment_terms_catalog), purchasing → platform (as of Remediation Phase 4 — legal_entity), purchasing → identity (actor), Billing → purchasing (A/P write-backs, deferred), Search → purchasing (vendor.search_vector, deferred), Files → purchasing (shipment_photo_ref — moved to receiving.goods_receipt.shipment_photo_ref with the table, still deferred).

10. Deferred / Future Items

Tracked in OPEN_ITEMS (9 purchasing rows, plus Remediation Phase 4 additions): Billing write-backs; shipment_photo_ref (Files); search_vector+trgm (Search/pg_trgm); no PurchasingService; the reorder→PO service loop; the AI Invoice OCR pipeline; JSONB example shapes; plus cross-references (spend-ceiling → identity/ai/pricing; over-receipt idempotency → pos/orders). New from Phase 4: the payment_terms_id/vendor.payment_terms enum-to-catalog sync-cutover decision (incl. the 4-code vocabulary mapping for cod/prepaid/net_7/2_10_net_30), and the legal_entity new-tenant-provisioning gap (no trigger/service creates a legal_entity row for tenants created after Phase 4's backfill, cross-referenced from PROJECT_DECISIONS #40 — a platform-owned gap, not purchasing-specific). All 16 v1 tables built — zero deferred (unlike pos's 10-table deferral). The 2026-07-10 Header/Line Remediation reopen (DR-Q) added zero new OPEN_ITEMS rows — all 3 fixes (#10/#12/#4) closed clean with no deferral (zero live violations found on any of the audited tables, no data cleanup needed).

The 2026-07-10 Receiving extraction (DR-S) CLOSES 2 more previously-open rows, neither of which needed a Purchasing-side deferral of its own to close: (1) the vendor_invoice_match.purchase_receipt_line_id bare-FK gap that DR-R itself had disclosed as deliberately out of scope ("stays bare... purchase_receipt_line has no UNIQUE(id, tenant_id)") — now resolved by this LATER, separate reopen, not by DR-R's own fix (see DR-R's own annotation, §7); and (2) the purchasing↔inventory cross-reference row tracking fix #7 (movement-line linkage) / fix #11 (over-receipt tolerance) as "deferred to the upcoming Receiving extraction" — both are now built, as receiving's own capabilities. Row 206's JSONB-example-shapes gap narrows by one column: purchase_receipt.attributes moved to receiving.goods_receipt.attributes with the table itself, so documenting its example shape is now receiving's own gap, not Purchasing's — the remaining 3 (vendor.attributes, vendor.blackout_config, vendor_item.attributes) stay open here. The 9 base purchasing rows are otherwise unaffected and remain open exactly as before (Billing write-backs, search_vector+trgm, no PurchasingService, the reorder→PO service loop, the AI Invoice OCR pipeline, JSONB example shapes for the 3 remaining columns, and the 2 cross-references — spend-ceiling → identity/ai/pricing, and the general over-receipt/cross-writer-stock-race idempotency discipline → pos/orders, which now also binds a future ReceivingService, not only PurchasingService). ReceivingService does not exist yet either — schema-only, same pattern as every other product module's first pass — but that gap belongs to receiving's own OPEN_ITEMS rows, not Purchasing's.

11. v1 Exclusions Re-Confirmed

None at the table level. All 16 of v1's tables survive BUILT (PROJECT_DECISIONS #30, Block 3). The only exclusions are column-level forward-refs into unbuilt modules (Billing/Files/Search) and the deferred search_vector — a different category from a table-level exclusion. vendor_credit_line (added 2026-07-10, DR-Q) has no v1 precedent at all — a genuinely new v2-only table, not a v1 table being restored or excluded.

2 of v1's 16 tables — purchase_receipt/purchase_receipt_line — are no longer owned by Purchasing as of 2026-07-10. This is a MOVED classification, not a table-level exclusion or a drop — the same category PROJECT_DECISIONS already established for tenant_business_profileplatform.tenant_profile (#34/#35) and for approval_workflow/approval_routing_rule/approval_requestapprovals (#44), applied here for the 2nd time to a table set leaving an already-locked module for a brand-new one (the exact parallel is the Approvals case, not the Admin/Platform identity absorption — Receiving, like Approvals, didn't exist as a module before this move). Both tables survive BUILT, renamed (goods_receipt/goods_receipt_line), inside receiving — see §7 DR-S and docs/modules/module_spec/receiving.md. Purchasing's own v1-table accounting is therefore: 14 of 16 still resident here, 2 MOVED out, 0 dropped, 0 excluded.

Last modified: Jul 10, 2026, 5:42 PM PT
On this page
Esc