Order Confirmation on the Existing System

Superseded on 10 September 2026. IMP is being built on the greenfield design at /order-confirmation/. This page is kept as the record of the alternative that was designed and not pursued.

The build that takes IMP live first. It delivers the same desk as the greenfield order-confirmation design, with the same rules and the same experience for IMP and its customers, on the system that runs in production today. The greenfield remains the target; what follows works out, from the problem and the constraints, what the interim build has to be.

1. The problem

IMP buys printed tin-can sheets from one supplier, RIC in Turkey, and sells the finished cans to six customers. The sheets carry each customer's artwork. IMP therefore cannot order sheets for an order until the customer has confirmed the artwork on every line of it.

Today one person does this by hand: find the current design for each line, send the set to the customer, chase for a reply, file whatever comes back, work out sheet quantities from can quantities, draft the supplier purchase order. About two hours per order, repeated on every order because customers change artwork between orders. A wrong design on a print run costs around $10,000, and it has happened with both IMP and the customer having confirmed the wrong version.

Sari, the decision maker at IMP, gave one hard requirement. When a customer sends him a new version of a design, he forwards it, and he needs to be certain that this version goes out for confirmation and the old one does not. He cannot tell versions apart by eye, he will not remember which is current, and he does not want to compare files or read version lists. Ask him only when the system is genuinely unsure.

Two facts about how IMP works decide much of what follows. Almost all of Sari's traffic, orders and design files alike, arrives on WhatsApp. And every order is reconfirmed, even when the same artwork was approved last week, because the cost of a wrong run is higher than the cost of asking again.

2. Constraints

These are the facts that decide the design, as distinct from preferences.

3. What the system has to do

Derived from the problem, before any choice of mechanism.

  1. Know that an order has arrived, with its lines, from the message it arrived in.
  2. Know, per line, which artwork version the customer is being asked to confirm.
  3. Put that version in front of the right person at the customer, and keep asking until each line has an answer.
  4. Record each answer with evidence that would settle a dispute a year later: who said it, in which message, in which words.
  5. Accept a new design file from Sari and make it the current version without his involvement, unless the system cannot tell which brand it belongs to.
  6. Convert confirmed can quantities to sheets, covers and bottoms, show the working, and draft the supplier order for a person to approve.
  7. Do all of the above per customer, since each customer has its own contacts, patience, and rules about what counts as a yes.

Requirement 4 rules out any design where confirmation is a status flag. Requirement 5 rules out any design where Sari picks a version. Requirement 7 rules out a graph per customer.

4. Decisions

For each decision: the options considered, and why one won.

Scope first. Two scopes were possible: the full desk of the prototype, or the smallest thing that chases a confirmation, which knows an order came in, sends the brand files for its lines, and asks until they confirm. The full desk is built. The reasoning is Sari's own: order confirmation is one link in a chain he wants to run without employees, and a link that still needs a person to convert quantities and draft the supplier order saves one of the two hours. The supplier order is also where his "Caramel branding" mistake happened, and requirement 6 exists because of it. What makes the full scope affordable is the build order in section 10, where each piece ships alone.

4.1 Where the loop runs

The loop is a long-running conversation with timers: send, wait, remind, read, repeat. Three ways to run it on the existing system.

An Engine B graph plus Go handlers for the steps no node can do. The graph has the nodes a chase needs: a trigger, request_approval, wait_for_event, delay, send_email, notify. New Go code is limited to reading and deciding. Costs: the seven-day sweep must be raised for this graph; a graph is data, so it is tested by replay rather than by unit test.

A pure Go handler with its own state rows and the existing cron for timers, the way approval timeouts already work. Cleaner to port to Temporal and independent of a frozen engine. Cost: it rebuilds waiting, reminders and reply matching that the interpreter already has, so it is the slowest to a live order.

A hybrid, Go owning the state machine and the graph only sending and collecting approvals. Cost: two owners of one loop, which is how the 26 August incident happened, where the pipeline wrote bills the workflow was supposed to own.

The graph wins on time to a live order, and the cost is acceptable because the graph is thin: sequencing and waiting only. Every step that reads or decides is a Go handler with a typed result, which is the part that ports to a Temporal activity unchanged.

4.2 How an order enters

The prototype had a Start button because nothing upstream existed. In production the order is a message: the customer's purchase order as a PDF or a photo, on Sari's WhatsApp, forwarded to Aradus the way he will forward artwork. Nobody re-types it. The existing pipeline classifies it as a purchase order and order entry creates the sales order with a confidence per line.

A misread quantity that becomes a pack, then a sheet count, then a supplier order is the one mistake the desk must not let through unreviewed. So a gate exists between order creation and the chase. It is a per-customer threshold rather than a step every order takes: when every line clears it the run starts on its own; otherwise the order waits in Needs you with the weak lines highlighted and one tap starts it. A customer who sends clean PDFs should not pay for one who sends photographs.

