Duplicate invoice checks.
Before approval.
A repeated invoice is easy to miss. Here is a small, runnable check that shows what to compare, when to stop, and what to put in front of a reviewer.
What should a duplicate check do?
Compare an incoming invoice with the records already available for the same supplier. Keep the matching references, flag conflicting fields, and give a reviewer enough evidence to decide. A clean comparison alone does not authorize approval or payment.
This example starts after document extraction. It checks structured fields with deterministic rules: supplier ID, invoice number, amount, and currency. There is no language model, OCR, or client data in the runner.
Two rules. The same inputs.
The literal rule compares the supplier ID and invoice number exactly. The normalized rule also ignores ASCII letter case, spaces, and hyphens in the invoice number. Supplier IDs remain exact in both rules.
Both rules compare against a supplied ledger and earlier records in the same batch. They validate the input and flag amount or currency conflicts. The normalized rule preserves leading zeros; a number that matches only after removing them goes to review. Normalization can connect different numbers by mistake, so its matches remain possible duplicates.
- Needs review: incomplete data, conflicting records, or ambiguous numbers.
- Possible duplicate: a reference matches under the selected rule.
- No match: no matching reference in the supplied comparison set. The normal approval process still applies.
What the run produced.
- Literal comparison
- 12/16 expectations matched
- Normalized comparison
- 16/16 expectations matched
Counts show agreement with manually assigned expectations on 16 deliberately chosen synthetic scenarios, not accuracy on real invoices or evidence of savings.
The difference comes from four deliberately included cases: spacing, letter case, a leading-zero ambiguity, and conflicting references. These cases explain the rule change; they do not establish which rule will work best on your documents.
Open a case. Inspect the decision.
Each case is independent. The target is the incoming invoice; ledger and priorBatch contain its reference records. Expand a case to compare both decisions and their explanations.
01Exact copy of a ledger invoice
Expected: Possible duplicate
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
Possible duplicate
The supplier, compared invoice number, amount and currency match a supplied reference. A person must decide whether this is a duplicate.
Matching references: ledger: ledger-1
Normalized rule
Possible duplicate
The supplier, compared invoice number, amount and currency match a supplied reference. A person must decide whether this is a duplicate.
Matching references: ledger: ledger-1
02Spaces replace the invoice-number hyphen
Expected: Possible duplicate
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": " INV 1042 ",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
Normalized rule
Possible duplicate
The supplier, compared invoice number, amount and currency match a supplied reference. A person must decide whether this is a duplicate.
Matching references: ledger: ledger-1
03Invoice-number letters change case
Expected: Possible duplicate
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "inv-1042",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
Normalized rule
Possible duplicate
The supplier, compared invoice number, amount and currency match a supplied reference. A person must decide whether this is a duplicate.
Matching references: ledger: ledger-1
04The same invoice number belongs to another supplier
Expected: No match
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-002",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
Normalized rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
05The invoice number matches but the amount changes
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "125.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
Needs review
At least one matching supplier/number reference has a different amount or currency; inspect the source records.
Matching references: ledger: ledger-1
Normalized rule
Needs review
At least one matching supplier/number reference has a different amount or currency; inspect the source records.
Matching references: ledger: ledger-1
06The invoice number matches but the currency changes
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "EUR"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
Needs review
At least one matching supplier/number reference has a different amount or currency; inspect the source records.
Matching references: ledger: ledger-1
Normalized rule
Needs review
At least one matching supplier/number reference has a different amount or currency; inspect the source records.
Matching references: ledger: ledger-1
07The extracted invoice number is missing
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
Needs review
The target or supplied reference data is incomplete, invalid or outside this example's supported format.
Matching references: None
Input issues: target: missing invoice_number; target: invoice_number must contain ASCII letters/digits, spaces or hyphens, 1–64 characters
Normalized rule
Needs review
The target or supplied reference data is incomplete, invalid or outside this example's supported format.
Matching references: None
Input issues: target: missing invoice_number; target: invoice_number must contain ASCII letters/digits, spaces or hyphens, 1–64 characters
08The amount uses an unsupported decimal separator
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120,00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
Needs review
The target or supplied reference data is incomplete, invalid or outside this example's supported format.
Matching references: None
Input issues: target: amount must be a positive decimal string, at most 999999999.99 and two decimal places
Normalized rule
Needs review
The target or supplied reference data is incomplete, invalid or outside this example's supported format.
Matching references: None
Input issues: target: amount must be a positive decimal string, at most 999999999.99 and two decimal places
09Removing leading zeros would create a match
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-001042",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
Normalized rule
Needs review
A number matches only after removing leading zeros from digit groups. The example does not assume these numbers are equivalent.
Matching references: ledger: ledger-1
10A duplicate is present earlier in the current batch
Expected: Possible duplicate
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
},
"ledger": [],
"priorBatch": [
{
"id": "batch-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
]
}Literal rule
Possible duplicate
The supplier, compared invoice number, amount and currency match a supplied reference. A person must decide whether this is a duplicate.
Matching references: batch: batch-1
Normalized rule
Possible duplicate
The supplier, compared invoice number, amount and currency match a supplied reference. A person must decide whether this is a duplicate.
Matching references: batch: batch-1
11No matching invoice number exists in the supplied context
Expected: No match
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1043",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
Normalized rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
12The same input record ID appears twice
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
},
"ledger": [],
"priorBatch": [
{
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
]
}Literal rule
Needs review
A record ID occurs more than once in the supplied context; check the import before interpreting matches.
Matching references: None
Input issues: repeated id: incoming-1
Normalized rule
Needs review
A record ID occurs more than once in the supplied context; check the import before interpreting matches.
Matching references: None
Input issues: repeated id: incoming-1
13An exact match and a formatted match disagree on amount
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
},
{
"id": "ledger-2",
"supplier_id": "SUP-001",
"invoice_number": "inv 1042",
"amount": "125.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
Possible duplicate
The supplier, compared invoice number, amount and currency match a supplied reference. A person must decide whether this is a duplicate.
Matching references: ledger: ledger-1
Normalized rule
Needs review
At least one matching supplier/number reference has a different amount or currency; inspect the source records.
Matching references: ledger: ledger-1, ledger: ledger-2
14An earlier reference invoice has incomplete data
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
Needs review
The target or supplied reference data is incomplete, invalid or outside this example's supported format.
Matching references: None
Input issues: ledger[0]: missing amount; ledger[0]: amount must be a positive decimal string, at most 999999999.99 and two decimal places
Normalized rule
Needs review
The target or supplied reference data is incomplete, invalid or outside this example's supported format.
Matching references: None
Input issues: ledger[0]: missing amount; ledger[0]: amount must be a positive decimal string, at most 999999999.99 and two decimal places
15Leading zeros are kept on a genuinely different number
Expected: No match
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-001043",
"amount": "120.00",
"currency": "USD"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
Normalized rule
No match
No reference matches within the supplied ledger and earlier batch under this rule. This is not permission to approve or pay.
Matching references: None
16The invoice currency is outside this example
Expected: Needs review
Input and reference records
{
"target": {
"id": "incoming-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "JPY"
},
"ledger": [
{
"id": "ledger-1",
"supplier_id": "SUP-001",
"invoice_number": "INV-1042",
"amount": "120.00",
"currency": "USD"
}
],
"priorBatch": []
}Literal rule
Needs review
The target or supplied reference data is incomplete, invalid or outside this example's supported format.
Matching references: None
Input issues: target: currency must be USD, EUR or GBP in this example
Normalized rule
Needs review
The target or supplied reference data is incomplete, invalid or outside this example's supported format.
Matching references: None
Input issues: target: currency must be USD, EUR or GBP in this example
Run the same check yourself.
Download these five files into one folder. With Python 3.9 or later, run the command below from that folder. It recalculates the results and checks that they match the published report. No packages or network access are needed.
- Python runner ↓
- Synthetic inputs ↓
- Expected decisions ↓
- Generated results ↓
- Instructions and limitations ↓
python3 run.py --checkTo test your own synthetic cases, edit the inputs and their expected decisions, then run python3 run.py --output results.json. Read the instructions before changing the supported input format.
What a real workflow still needs.
This is one check inside an invoice workflow. A production implementation also needs reliable extraction, supplier identity rules, access controls, a complete comparison set, audit records, and an approval owner. The example does not demonstrate concurrent processing, persistent duplicate prevention, or integration with a finance system.
The demo accepts positive decimal amounts in USD, EUR, or GBP only. It does not support credit notes, other currency formats, or arbitrary invoice-number scripts. Test broader conditions against representative documents before deciding how any match should affect the workflow.
We use a worked example like this to make the next project conversation concrete: which inputs matter, which failures to test, what evidence to retain, and who accepts the result.