Skip to content

Matching a Purchase Order to Its Amendments

9 min read · updated August 11, 2026

Nobody sends you a diff. An amended purchase order arrives as a complete document with a revision number on it, and finding what actually changed between revision 2 and revision 3 is your problem, not the buyer’s.

An amendment is a full reprint

Because the amendment is generated fresh by the buyer’s system, fields change that carry no commercial meaning at all. Compare two revisions naively and the following will differ on almost every pair: the print date and time in the header or footer, the document identifier or barcode, page counts and page-break positions, the buyer’s contact name if staff changed, recalculated totals, sequence numbers, and any “printed by” annotation. Line ordering also shifts when a line is cancelled and the reprint closes the gap.

The result is that a text or field diff reports a dozen changes, of which zero or one matter. Teams respond by raising a threshold or by only alerting on totals, and both lose the case that motivated the work: a delivery date that moved by three weeks on one line, with the total unchanged.

So the pipeline has to compute a semantic delta over a normalised representation, and it needs to know which fields are commercially meaningful before it starts. That list is short and worth writing down explicitly: per line, the item code, description, quantity, unit of measure, unit price, delivery date and delivery location; per header, the supplier, currency, payment terms, delivery terms and the ship-to address. Everything else is presentation.

Key on the line number, never on position

The single most consequential decision is what identifies a line across revisions. Position in the table is the obvious choice and it is wrong: delete line 2 and every subsequent line shifts up, so a positional diff reports that lines 2 through 40 all changed.

Purchase orders almost always carry an explicit line number — often in tens (10, 20, 30) precisely so that lines can be inserted later without renumbering. That number is the key. Where a document genuinely has none, construct a stable key from the item code plus a sequence within that code, and record that you did so.

// keyed comparison, not positional
const byKey = (lines: POLine[]) =>
  new Map(lines.map((l) => [l.lineNo ?? `${l.itemCode}#${l.seq}`, l]));

const prev = byKey(rev2.lines);
const next = byKey(rev3.lines);

const added    = [...next.keys()].filter((k) => !prev.has(k));
const removed  = [...prev.keys()].filter((k) => !next.has(k));
const common   = [...next.keys()].filter((k) => prev.has(k));

Watch for the cancellation convention. Many systems do not remove a cancelled line; they reprint it with a quantity of zero, or with a deletion indicator, or with the quantity struck through. A cancelled line that still appears is not “unchanged” and it is not “removed” either — it is a quantity change to zero, and treating it as a removal loses the audit trail of why the goods are no longer coming.

Computing the delta that matters

Normalise before comparing, or the delta fills with noise. Amounts to integer minor units with their currency; dates to ISO from whatever the document printed, which is its own hazard when the format is ambiguous between day-first and month-first and wants a date-field validation rule of its own; quantities with their unit of measure, since 1 CTN and 12 EA may be the same thing; descriptions whitespace-collapsed and case-folded for comparison while keeping the original.

Then classify each change rather than reporting it as a string pair, so that downstream systems can act on the class:

{
  "po_number": "4500123456",
  "from_revision": 2,
  "to_revision": 3,
  "header_changes": [
    { "field": "delivery_terms", "from": "FCA Rotterdam",
      "to": "DAP Manchester", "class": "commercial" }
  ],
  "line_changes": [
    { "line_no": 20, "field": "delivery_date",
      "from": "2026-03-04", "to": "2026-03-25", "class": "schedule" },
    { "line_no": 40, "field": "quantity",
      "from": 120, "to": 0, "class": "cancellation" }
  ],
  "unchanged_lines": [10, 30],
  "reprint_only_fields": ["printed_at", "page_count", "buyer_contact"]
}

Emitting unchanged_lines explicitly is worth the bytes. It is the difference between “we found no change on line 10” and “we did not look at line 10”, and when an amendment is later disputed that distinction is the whole question.

The delivery-terms change in that example is not a cosmetic one: moving from FCA to DAP moves the cost and risk of carriage from the buyer to the seller, which changes whether freight should appear on the resulting invoice at all. That is why the term is on the commercial list — see extracting delivery terms from a purchase order.

Revisions arrive out of order

Documents arrive by email, portal download and EDI, and none of those guarantees order. Revision 3 turning up before revision 2 is routine; so is receiving revision 2 twice because a mailbox was reprocessed.

Order by the revision number on the document, never by receipt time. Then three rules make the pipeline safe:

  • Monotonicity. Applying a revision lower than or equal to the current state is a no-op that is logged, not an update. This is what makes reprocessing an inbox harmless.
  • Gap handling. If revision 3 arrives and you hold revision 1, you can still apply it — the amendment is a full state, not a patch — but the delta you compute is 1 to 3 and you should record that revision 2 was never seen. A delta that silently spans a missing revision looks like a single large change.
  • Idempotency. The same document processed twice must produce the same state and no second downstream event. Key the processing record on PO number plus revision, not on the file.

Where the document carries no revision number at all — some amendments only say “AMENDED” with a date — fall back to the document date and treat equal dates as requiring review. Do not invent an ordering from receipt time and then act on it.

Applying only what moved

The point of all of this is downstream restraint. A goods receipt already booked against line 10 should not be disturbed because line 40 was cancelled. A supplier acknowledgement should not be re-requested because the buyer’s contact name changed. A price change on one line should reprice open commitments on that line only.

So emit per-field events rather than a single “PO updated” event, and let each consumer subscribe to the classes it cares about: schedule changes to planning, quantity and cancellation to warehouse and commitments, price to finance, delivery terms to logistics. The reprint-only fields go to the audit record and nowhere else.

Store every revision as received, alongside the computed delta and the extraction that produced it — the field-level audit trail is the deliverable here, not a by-product. Amendment disputes are settled by producing the document, and an extraction pipeline that keeps only the latest state cannot answer the question that will eventually be asked. Matching the resulting invoice back to the right revision is the other half of this problem, covered in extracting purchase order numbers from invoice headers.