Until the production WhatsApp number exists, forwarding goes to a connected mailbox or an upload. The handler is the same; only the door changes.

4.3 The unit of confirmation is the line

Confirmation could be recorded per order (one yes), per brand (this design is approved), or per line (this version, on this order, for this quantity). Per order fails requirement 4 when the customer confirms three lines and rejects one. Per brand fails Sari's second fact: every order is reconfirmed, so an approval from last week does not carry. Per line survives both a partial reply and a per-order reconfirmation, and it is what the tracker shows.

4.4 Replacement by agreement of three signals

Requirement 5 is the hard one. A file arrives with no order attached and a caption at best. Which brand does it belong to, and is it new?

One signal is not enough. The bytes alone say only whether this exact file has been seen. The text in the artwork says which brand it is for, but a re-export of the same design carries the same text, and two brands from one customer can share most of their text. Visual similarity to the current version says whether the design changed, but a customer's two flavours can look alike. The origin, whose WhatsApp number or mailbox it came from, narrows the customer but not the brand.

So the rule is agreement. Same bytes as the current version: a duplicate, no version, a one-line "already current" back to Sari. Otherwise read the brand name out of the artwork, compute a perceptual fingerprint and compare it with the current version of each candidate brand for that customer, and note the origin. When all three point at one brand, it becomes the new current version and Sari gets a receipt naming what replaced what. When they disagree, or the file is near-identical to the current version so that "new version or the same file?" is a fair question, Sari gets a one-tap ask listing the candidates. That is what "only ask when genuinely unsure" means in mechanism.

Whether the rule may act alone is a per-customer setting, because IMP's answer is one customer's answer. A customer with a compliance team wants every replacement approved.

4.5 One mechanism for asking a person

The desk asks a person in three places: a line that matches no brand, a replacement the rule is unsure about, and the supplier PO review. The existing order_approvals table already carries a proposal, an AI confidence, an assignee, a decision and an expiry, with kinds for branding confirmation and file replacement. Inventing a second ask store would fork the record of decisions across two tables. All three asks are rows here.

4.6 Rules are data, per customer

Who to send to, who to escalate to, how often to remind, what a marked-up screenshot counts for, whether the rule may replace on its own: each is a customer's answer, and each is something a person will change after an intervention. They are stored as one versioned row per customer, read by the graph at run time. That is what lets one graph serve six customers and the next tenant. Learned mappings, "Ramadan Edition Gold at this customer means this brand," go to entity_aliases, which the matcher already reads.

5. Architecture

