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-10 — goods_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-10 — vendor/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 PurchasingService — purchase_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
vendoroff-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_invoicehas no'paid'status. Purchasing owns the document; Billing owns payment (single owner per fact).billing_ap_ref/payment_status_ref/paid_atare Billing write-backs. - DR-E — LIGHT tier carries
automation_sourceuniformly. A deliberate, disclosed deviation from crm'screated_by-only LIGHT tier — knowing human/agent/system provenance is useful on every purchasing row given the module's rich agent surface.purchase_receipt_lineis LIGHT (not FULL) because the accept/reject inspection is a human physical-dock decision; the receiving-anomaly review seam lives on the FULLpurchase_receiptheader — matching orders'order_fulfillment[FULL] /order_fulfillment_line[ZERO] split (this line getsautomation_sourcebecause it writes to inventory). (2026-07-10:purchase_receipt/purchase_receipt_linethemselves moved toreceiving.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_creditare 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_centsonpurchase_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_matchlinks 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 toreceiving.goods_receipt_linewhen 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 nameinventory_movement_idKEPT on both (no rename). (2026-07-10: the first of these 3 moved with its table — it's nowreceiving.goods_receipt_line.stock_movement_id, renamed there since it left Purchasing's own v1-naming scope.vendor_return_line.inventory_movement_idstays in Purchasing, unchanged — still a bare, not composite, FK intoinventory.stock_movement, a pre-existing gap out of the Receiving extraction's own named scope. See DR-S.) - DR-J —
draft_po_idclosure. 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_keyadded. v1's rationale referenced it but the schema omitted it; added (NULL-safe unique) for theInventoryService.receive()dedup. (2026-07-10: this column now lives onreceiving.goods_receipt.idempotency_key, not on a Purchasing table — moved withpurchase_receipt. See DR-S.) - DR-M — spend-ceiling deferred (see §4). DR-N — no
PurchasingServiceyet (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 reachstatus IN ('sent','partial','received')withoutapproval_status='approved'recorded — the C8 boundary) andchk_purchase_order_no_self_approval(approved_by_actor_id≠created_by_actor_idwhen both set) topurchase_order; andchk_vendor_credit_fully_applied_requires_actor_at/chk_vendor_credit_void_requires_actor_attovendor_credit. Same-day corrective migration20260708160000fixed a live NULL-bypass bug in the first CHECK (a bareapproval_status = 'approved'comparison passes when NULL) by adding an explicitapproval_status IS NOT NULLguard. 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_idandvendor_invoice.entity_id(both nullableuuidFK → newplatform.legal_entity) — 2 of the 10 tables independently derived as plausibly differing per legal entity within one tenant. Header-only by disclosed rule (noentity_idon line items or onvendoritself — a line item's entity is always inherited via its header's FK). Item 17b (Payment Terms enum→catalog, additive-interim):vendor.payment_terms_idandpurchase_order.payment_terms_id(both nullableuuidFK → newshared.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 cleanvendor.payment_termsequivalent — 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_rolluponpurchase_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_numberpartial-unique indexes onpurchase_receipt_line/vendor_invoice_line/vendor_return_line/purchase_order_template_line, matchingpurchase_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 tablevendor_credit_line(8 cols, ZERO tier, write-once) — per-line decomposition ofvendor_credit, 3 composite FKs(col, tenant_id) → (id, tenant_id)intovendor_credit/vendor_invoice_line/vendor_return_line(all 3 needed a new prerequisiteUNIQUE(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 triggertrg_vendor_credit_line_validate_against_creditrejecting any line-sum that would exceedvendor_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.tssections 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_idupgraded from a bare FK to compositevendor_invoice_match_vendor_invoice_line_id_tenant_fkey (vendor_invoice_line_id, tenant_id) → vendor_invoice_line(id, tenant_id), using DR-Q's ownvendor_invoice_line_id_tenant_id_uniqueprerequisite (added by fix #4 above, not by this later batch).purchase_receipt_line_iddeliberately stays bare (purchase_receipt_linehas noUNIQUE(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 — renamedgoods_receipt_line_id, now composite — whenpurchase_receipt_lineitself moved out and gained its ownUNIQUE(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 docvrida-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 wasapproval_workflow/approval_routing_rule/approval_requestleaving Admin for the newapprovalsschema (PROJECT_DECISIONS #44).purchase_receipt/purchase_receipt_lineMOVED + RENAMED toreceiving.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 indocs/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 newUNIQUE(id, tenant_id)prerequisites (vendor,vendor_address,purchase_order,purchase_order_line) — needed byreceiving.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_idandvendor_return_line.purchase_receipt_line_idboth RENAMEDgoods_receipt_line_idand 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 onlyvendor_invoice_match). This closes 2 previously-open items: thevendor_invoice_match.purchase_receipt_line_idbare-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, asreceiving's own capabilities, notPurchasingService'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_receiptwas initially missing its ownUNIQUE(id, tenant_id), needed becausegoods_receipt_linereferences its header via a composite FK — the first migration attempt failed outright on this; (2) theadmin.custom_field_definitionbackfill for the retiredentity_typeCHECK 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::numericcast 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 viaBEGIN/EXCEPTION WHEN invalid_text_representationfalling back to the existingCOALESCE(...,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_refs → ai.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-10 — receiving.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_profile → platform.tenant_profile (#34/#35) and for approval_workflow/approval_routing_rule/approval_request → approvals (#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.