Build record. Decided by Matthew 2026-09-29; built and deployed by OpenCode the same day
(feature/pharmacy-session-20260929@085f7ba, migrationpharmacy_stock.0010applied to the client
tenant by the deploy's own new step); verified live by Hermes 2026-09-29 — the evidence is in §4.
This is the page for the shape of the files the pharmacy app exchanges with a wholesaler, in both directions.
It exists because the shape is now settled enough to be a contract rather than a discussion: the import side
(price files) and the export side (order files) both have a defined form, and a file that does not match it is
refused by name rather than guessed at.
Related: the Supplier Portal Interaction
scoping note and its stages; Supply Orders — Build Spec
and the pricing phases; Price File Import — Drag and Drop;
the archived research pages CSV Format (a convention page, not a spec),
Supplier CSV Files and
Selenium Gadget.
An order file is a flat two-column grid answering two questions for the supplier's warehouse system: which item
(the code column) and how many outer packs (the quantity column). Comma separated — never tabs or semicolons.
One pack size per row: the same drug in two pack sizes is two rows, because they carry different codes. The
quantity is always whole outer commercial packs, never the individual pill count. No header row, and no supplier
id — the file is uploaded into the portal, so the supplier and the pharmacy are known from the login. The
out-of-the-box default is the "Standard UK Wholesale Export".
The price-file side keeps the general anatomy he confirmed at the same time (and which the app already honoured): a
product code (PIP is the identifier all mainlines and short-liners cross-reference) kept as text so leading zeros
survive, a description, a pack size, and a clean decimal price; the column mapping is per supplier, confirmed by a
human and recorded, never invented. No single UK layout is mandatory, so the mapping screen is the mechanism, not a
fixed template.
A header row is the exception, not the rule. The order profile says so in its own words, and a header is added
only for a specific wholesaler whose upload screen has actually been seen.
085f7ba)GENERIC profile keeps its key (it is stored on Supplier.export_profile, soheaders: []; the export view writes a header row only when a profile carries one, so an order with N linesfilename_for() and the portal signal are untouched.code_field = (pip_code, barcode), so a PIP code is used whereparse_price treats a comma as a thousands separator only where it1,234.56 → 1234.56, 1,234,567.89 → 1234567.89); any other comma shape (1,20, 0,89,3,50) is refused with "the price uses a decimal comma, which is not supported". This matters because the; as a delimiter, so a semicolon-delimited EU-style export is a file we invite.PIP_CODE,PIP Code and PIPCode all reach the same hint; the exact comparison stays as a fallback. Hints only suggest.SupplierPriceFile records has_header plus the four positions (migration 0010), thepharmacy_stock/sample_price_files/ with a README.md carrying every count.Four synthetic files, replayed through the real importer without writing anything, on test-client-pharmacy:
| File | Shape | Headered read | Declared headerless |
|---|---|---|---|
synthetic-portal-export-five-column.csv |
comma, 5 columns | 7 rows, matched 3 / unmatched 1 / rejected 3 | 8 rows |
synthetic-renamed-headers-decoys.tsv |
TAB, renamed headers, a List Price decoy |
4 rows, matched 2 / unmatched 2 | 5 rows |
synthetic-no-header-positional.csv |
comma, no header row | 2 rows — the first line eaten as the header | 3 rows |
synthetic-semicolon-decimal-comma.csv |
semicolon, decimal commas | 3 rows, all "matched", every price ×100 | 4 rows |
Two findings, and two corrections to earlier records. (a) A headerless file was not refused as an earlier
note of mine claimed: read_file took the first non-blank line as the header, so the first data line was consumed
and silently lost — the "No header row was found in the file" error fired only for a file with no non-blank
line at all. That claim is corrected on the resume note and in the housekeeping
log. (b) The earlier rule "no headerless order sample may ever be created as an app output" is reversed:
headerless is the standard default. The three-column order variant stays unwarranted.
DEMO-OTC-BARCODE-0002,1 (Synthetic Wholesaler Two) and 2345678,32 (Synthetic Wholesaler Three) — one line per<supplier slug>-order-<reference>.csvsynthetic-wholesaler-two-demo-order-ORD-20260929-1336FF.csv). Both drafts were cancelled again with a reason.1,20 / 0,89 / 3,50 → refused with the named decimal-comma sentence;1,234.56, 1,234,567.89, 12,345, £1.20 → accepted; 0.00 → price must be positive; blank andPIP_CODE, DESCRIPTION, PACK_SIZE, TRADE_PRICE all pre-select; the renamed-header fileDescription and Pack only, leaving Trade Cost the operator's deliberate choice overList Price — which is the point of the screen.Column 1…), the first line shown as Row 1 data, and the warning;0010 is applied on the client tenant and migrate_client --slug test-client-pharmacy --plan reports "No planned migration operations"; scripts/entrypoint.sh now callsmigrate_client_tenants for the tenants of a sector the app serves.DEMO-OTC-IBU-200's cached on-hand is 20.00 against ascripts/run_pharmacy_tests.sh,verify-pharmacy-price-ordering.py 26/26,verify-pharmacy-price-headerless-live.py 12/12, verify-pharmacy-order-value.py 47/50 (§5 explains thedev was behind the deployed revision and is now level with it. The app host was serving the feature branchorigin/dev still sat on the previous supply-ordering commit, so a later deploy from dev would havedev to 085f7ba (a clean fast-forward: dev was andev = deployed = 085f7ba.verify-pharmacy-order-value.py (47/50) are exactly the three checks for that sentence. Fix:ProductCode,Quantity"). Mine, one page at a time.