Procurement lifecycle

This page walks the parts-procurement lifecycle of Rails Sandbox — a Rails-compatible API (a faithful mock of the public Integrations API, plus clearly-labelled proposed extensions). It names every dotted wire method in order, the status each produces, and the exact point where the public 2026-01 contract ends.

Every example below is an RPC-over-POST call:

POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/<dotted.method>
Authorization: Bearer <api_key>
Partly-Integration-ID: <integration_id>
Content-Type: application/json

See Quickstart for credentials and a first call, the API Reference for full request/response shapes, and Errors for the coded failure model.


The two state machines

Two things move through states: parts and procurements. The enum values below are quoted verbatim from the contract — do not invent others.

Part statusPartStatusV1:

"estimated" | "ordered"

A newly inserted part starts "estimated". It transitions to "ordered" once a procurement has been placed for it.

Procurement statusprocurements.v1.buyer.Status:

"order_requested" | "order_confirmed" | "cancelled"

A procurement is created in "order_requested" and is transitioned to "order_confirmed" by the supplier. "order_confirmed" is the terminal public state — see the boundary marker below. ("cancelled" is enumerated in the contract; the sandbox lifecycle does not drive a cancellation path.)

ASCII view of the happy path:

                 ── PUBLIC 2026-01 CONTRACT ──────────────────────────┐
 part: estimated ──(place)──► ordered                                 │
                                                                      │
 procurement:  (create) ──► order_requested ──(supplier confirms)──►  │
                                                       order_confirmed │  ◄── terminal public state
 ──────────────────────────────────────────────────────────────────  ┘
                                                          │
                  ── PROPOSED EXTENSIONS (beyond 2026-01) ┴── (see below)
                                  invoices appear after order_confirmed

Ordered walkthrough — tier1.prepareorder_confirmed

The faithful path uses these dotted wire methods, in workflow order. Read-only resolution helpers (repairer.work-providers.list, plus the job-read methods) are listed where they fit.

1. tier1.prepare — warm the catalogue (no state change)

Queues a vehicle VIN/VRM for future use in other the API (bulk of up to 1000). Fire-and-forget: returns { invalid_identifiers } and makes no state change to any job, part, or procurement.

curl -X POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/tier1.prepare \
  -H "Authorization: Bearer partly_demo_repairer_3f8a1c0d9e2b4a67b1c2" \
  -H "Partly-Integration-ID: 0c000000-0000-4000-8000-000000000001" \
  -H "Content-Type: application/json" \
  -d '{ "identifiers": [{ "vin": "JTDBR32E730012345" }] }'

2. repairer.work-providers.list — resolve the insurer (read-only)

Fetches the list of work providers (insurers) for a repairer so you can attach the right work_provider_id to a job. Empty request body. Omit the work provider entirely for cash / self-pay jobs.

3. repairer.jobs.insert — open or update a repair job

Upserts a repair job (insert-or-update keyed by identity). On success it emits a repairer.jobs webhook whose payload carries change_action of "inserted" or "updated" (see Webhooks). No procurement exists yet.

curl -X POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/repairer.jobs.insert \
  -H "Authorization: Bearer partly_demo_repairer_3f8a1c0d9e2b4a67b1c2" \
  -H "Partly-Integration-ID: 0c000000-0000-4000-8000-000000000001" \
  -H "Content-Type: application/json" \
  -d '{
    "identity": { "external": "CCC-2026-04817" },
    "repairer_site_id": "0b000000-0000-4000-8000-000000000001",
    "work_provider_id": "0a000000-0000-4000-8000-000000000020"
  }'

4. repairer.jobs.parts.insert — identify parts

A full-list upsert of the job's parts, keyed by part identity. Each newly inserted part starts at status: "estimated".

Wire note: repairer.jobs.parts.insert is the one method whose error bodies are bare JSON strings ("not_found", "unprocessable", …), not { "type": "..." } objects. This asymmetry is reproduced faithfully from the contract; see Errors.

5. repairer.jobs.baskets.latest.get — recommend a basket (read-only)

Fetches the latest priced basket for the job. Returns:

{ "offers": [], "suppliers": [] }

A job that exists but has no priced parts returns an empty basket, not an error.

Currency: the basket response carries no currency field. OfferV1 exposes trade_price (a bare string, NZD ex-GST) with no accompanying currency, and the basket envelope has only offers and suppliers. Currency is a procurement-level concept — it first appears on procurements.v1.buyer.Procurement.currency_code ("NZD"). This is decision D52: any currency shown alongside a basket offer is invented. The Rails MCP's projected basket may attach a derived presentation-layer currency label, but that is never a wire claim.

