Specification · p2p @ 0.1-draft

p2p — Procure to Pay · Area Specification

SpecActsInvariants

Spec

Status: 0.1-draft SKELETON — curated before implementation, on purpose. This is the artifact to argue with: every section below is a claim about how SMB purchasing should work under governed agents, written down so a design partner can tell us where it is wrong before a line of the module exists. Open questions are marked ⚑.

o2c's mirror image: o2c is money coming in for things we sell; p2p is money going out for things we buy. Same doctrine — one definition per act, agent and UI on the same guards, one audit trail, refusals that name their numbers.

1. Calibration: who this is for

Same band as o2c: 5–200 people, one legal entity, one currency, one warehouse. Businesses that BUY stocked goods on terms — distributors, wholesalers, light manufacturers — plus the service bills every business has. Between a toy and SAP MM: no requisition chains, no RFQ tournaments, no consignment; a real match gate, real receipt discipline, real AP.

2. Scope

In: vendors, purchase orders, goods receipts, vendor bills, the 3-way match, AP aging, payment RECORDING, debit notes, duplicate-bill guard, GRNI visibility, item cost from receipts.

Out (see §9): payment EXECUTION (no rails, ever — INV-22 family), landed cost, multi-currency, FIFO/specific-identification costing, requisitions and approval chains, drop-ship, consignment, recurring bills.

3. Entities and lifecycles

4. The acts (draft list — names are the contract, counts are not)

Writes (~15): create_po, issue_po, amend_po, cancel_po, close_po_short, receive_goods, record_vendor_bill, dispute_bill, approve_bill, void_bill, record_bill_payment, apply_bill_payment, record_debit_note, apply_debit_note, return_to_vendor.

Reads (~6): po, vendor_position, bill (the match report rides on it), ap_aging, unbilled_receipts (the GRNI list), payment_worklist (approved bills by due date).

Environment acts (core): create_vendor, update_vendor (banking fields human-only), set_match_tolerance.

⚑ Open: does approve_bill need its own permission tag (ap.approve) or does o2c's credit.authority pattern generalize to a shared "money authority" tag?

5. Invariants (P2P-n, namespaced like CRM-n/JH-n)

⚑ Open: is GRNI accrual right for the smallest books, or should sub-N-employee mode expense-on-receipt? (Lean: GRNI always — it is derivable, and hiding it is how month-end surprises happen. A design partner may prove us wrong.)

6. Required read models

vendor_position (open POs, unbilled receipts, open AP, next payments due), ap_aging (mirror of AR aging), unbilled_receipts, payment_worklist, and the bill read carrying its full match evidence — the artifact you hand an auditor.

7. Contract vs freedom

Contract: the match gate at approval; receipt-only stock entry; human-only banking; derived cost; payment recording only. Freedom: tolerance values, PO numbering, whether service bills 2-way match (no receipt) — ⚑ likely contract as "2-way for unstocked lines," to be argued.

8. Conformance (planned scenarios)

01 happy path PO→receipt→bill→approve→pay · 02 the match gate refuses (price high, qty over, then tolerance raised with reason — the credit-gate story, mirrored) · 03 partial receipts and GRNI · 04 dispute → resolve → pay · 05 duplicate bill guard · 06 debit note and return-to-vendor · 07 the agent tries to change vendor banking (refused, logged) · 08 costing: two receipts at different costs → moving average → COGS on next o2c shipment.

9. Deferred — with reasons

ItemWhy deferred, not rejected
Payment execution / bank filesThe system records; it never moves money. Anything else changes what Saybooks is.
Landed costReal for importers; drags freight allocation design. Own pass.
Multi-currencySame whole-area concern as o2c §9.
FIFO / specific identificationMoving average first; method choice needs a design partner with a reason.
Requisitions & approval chainsSMB calibration: one approval act with a permission tag. Chains are enterprise theater until someone proves otherwise.
Drop-ship / consignmentStock-ownership semantics beyond one warehouse.
Recurring billsSubscription lifecycle, different vertical.

---

Change log: 0.1-draft skeleton (2026-08-24) — curated before implementation; written for design partners to mark up. No module claims this spec yet; the conformance machinery ignores it until one does.

Acts

ActKindRequired
create_powritevendor lines
issue_powritepo
amend_powritepo
cancel_powritepo reason
close_po_shortwritepo reason
receive_goodswritepo lines
record_vendor_billwritevendor bill_number lines
dispute_billwritebill reason
approve_billwritebill
void_billwritebill reason
record_bill_paymentwritevendor amount
apply_bill_paymentwritepayment bill
record_debit_notewritevendor amount kind reason
apply_debit_notewritedebit_note bill
return_to_vendorwritepo lines reason
poreadpo
vendor_positionreadvendor
billreadbill
ap_agingread
unbilled_receiptsread
payment_worklistread
create_vendorenvironmentname
update_vendorenvironmentvendor
set_match_toleranceenvironmentreason

Invariants

IdInvariant
P2P-1Money is integer cents, everywhere.
P2P-2Nothing is paid without an approved bill; nothing approves without a match on the record.
P2P-3The 3-way match names its numbers; tolerance is config, breach is a refusal.
P2P-4Stock enters only through a receipt; receipts are immutable facts.
P2P-5Item cost is derived from receipts, never entered.
P2P-6Vendor bank details are human-only; an agent can record a bill, never redirect money.
P2P-7A disputed bill cannot be paid; dispute and resolution carry reasons.
P2P-8PO prices freeze at issue; variances are shown, never averaged away.
P2P-9Payments are recorded, never executed — no outbound rails.
P2P-10The duplicate-bill guard surfaces warnings; it never auto-resolves them.
P2P-11Over-receipt and over-billing are refusals naming the quantities.
P2P-12Every write is a logged command with an actor; refusals are logged too.
P2P-13Journal entries derive balanced: receipt to GRNI, approval to AP, payment to cash; COGS arrives with cost.
P2P-14Goods received not invoiced is always visible — a real liability, not a residue.