Lessons · Lesson 2 of 5
The queue, and the key that repeats
How a document is born, what the external key guarantees, and the two rules in the mapper that cannot fire today.
Lesson 2 of 5 · 24 min
A handover is not a message, it is a record
When this app tells another system something, it does not send a message and forget. It writes a row. It gives that row a name. It keeps the row afterwards.
The name is the whole design. The name is worked out from the record rather than invented, so the same handover always produces the same name. That is what lets somebody press the button twice without fear. This lesson is about what the name is made of, what it protects, and one thing it does not protect at all.
Vlad has an approved purchase request for the canvas on STY-352. Alina has just confirmed ORD-1362. Neither of them did anything about the ledger, and the app has already written two rows.
Seven document types, and the two you will see
The app defines seven outbound document types. That is the full vocabulary. Which of them apply to you falls out of the records your workspace actually creates — a factory that never subcontracts never produces a service request.
| Type | Label on the queue | Produced today from |
|---|---|---|
| Purchase request | Purchase request | An approved purchase request |
| Sales order | Sales order | An order that is not Draft and not closed |
| Goods received | Goods received | Nothing yet |
| Material issue | Material issue | Nothing yet |
| Dispatch | Dispatch | Nothing yet |
| Commission | Commission | Nothing yet |
| Service request | Service request | Nothing yet |
The last column is the honest one. Two of the seven can be produced today, and the reason is a single function that gathers what is exportable. It reads approved purchase requests, and it reads orders. It reads nothing else.
The physical-event documents — a receipt, an issue, a dispatch — have their type, their label and their place in the file order. They have no source of records behind them. The commission document has a rule of its own as well. It is only planned when the workspace is a buying agent and the order carries a commission percentage, and the export action passes a hard-coded answer of no.
Two moments of birth, one name
A document row is created at either of two moments. This matters, because on screen the two look unrelated.
The first is the business event. When Vlad's purchase request is approved, the approval action queues its purchase-request document. When Alina confirms the order, the confirm action queues its sales order. Neither of them chose to.
Both writes are wrapped so that a failure is swallowed. The comment in the source says the auto-queue must never break the approve or confirm flow, and it does not. A queue failure at this moment is silent by design. Nothing on the order says a document did not get written.
The second moment is the export. Corina presses Export for ERP, and the app gathers everything currently exportable and plans a document for each.
Those two paths would duplicate every document in most systems. Here they cannot, because both compute the same name.
The name is called the external key. It is four parts joined by colons: the record type, the record id, the document type, and a revision. Vlad's request becomes purchase_request:41:po_request:1.
The column that holds it is unique, and the insert is written to do nothing on a collision rather than to fail. So the second write of the same key is not an error and not a duplicate. It is nothing at all.
What a repeat export really does
Here is where a careful reader should slow down. "You can safely export twice" is true of one thing and false of another.
The queue is protected. Export on Monday and again on Tuesday, and the sales order for ORD-1362 is one row, written once, with one status.
The file is not. The export gathers the current exportable records every time, and writes a row into the download for each one, whether or not that document was already sent. Corina's Tuesday file contains ORD-1362 again. Nothing in the app deduplicates the file, and nothing marks a row as previously sent.
That is not a bug, and knowing why is the point of this section. The app cannot know what Mirela's ledger did with Monday's file. It might have been imported, half-imported, or left in a downloads folder. So the app hands over the outstanding set each time, and puts the external key in the first column of every row. That column is the one thing the ledger can dedupe on. The protection is real, but it lives on the other side of the handover, and it only works if whoever loads the file uses that column.
Five statuses, and the three that no code reaches
A queued row carries a status, and the app defines five of them: Pending, Exported, Acknowledged, Linked and Error. The lifecycle they describe is a real one. A document is queued, sent, acknowledged by the receiving system, and finally linked to the number that system gave it.
Only the first two are ever reached. A row is created Pending. The export marks it Exported. Nothing in the application ever writes Acknowledged, Linked or Error. Nothing ever writes the field that holds the other system's returned reference either. The function that would do it exists and takes all the arguments it needs, and its only caller outside the test suite is the export, setting Exported.
This matters for a specific reason. A ledger checks a supplier invoice against the purchase order and the goods receipt before it pays, and that check is called a three-way match. To run it, the ledger needs the app's receipts filed against its own purchase-order number — so the app has to read that number back. The column for it is there. The function to fill it is there. The path that would call it is the live connection, and the live connection is not wired.
The two rules in the mapper that cannot fire
The function that turns a record into documents is small and pure. It carries two rules that come straight from the domain. One of them works. One of them cannot.
The working rule is direction. An order carries a perspective, and the mapper reads it: a maker's order becomes a sales order, a buyer's order becomes a purchase request. Today every call passes maker, because the perspective column is designed and not built, and the app is a factory app. The rule is correct and its input is a constant.
The other rule is the one worth the rest of this lesson. Free-issue material is fabric the buyer supplies at no charge, and it must never produce a purchase request. A purchase request in a ledger becomes a payable. A payable for cloth nobody is buying is a phantom liability that somebody eventually has to unpick. The mapper implements exactly that. If the record says its sourcing model is buyer-supplied, it returns no documents at all.
Three separate facts stop that rule from ever running.
- No caller passes the field. The approval path queues the request without it. The export path gathers the request without it. The field is optional, so both calls are valid, and the value the mapper reads is always absent.
- The word does not match the app's vocabulary. The app's sourcing models are
owned,nominatedandsupplied, and anything it does not recognise is coerced toowned. The mapper compares againstbuyer_supplied, which the app never produces. That string appears in exactly two files: the comparison itself, and the test that hands the comparison its own literal. - The fact lives on the wrong record. Sourcing model is a property of a line in a bill of materials. A purchase request has lines too, and they carry a material, a quantity, a unit and a suggested supplier — and no sourcing model. So even a caller that wanted to pass the value has nowhere to read it from. And a request whose lines are mixed could not be described by one value anyway.
None of this is currently dangerous, and it is worth saying why before anyone panics. The export is a file that a person downloads and a person imports. Mirela reads what she is loading. The rule matters at the moment the live connection lands, because then the same document goes across without a person in the middle.
Check yourselfAlina amends ORD-1362 from 12,000 pieces to 13,500 after it has already been exported once. Trace what the ledger receives on the next export, and name the field that was designed for this case.Show the answer
The next export gathers the order fresh, so the row in the file carries 13,500. The external key is unchanged, because it is built from the record type, the record id, the document type and the revision — and the quantity is not one of those parts. The queue write is a no-op, so the row is still the one written the first time, still marked Exported. So the ledger receives the new quantity under the old key. Whether that is an update or a duplicate is entirely the ledger's decision, and the app has not told it which was intended. The field designed for this is the revision. It is the fourth part of the key, it defaults to one, and nothing in the application ever passes anything else — so an amendment cannot currently announce itself as a new document. The honest reading is that the app has the mechanism for versioned handover and has not yet connected it to the events that would bump it.
Check yourselfVlad asks why the free-issue rule should be fixed now, when the file export has a human reading it. Give the strongest case for leaving it and the strongest case for fixing it.Show the answer
The case for leaving it is that a rule with no caller has no behaviour, so there is nothing to break. The fix is also not one change but three — a field on the purchase-request line, a value passed at both call sites, and a vocabulary the two halves agree on. Doing that badly is worse than not doing it, because a half-wired suppression that fires on the wrong lines would silently stop legitimate requests from reaching the ledger. The case for fixing it is that the danger arrives with the live connection rather than gradually, and the code currently reads as though the protection is present. A future reader sees a named rule and a passing test, and has no reason to check whether the value ever arrives. That is the more dangerous state — not an absent guard, but an absent guard that looks like a present one.
Prompt · Read my ERP document queue back to me
After the first export, and any time the finance team says a document arrived twice or never arrived at all.
Help me read the outbound document queue in MerchandiserOS and work out what my finance system has actually received. I will paste the queue rows: document type, record, status. I will also tell you how many times we have pressed Export for ERP and whether each downloaded file was imported. First, explain what the queue can and cannot tell me. A row's status is what THIS app did, not what the other system did. Say plainly which statuses this app ever writes and which it does not. Treat any status I claim beyond that as something to question. Then work through the duplicate risk. The external key is stable per record and document type, so re-exporting is a no-op in the queue and NOT a no-op in the file. Tell me which of my rows would appear again in a second export file, and what my finance system would have to do with that column to avoid creating a second document. Finally, list any record I have amended since it was first exported, and explain what the other system receives for it. Never tell me a document was received. This app cannot know that.
AI can make mistakes — check anything you act on.