6. repairer.procurements.get — track a procurement (read-only)

Buyer/repairer view of one procurement by UUID: its status and lines. A procurement belonging to a different org resolves as not_found (scoped read).

7. supplier.procurements.confirm — confirm the order

Transitions a procurement order_requested → order_confirmed. This is a supplier-scope action: it requires a credential whose scope is "supplier". On confirm the order is locked, an invoice is created, and buyer + supplier …procurements webhooks fire with status order_confirmed.

The seed ships one procurement already placed in order_requestedPR-1, id 10000000-0000-4000-8000-000000000001 — so you can exercise confirm immediately, using the supplier credential:

curl -X POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/supplier.procurements.confirm \
  -H "Authorization: Bearer partly_demo_supplier_8b4e2f1a6c0d3e9f7a25" \
  -H "Partly-Integration-ID: 0c000000-0000-4000-8000-000000000002" \
  -H "Content-Type: application/json" \
  -d '{ "identity": { "id": "10000000-0000-4000-8000-000000000001" } }'

The supplier and repairer credentials are both preloaded by the seed, so a single caller can drive both sides by swapping the two header pairs. See Quickstart.

Lifecycle summary table

# Dotted wire method Faithful? Resulting status / effect
1 tier1.prepare faithful Catalogue warmup; returns { invalid_identifiers }; no state change
2 repairer.work-providers.list faithful Read-only; lists insurers for the repairer
3 repairer.jobs.insert faithful Upserts job; emits repairer.jobs webhook (change_action)
4 repairer.jobs.parts.insert faithful Full-list upsert of parts; new parts start estimated
5 repairer.jobs.baskets.latest.get faithful Read-only; { offers, suppliers }; empty (not an error) if no basket. No currency field
6 repairer.procurements.get faithful Read-only buyer view by UUID; cross-org → not_found
7 supplier.procurements.confirm faithful order_requested → order_confirmed; locks order; creates invoice; fires webhooks

── End of the public 2026-01 contract: order_confirmed is the terminal state. ──

Everything above is a faithful mirror of the public 2026-01 surface. The public lifecycle terminates at order_confirmed. The methods below are not part of that contract.


Proposed extensions (beyond 2026-01)

These two steps complete the buyer's procurement loop — placing the order and reconciling the resulting invoice — but the public 2026-01 API exposes no public method for either. the upstream platform performs procurement-placement and invoice-reconciliation internally; here they are reconstructed as a clearly-labelled proposed extension so the end-to-end loop is demonstrable, never passed off as the real contract.

Every extension method announces itself: its HTTP response carries x_extension: true, and the Rails MCP result carries is_extension: true. Both extension surfaces open with this verbatim banner:

(Proposed extension beyond the public 2026-01 API — the public contract stops at order_confirmed; this completes the buyer loop.)

repairer.procurements.insert — place a procurement (proposed extension)

(Proposed extension beyond the public 2026-01 API — the public contract stops at order_confirmed; this completes the buyer loop.)

Creates a procurement in status: "order_requested" from a selection of offers, and moves the selected parts estimated → ordered. It emits buyer + supplier …procurements webhooks (status order_requested). This is the step that, in the faithful path, the seed has already performed for PR-1; for a brand-new job you place first, then confirm.

The full proposed-extension workflow order is therefore:

prepare_vehicle → open_repair_job → identify_parts → recommend_basket
   → place_procurement (ext) → confirm_procurement → reconcile_invoice (ext)

Single-confirmable-supplier constraint

The seed has three supplier orgs but one supplier credentialS1, "Christchurch Toyota — Parts", business_id 0b000000-0000-4000-8000-000000000010. A supplier can only confirm its own orders. Because this demo holds only S1's supplier credential, only a procurement placed from S1's offers can ever reach order_confirmed.

Where that constraint bites depends on which surface you use:

See Errors for the exact recovery text.

repairer.procurements.invoices.list — reconcile invoices (proposed extension)

(Proposed extension beyond the public 2026-01 API — the public contract stops at order_confirmed; this completes the buyer loop.)

Reads reconciled invoices and credit notes for a procurement (or job). An invoice is created on confirm with status: "reconciled", document_type: "invoice", and each line reconciliation: "matched". Invoices therefore appear only after order_confirmed — calling this before the order is confirmed returns nothing to reconcile.


Re-reading state — never assume, re-fetch

The lifecycle is event-driven and webhooks are identifier-only (payloads carry ids, not records), so after any transition you re-fetch the current truth by id:

After you confirm, call repairer.procurements.get for the new truth (order_confirmed), and repairer.jobs.parts.list to see the ordered parts flipped to ordered.


See also