{"spec":"AIP-BS-INVOICE-CHECK","version":"2026-10-03","standard":"invoice-check-build-standard","source":"https://aipathway.com.au/explore-ai/invoice-check-build-standard","markdown":"https://aipathway.com.au/explore-ai/invoice-check-build-standard.md","licence":"CC BY 4.0","how_to_read":"Nine of these eleven checks run on your own machine, against the stub, before you talk to anybody. The last one is a live tenant, and that one runs against your own live systems. One is not a software check at all: it asks what you measured before you started.","verdicts":{"pass":"asserted green here","fail":"asserted red here. This is a fixture: it names the check to price","shape_only":"shape holds on the stub, truth needs the live provider","with_us":"cannot be proved on the stub; runs against your own live systems (see with_us_next)","evidence_required":"not a software check. Attach what proves it"},"tally":{"total":11,"onStub":9,"productionOnly":1,"partial":2,"preconditions":1},"envelope_version":"1.0","port_shape":{"read_ledger":{"verb":"read","subject":"ledger"},"read_job":{"verb":"read","subject":"job"},"read_asset":{"verb":"read","subject":"asset"},"read_rule":{"verb":"read","subject":"rule"},"read_contacts":{"verb":"read","subject":"contacts"},"read_segment":{"verb":"read","subject":"segment"},"resolve_address":{"verb":"read","subject":"address"},"write_job":{"verb":"write","subject":"job"},"read_pricebook":{"verb":"read","subject":"pricebook"},"draft_quote":{"verb":"write","subject":"quote"},"write_invoice":{"verb":"write","subject":"invoice"},"write_ledger":{"verb":"write","subject":"ledger"},"record_promise":{"verb":"write","subject":"promise"},"record_service":{"verb":"write","subject":"service_record"},"release_payment":{"verb":"write","subject":"payment"},"talk":{"verb":"say","subject":"turn"},"disclose":{"verb":"say","subject":"disclosure"},"send_sms":{"verb":"say","subject":"sms"},"notify":{"verb":"say","subject":"alert"},"escalate":{"verb":"say","subject":"handoff"},"issue_notice":{"verb":"say","subject":"notice"},"answer":{"verb":"say","subject":"answer"},"publish_report":{"verb":"say","subject":"report"},"record_consent":{"verb":"write","subject":"consent"},"read_file":{"verb":"read","subject":"file"},"write_file":{"verb":"write","subject":"file"},"raise_task":{"verb":"write","subject":"task"},"read_node":{"verb":"read","subject":"node"},"write_node":{"verb":"write","subject":"node"},"read_register":{"verb":"read","subject":"register"},"write_register":{"verb":"write","subject":"register"},"index_passage":{"verb":"attach","subject":"passage"},"search_index":{"verb":"read","subject":"index"},"read_inbox":{"verb":"read","subject":"message"},"file_message":{"verb":"attach","subject":"message"},"write_order":{"verb":"write","subject":"order"},"read_quote":{"verb":"read","subject":"quote"},"write_log":{"verb":"write","subject":"log"},"read_sales":{"verb":"read","subject":"sales"},"read_sale":{"verb":"read","subject":"sale"},"draft_message":{"verb":"write","subject":"draft"},"send_message":{"verb":"say","subject":"message"},"evidence":{"verb":"attach","subject":"evidence"},"cite":{"verb":"attach","subject":"citation"},"rank":{"verb":"decide","subject":"order"},"gate":{"verb":"decide","subject":"gate"}},"provider":{"boundary":{"never":["approve a claim","alter a rate","match a PO by guesswork","pass a variation without its approval"],"stays_with_person":["approval","variations","disputes with a subcontractor"]},"job":"subcontractor invoices checked before they bounce","skill":"invoice-check","stub":"local.mock_invoices","production":null,"product":null,"insert":null,"with_us":{"kind":"own_systems","note":"No hosted step. The production check runs against the job system and ledger already in use."}},"with_us_next":"No hosted step. The production check runs against the job system and ledger already in use. There is nothing to buy: your build does this step on the business's own systems. No production run is offered yet, because the live port map is not published, so do not run AIPATHWAY_ENV=production. Run the pass test by hand on the real systems and attach the evidence.","default_path":null,"boundary":{"never":["approve a claim","alter a rate","match a PO by guesswork","pass a variation without its approval"],"stays_with_person":["approval","variations","disputes with a subcontractor"]},"substrate":{"how_to_read":"These are roles, not products. Work out which you already have by matching `providers[].detect` against the repository, then build or add only the gap. No check asserts against a vendor, so any substrate that answers the role passes the same checks.","requires":[{"role":"record","label":"Somewhere the book lives","need":"Durable storage that the workflow owns the write path to, keyed so the same record can be found twice.","when_missing":"Half a day. Any Postgres will do and the free tiers are ample at this size. The trap is not the database, it is letting people edit around the app that is supposed to own the value.","ports":["read_ledger","evidence","read_rule","release_payment"]},{"role":"schedule","label":"Something that runs it on time","need":"A trigger that fires on a cadence and can call an authenticated endpoint.","when_missing":"An hour. Expose one authenticated endpoint that starts the work and let anything call it, so the schedule is never welded to whoever is hosting you.","ports":["gate"]},{"role":"surface","label":"Something a person opens","need":"A view of the book that whoever does this job can read and act on without asking a developer.","when_missing":"Start at tier 0 and stop there until the business asks for more. The cheapest surface is not a screen, it is a scheduled email carrying the list, and for one person doing this job that is not a compromise, it is the right answer. On Microsoft 365 or Workspace you can skip straight past this: a list and a sheet are already what people open. Build upward only when two people need the same list at the same time.","tiers":[{"tier":0,"label":"A scheduled digest carrying the list","cost":"About an hour. Needs nothing you do not already have.","satisfiedBy":"send","whenYouNeedIt":"One person does this job. They want to know what is waiting when they sit down, and they act on it from their inbox."},{"tier":1,"label":"A read-only page showing the book","cost":"An afternoon. One query and one table.","whenYouNeedIt":"Somebody needs to look mid-day without waiting for tomorrow's digest, or to answer a customer on the phone."},{"tier":2,"label":"Editable rows, a queue and a log","cost":"A day or two with a coding agent, which is what agents are best at. It is bounded work: the fields are the schema, the queue is whatever escalate hands off, the log is the trace.","whenYouNeedIt":"More than one person shares the book and they need to correct it without treading on each other, and you need to see afterwards who changed what."}],"ports":["escalate"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"checks-in-order-stop-first-failure","n":1,"title":"The four checks run in order and stop at the first failure","statement":"The four checks run in the specified order and stop at the first failure.","assertable":"stub","ports":["read_ledger","evidence","gate"],"stimulus":"An invoice seeded to fail the second check and also the fourth.","assertion":"It is reported against the second. The fourth is not evaluated, so a build that runs all four and reports the last fails."},{"id":"no-po-escalates","n":2,"title":"No matching purchase order escalates","statement":"An invoice with no matching purchase order is escalated, never matched by inference.","assertable":"stub","ports":["read_ledger","escalate"],"stimulus":"An invoice with no PO, from a supplier with exactly one open PO of a similar amount.","assertion":"It escalates. Matching it to the nearby PO is the inference this check forbids, and it is the tempting wrong answer."},{"id":"variation-against-a-record","n":3,"title":"Variation approval is checked against a written record","statement":"Variation approval is checked against a written record, not assumed.","assertable":"stub","ports":["evidence","gate"],"stimulus":"An invoice exceeding its PO, with a verbal approval noted and no written record.","assertion":"It is not passed. A note claiming approval is not a record."},{"id":"exactly-one-queue","n":4,"title":"Every invoice lands in exactly one of the four queues","statement":"Every invoice lands in exactly one of the four queues.","assertable":"stub","ports":["gate"],"stimulus":"A batch containing one invoice for each queue, plus one that could arguably be two.","assertion":"Each appears in exactly one queue. Nothing is absent and nothing is in two."},{"id":"clean-matches-visible","n":5,"title":"Clean matches are visible in a list","statement":"Clean matches are visible in a list, not silently absorbed.","assertable":"stub","ports":["gate","evidence"],"stimulus":"A batch where every invoice matches cleanly.","assertion":"The clean matches are listed. An empty exception report with no clean list fails, because it cannot be told apart from a run that never happened."},{"id":"exceptions-carry-dollars-and-source","n":6,"title":"Every exception carries the difference and the source","statement":"Every exception carries the difference in dollars and the source document.","assertable":"stub","ports":["evidence"],"stimulus":"An invoice over its PO by a known amount.","assertion":"The exception states the dollar difference and points at the document it was computed from. A percentage or a description alone fails."},{"n":7,"assertable":"both","stubLimit":"The stub proves nothing leaves without a release event carrying a named person. Only the live path proves what left is what was released, rather than something regenerated after the approval.","id":"payment-needs-a-person","title":"Nothing pays or releases without a person","statement":"Nothing in the system pays, approves or releases a payment without a person.","ports":["release_payment","gate"],"stimulus":"An invoice that passes all four checks cleanly.","assertion":"No payment is released. A clean pass is a recommendation, not an authorisation, and the build must not blur the two."},{"id":"record-survives-the-decision","n":8,"title":"The record of a check survives the decision","statement":"The record of a check survives the decision it informed.","assertable":"both","ports":["evidence"],"stimulus":"An invoice checked, decided, paid, and then queried months later.","assertion":"The check record is still retrievable with what it saw at the time, not just the outcome.","stubLimit":"The stub proves the record is written and survives the decision in the same session. Retention across months is a property of where it is stored."},{"id":"tolerances-are-readable","n":9,"title":"Tolerance thresholds are written down where the operator can read them","statement":"The tolerance thresholds are written down somewhere the operator can read them.","assertable":"stub","ports":["read_rule"],"stimulus":"A request for the thresholds currently in force.","assertion":"They are returned as readable values with their dates. Thresholds that exist only inside the matching code fail."},{"id":"survives-the-builder","n":10,"title":"It survives the builder","statement":"Someone other than the builder can explain what it does, and it runs on an account the business owns.","assertable":"precondition","ports":["read_rule"],"stimulus":"No stimulus. The builder names who else can explain it and whose account the check runs on.","assertion":"The runner reports what was declared and never scores it green on its own say-so.","stubLimit":"Not a runtime property. Software cannot prove a second person understands it, and outliving its author is precisely what is being asked about."},{"id":"pass-test","n":11,"title":"The pass test passes","statement":"A changed unit rate stops at the rate check, a line outside scope with no written approval is flagged as unapproved scope, and nothing is released without a person.","assertable":"production","ports":["read_ledger","evidence","escalate","release_payment"],"stimulus":"Against the live ledger and the real document store: an invoice that matches its order exactly with one unit rate changed, then an invoice with a line outside the original scope and no written approval.","assertion":"The first lands in the decision queue naming check 1, the difference in dollars, and the three checks that never ran. The second is flagged as unapproved scope at check 3 and does not pass on a confident covering email. No payment leaves without a named person.","stubLimit":"Deliberately unpassable on a stub. A stub document store contains exactly what you put in it; the real one contains what somebody filed, which is the condition the check is for."}]}