flowchart TB
    M[forwarded message<br/>mail, upload; WhatsApp when the number exists] --> P[document pipeline<br/>classify, order entry]
    P -- purchase order --> O[sales order created]
    P -- artwork file --> R[replacement handler<br/>three-signal rule]
    O --> G[Engine B graph<br/>one run per order]
    G --> MT[match_brands]
    MT -- unmatched line --> A1[order_approvals ask]
    MT --> PK[send_branding_pack]
    PK --> W[wait_for_event<br/>reply or link click]
    W -- delay --> RM[reminder, escalation]
    W --> RD[read_signoff_reply]
    RD -- rejected --> N[needs you]
    RD -- all confirmed --> CV[convert_bom]
    CV --> PO[draft_supplier_po]
    PO --> A2[request_approval<br/>PO review]
    A2 --> S[send from IMP's address]
    R --> V[new brandings version<br/>or one-tap ask]
    UI[Command Center desk<br/>queue, tracker, rulebook, PO review] -.reads and decides.-> G

Three parts, one owner each.

The graph owns sequencing and waiting. One run per order, started by the order-created event or the operator's tap. It calls the matcher, sends the pack, parks on wait_for_event, fires reminders on delay, escalates after the rulebook's cap, and hands converted quantities to request_approval. One template per tenant, reading the rulebook at run time. The stale-run sweep gets a per-graph maximum of 60 days for this template.

Go handlers own reading and deciding. Five new node types, each a deterministic function or a model call returning a typed result with a confidence:

node does returns
match_brands matches each order line to the customer's current brand versions: aliases first, then name similarity per line: brand version or none, confidence, candidates
send_branding_pack one email to the sign-off contact with a preview and a tokenized link per line the dispatch id; lines moved to sent
read_signoff_reply reads a reply or a link click into per-line verdicts per line: confirmed, rejected with instruction, deferred, unclear, with the quoted words
convert_bom turns confirmed cans into body sheets, covers and bottoms through bill_of_materials quantities per component with the working, frozen
draft_supplier_po drafts the purchase order, rounded to the supplier's minimum a purchase orders row in draft

A sixth handler, the replacement rule of 4.4, runs outside any order on an artwork file arriving through the pipeline.

The desk is Next.js pages in the Command Center: the queue split Needs you, With customer, Supplier PO, Sent; the order view with the per-line tracker, Remind now and record-by-phone; the rulebook editor; the PO review with Approve and send. Pages read the rows the graph writes and act through the approvals API. They do not read run state, so the runtime can change underneath them.

6. Data

Each change traces to a requirement in section 3.

order_brandings becomes per line (requirements 2, 3, 4). Today it is keyed by order and brand with a status. It gains order_line_item_id, a pin to the exact brandings version row, a line state (matched, needs_you, sent, reminded, confirmed, rejected, deferred, superseded), reminder_count, last_reminded_at, sent_at, and signoff_evidence: the inbox message id and the quoted words, or "by phone, recorded by Layla at 14:02". One open row per line, enforced by a partial unique index.

brandings gains what the rule needs (requirement 5). content_hash, phash, identity_text, source and source_message_id, is_current, and decided_by (the rule, or the person who answered the ask). Versions stay one row each; a replacement is a new row and the old one loses is_current.

order_approvals is reused (decision 4.5) with two kinds added, branding_match and supplier_po_review, beside the two that exist. proposal holds candidates and confidence; decision holds the answer and a save-as-rule flag.

entity_aliases is reused (decision 4.6) for learned line-name-to-brand mappings.

branding_rulebooks is new (requirement 7). One versioned row per tenant and customer: sign-off and escalation contacts, reminder interval and cap, deferred handling, whether a marked-up screenshot counts, archive days, replacement mode, match threshold, order-entry threshold for auto-start, and whether open lines are reconfirmed when their brand gets a new version. For IMP that last one is yes.

Supplier PO (requirement 6). A purchase orders row with source_order_id pointing at the sales order, each line carrying the frozen BOM working as JSON so the review can show how 3,893 body sheets came to be.

7. Flows

Intake. A forwarded purchase order lands through a mailbox, an upload, or later the WhatsApp number. The pipeline classifies it, order entry extracts the lines with a confidence each, and the sales order is created with the customer resolved from the forwarded header or the sender. The order-created event starts the run if the lines clear the customer's threshold; otherwise the order waits in Needs you.

Matching. match_brands runs over every line. Above the threshold the line is pinned to the brand's current version. Below it, or with no candidate, an ask opens with three choices: new brand and ask the customer for the file, map to an existing brand, or pause the order. The answer can be saved as an alias. Nothing is sent while a line is unresolved.

Pack. One email to the sign-off contact: a preview per line and one link per line, each link carrying a token for order, line and pinned version, so a click is a per-line verdict with nothing to read. Lines move to sent. delay fires at the reminder interval; each reminder re-sends only the unconfirmed lines and increments the count; past the cap the escalation contact is added and Sari is told.

Replies. A reply or a click raises the event the run is parked on. read_signoff_reply returns per-line verdicts with the words that decide each. Confirmed pins the line with evidence. Rejected with an instruction moves the line to Needs you and tells Sari's designer; when the new file arrives it goes through replacement and only that line is re-sent. Deferred follows the rulebook. Unclear opens an ask. A confirmation relayed by phone is recorded from the tracker with who said it and when.

Replacement. As in 4.4, at any time. When a version changes and the rulebook says reconfirm, open lines pinned to the old version are re-sent with the new one.

Convert and PO. When every line is confirmed, convert_bom produces components with the working per line, draft_supplier_po rounds to the minimum order quantity and drafts the purchase order, and a request_approval opens the review: Approve and send, or Change an item. Approve sends the PO from IMP's own address, posts the purchase to the sales order, and logs the receipt.

8. Failure modes and invariants

9. Tests and evals

Every model-facing piece has a golden set before it ships, in the two-layer shape the repository already uses: deterministic replay in CI, live-model accuracy on demand.

10. Build order

Each step is usable on its own and the next does not undo it.

  1. Replacement handler. Sari's hard requirement, and it works with no order in the system. Tuned on the 45 files.
  2. Intake and matching, tracker read-only. Forwarded POs become orders; lines are matched and pinned; the desk shows the tracker and sends nothing.
  3. Pack, reminders, reply reading. The chase goes live by email.
  4. Convert and PO review. The supplier order closes the loop.
  5. Rulebook editor and queue views. Until then the rulebook is a row an engineer edits.

WhatsApp attaches to steps 1 and 3 when the production number exists. Nothing waits on it.

11. The path to the greenfield

The data here was chosen so that the move is a column mapping. order_brandings per line is line_signoffs. brandings with is_current and the hashes is brands plus brand_versions flattened into one table. branding_rulebooks is a policies row. Each order_approvals row is a proposal and its decision in one row. Each Go handler is an activity with the same input and output; the graph's sequence becomes OrderConfirmationWorkflow and the replacement handler becomes ArtworkReplacementWorkflow. What does not move is the graph itself, which is the part with the least logic in it.

12. Inputs still needed

The 45 artwork files are in hand, copied from the production bucket.