# Duplicate invoice checks before approval

This runnable example compares two rules on 16 deliberately chosen synthetic scenarios. It starts with structured invoice fields: extraction from a PDF, OCR and language models are outside this example. Supplier IDs and invoices are invented. No client records are included.

The result is a review label with the records that caused it. The script cannot approve, reject or pay an invoice. `NO_MATCH` still requires a person's review.

## Run the example

Download all five files into one directory: `run.py`, `input.json`, `expected.json`, `results.json` and this `README.md`. Python 3.9 or later is sufficient. No packages, accounts, network access or API keys are needed.

From that directory:

```sh
python3 run.py --check
python3 run.py
```

The first command checks that the generated JSON is byte-for-byte identical to `results.json`. The second prints that report. The expected check output is:

```text
Results match results.json (16 synthetic scenarios).
```

To save a separate copy:

```sh
python3 run.py --output my-results.json
```

To try your own synthetic scenarios, copy and edit both the input and expected-label files, then run:

```sh
python3 run.py --input my-input.json --expected my-expected.json
```

Assign expected labels before looking at the runner's answers. Changing only the labels to make a result pass does not validate a rule. The supplied expectations document the example's policy; they are not an independent benchmark.

## What the fields mean

An invoice record has exactly these five fields:

```json
{
  "id": "incoming-1",
  "supplier_id": "SUP-001",
  "invoice_number": "INV-1042",
  "amount": "120.00",
  "currency": "USD"
}
```

- `id` identifies one supplied record. It must be unique across the ledger and batch within its scenario.
- `supplier_id` is an already resolved, stable supplier identifier. It is compared exactly, including case. Names, email addresses and bank accounts are not used to infer identity.
- `invoice_number` supports ASCII letters, digits, spaces and hyphens, up to 64 characters, with at least one letter or digit.
- `amount` is a positive decimal string with at most two decimal places, up to `999999999.99`. Commas, exponent notation, numeric JSON values, zero and negative values need review in this example. Amounts are compared as decimal values, so `120.0` and `120.00` are equal.
- `currency` is exactly `USD`, `EUR` or `GBP`. This small set is an example boundary, not a list of all currencies. No conversion is performed.

Both identifiers accept 1–64 ASCII letters, digits, underscores or hyphens, beginning with a letter or digit. Unsupported fields and formats need review instead of being silently discarded.

Each independent scenario contains `ledger` and `batch` arrays. The last batch record is the target. The comparison includes the supplied ledger and every earlier batch record. Results preserve this distinction as `scope: "ledger"` or `scope: "batch"`. The script has no memory across scenarios and never adds a record to a ledger.

## The two rules

Both versions validate every supplied invoice first. Missing, invalid or unsupported data results in `NEEDS_REVIEW`. So does a repeated record ID. Invalid reference data takes priority over reporting no match, even if that reference belongs to another supplier.

The **baseline** compares the exact supplier ID and literal invoice number. For a matching pair, changed amount or currency requires review; equal amounts and currencies produce a possible duplicate.

The **candidate** additionally removes ASCII spaces and hyphens and folds invoice-number letter case. Thus `INV-1042`, ` INV 1042 ` and `inv-1042` become possible matches for the same supplier. This assumption needs checking against a supplier's numbering rules before use on real data. Distinct legitimate numbers can collapse to the same comparison key.

Leading zeros are not removed from the comparison key. A separate ambiguity check spots numbers such as `INV-001042` and `INV-1042` and returns `NEEDS_REVIEW`. It does not declare them equivalent. `INV-001043` remains different from `INV-1042`.

If multiple references match, a conflicting amount or currency takes priority over an otherwise matching record. Any leading-zero ambiguity also requires review. The report includes all matching references so that the reason can be inspected.

| Label | Meaning | Next step |
| --- | --- | --- |
| `POSSIBLE_DUPLICATE` | Supplier, compared number, amount and currency match a supplied reference. | Review the source documents and reference records. |
| `NEEDS_REVIEW` | Data is invalid, input is repeated, amounts or currencies conflict, or numbering is ambiguous. | Resolve the listed issue before proceeding. |
| `NO_MATCH` | This rule found no reference within the supplied context. | Continue the normal review; absence of a match is not approval. |

## Reproduced results

| Rule | Fixture expectations matched | Needs review | Possible duplicate | No match |
| --- | ---: | ---: | ---: | ---: |
| Literal-number baseline | 12 of 16 | 7 | 3 | 6 |
| Normalized-number candidate | 16 of 16 | 9 | 4 | 3 |

These are agreements with manually assigned labels on a small, deliberately selected set. They do not estimate real-world accuracy, missed-duplicate rates, financial savings or processing speed. There is no separate held-out test set.

Four scenarios explain the difference:

1. Spaces replace a hyphen: the baseline returns `NO_MATCH`; the candidate finds a possible duplicate.
2. Letter case changes: the baseline returns `NO_MATCH`; the candidate finds a possible duplicate.
3. Extra leading zeros: the baseline returns `NO_MATCH`; the candidate identifies an ambiguity for review.
4. One literal match and one formatted match disagree on amount: the baseline reports a possible duplicate; the candidate exposes the conflicting reference.

Other scenarios check different suppliers, changed amount or currency, missing invoice number, invalid amount, a previous batch copy, a new number, repeated input IDs, invalid reference data, a distinct number containing leading zeros and unsupported currency.

## Where this example stops

- It assumes the extracted fields and resolved supplier ID can be inspected against their source. It does not measure extraction quality.
- It only sees the supplied context. An incomplete ledger, missing earlier batch or incorrect supplier ID can hide a duplicate.
- It does not distinguish invoice-number reuse across dates, entities or accounting periods. These fields are not present; legitimate reuse may be flagged.
- It does not perform fuzzy matching, supplier-name resolution, exchange-rate conversion or document comparisons. Different numbering errors can still be missed.
- Credit notes, zero-value documents, unlisted currencies and non-ASCII invoice formats are outside the example's supported inputs.
- It is an in-memory teaching example capped at 100 scenarios and 100 records per scenario. It has no persistence, concurrent-import handling, authentication, approval workflow or payment connection.

Before adapting the rule, define the relevant supplier-number policies and reference scope, collect representative labelled records, separate rule development from evaluation, and review both missed duplicates and unnecessary review flags. The right next action depends on the surrounding process; it is not encoded as a financial action here.

## Reproduction and test interface

`run.py` exposes `generate_results(dataset, expected)`, a pure function accepting the two parsed JSON documents and returning a report. It does not mutate either input. `serialize(result)` produces the stable JSON text used by `--check`.

`results.json` includes the target, full supplied context, expected label, both decisions, reasons and matching records for every scenario. Its `policy.actions` explicitly keeps human review required and all automatic approval, rejection and payment disabled.

In the website repository, the regression command is:

```sh
node --test tests/invoice-demo.test.mjs
```

The eight tests cover deterministic reproduction, all 16 explicit expectations and four baseline misses, reference scopes, conflict priority, leading zeros, 32 invalid target variants plus invalid/repeated context, human-review-only outcomes, stale-result detection and mismatched fixture labels. The test file uses Node's built-in test runner and invokes this Python script; the downloaded example itself needs only Python.
