hamsa software
← All articles
AI tooling8 min read

When an invoice extractor borrows the wrong date

An extractor finds a valid date on an invoice and assigns it to the wrong field. The mistake survives a format check; following the cited line explains what the next check needs to establish.

Four shutter-like geometric segments surround an empty centre, suggesting distinct evidence states around an unresolved invoice field.

A document with one awkward date

The invoice date is perfectly readable. The due date is not. When an extractor copies the first into both fields, the result can pass an ordinary date check and arrive in the accounts system looking reassuringly complete. Nothing is wrong with the date’s format. It has simply answered the wrong question, and the tidy response has made that mistake harder for the next person to spot.

That is the detail I would follow before looking at an extraction confidence score. A person reading the original has a reason to stop at the unreadable line. A system required to fill every field can remove the visible reason for stopping without learning anything more about the deadline. The workflow needs a way to carry that unresolved question forward alongside the amounts it can actually support.

The five-line invoice below is an invented example, kept small enough to trace every proposed answer back to its source. There is no purchase-order reference. The [unreadable] marker stands for a region we cannot read, rather than words printed on a real invoice. We can use this document to examine the extraction rules; it tells us nothing about how accurately a particular model or OCR engine reads real invoices.

We want four fields: total, tax, purchase-order reference and due date. The raw tax phrase is GBP 0.00; the normalised amount is the decimal string 0.00. The total becomes 120.00 GBP. The purchase-order reference has not been found in this source version. The due-date region exists but supplies no readable value. Those last two facts are different, even though both prevent us producing a value.

text
L1  Invoice: INV-DEMO-017
L2  Invoice date: 14/06/2025
L3  Due date: [unreadable]
L4  Tax: GBP 0.00
L5  Total due: GBP 120.00

Null is not a particularly small amount

JSON null and an absent property are different things; JSON Schema documents that distinction explicitly. Zero is different again. [1] If a field disappears from an extraction response, the caller cannot tell whether it was overlooked, unsupported or genuinely not found. Requiring a field result with an explicit state makes that ambiguity visible. It does not require pretending that every field has a value.

For this example, the contract uses present, not_stated, unreadable and conflict. A present result carries raw text, a normalised value and evidence. A not_stated result has a null value and says that the field was not found in the supplied source. unreadable records a located region whose value remains unresolved. conflict preserves competing candidates rather than choosing whichever looks more convenient.

Keep the state and value separate. The explicit tax zero is present with value 0.00. The missing purchase-order reference is not_stated with value null. Turning every missing value into zero collapses these meanings, as does treating any false-looking value as missing. The distinction needs to survive the programming-language checks so the accounts colleague can tell an explicit zero from an amount that has never been established.

not_stated also has a modest meaning. It describes the material examined, not every page that might exist elsewhere. An incomplete upload or failed text extraction can hide a reference. Record the document and text-extraction versions, and make missing-page checks part of intake. Otherwise the system can confidently report absence from a source it has not received in full.

Four states preserve different evidence

The synthetic invoice separates an explicit zero from absence, an unreadable region and conflicting candidates.

Scroll sideways to see all columns.

Field conditionStateValue / evidence
Tax says GBP 0.00present0.00; retain the stated text
No purchase-order reference foundnot_statednull; absence is limited to the supplied source
Due-date region unreadableunreadablenull; preserve the region for review
Competing supported candidatesconflictnull; retain both candidates
These states belong to the extraction contract. They do not decide whether an invoice is genuine or payable.

Give the value an address

JSON Schema can require properties, restrict their types and reject unexpected structure. [2] In this contract, present requires non-empty raw text, a non-empty value and at least one evidence reference. The other states require null in value. These rules reject a contradictory result such as not_stated paired with value 0. They cannot establish that the proposed value is supported by the invoice.

For our text fixture, evidence points to a line in a stored source version. A production document may need page coordinates, a text span or both. Keep the original beside the extracted text: a correct quotation of faulty OCR is still a faulty reading of the image. A source identifier should take the reviewer to the exact material used, not whichever file currently happens to share its name.

Look at the proposed due date: 14 June, supported by L2. Checking that the quotation exists gets us nowhere, because it really is there. L2 says Invoice date. The requested field is Due date. Evidence must support the requested field, not merely contain the requested-looking characters. A reviewer who can see the field and cited line together can understand the rejection without having to distrust every other value in the response.

Even the supported amounts only tell us what this invoice says. A supplier could state the wrong total, submit the same invoice twice or send a document that is not genuine. Arithmetic, duplicate and supplier checks still have work to do. I would keep that division visible: a source reference is useful because it answers a precise question, and becomes less useful when the interface lets it imply that the whole payment is approved.

json
{
  "field": "due_date",
  "sourceVersion": "demo-017@v1",
  "result": {
    "state": "present",
    "raw": "14/06/2025",
    "value": "2025-06-14",
    "evidence": [
      "L2"
    ]
  }
}

Follow the wrong date through the checks

The small checker used for this example has three stages. First, it checks the result contract: permitted state, required members and compatible value types. Second, it resolves the evidence line and checks the expected field label and exact raw phrase. Third, it recomputes the normalised value from the supported raw text. Each stage can reject the proposal for a different reason.

Our copied date passes the first stage. It fails the second because the cited line starts with Invoice date, while this field requires Due date. Normalising 14/06/2025 to 2025-06-14 leaves that mismatch unchanged: the date is valid, but the source assigns it a different role. Keep this rejection reason available to the reviewer so they can inspect the right region rather than investigating an unspecified extraction failure.

For total and tax, the toy checker accepts the known GBP prefix and parses the remaining text using Decimal constructed from a string. Python documents why that avoids importing a binary floating-point approximation into the decimal value. [3] This fixture permits exactly two decimal places; it is a deliberately narrow rule for these two amounts, not a universal currency parser or an accounting rounding policy.

The recomputed tax is 0.00, so a candidate claiming 120.00 from the same raw phrase fails normalisation. An invented raw total of GBP 125.00 fails the earlier exact-evidence check. These are useful distinctions: one proposal misinterprets real evidence, another invents its support, and the copied due date cites a real phrase with the wrong role. One generic confidence score would obscure all three.

Five proposals, and what their evidence actually supports

An illustrative invoice fixture separates explicit zero from missing and unreadable information, then rejects a date copied from the wrong field.

Scroll sideways to see all columns.

ProposalExact sourceState and valueResult
TotalL5 · Total due: GBP 120.00present · 120.00 GBPAccept candidate; document says this amount
TaxL4 · Tax: GBP 0.00present · 0.00 GBPAccept candidate; explicit zero is retained
Purchase orderNo matching line in demo-017@v1not_stated · nullRetain absence; optional-field policy decides draft eligibility
Due dateL3 · Due date: [unreadable]unreadable · nullKeep unresolved; do not schedule from a guess
Guessed due dateL2 · Invoice date: 14/06/2025present · 2025-06-14Reject evidence: real date, wrong field
These are original synthetic inputs and deterministic decisions, not model-performance measurements. Candidate acceptance does not certify arithmetic, supplier identity or document authenticity.

Resolve the question without rewriting its history

At this point, accept the supported total and tax as candidates in a draft. Leave the purchase-order reference explicitly not stated. Keep the due date unresolved and block any automatic step that requires a confirmed deadline. Whether an optional purchase-order reference may remain empty is a business rule to agree with the accounts team, rather than a decision for the extractor to improvise.

The reviewer should see the due-date region, the rejected candidate and the reason for rejection together. If they obtain a clearer original, create a new document version and a revised field result linked to it. If they establish the deadline through another authorised source, record that source and resolution separately. Do not alter the old raw phrase until it appears to justify the corrected answer.

Reprocessing makes that record useful again. A correction to this invoice’s due date need not be rediscovered each time the same version is opened. But another invoice with a familiar layout has not inherited the answer. Reuse the resolution only while its document identity, field and version still match; otherwise reopen the question. That gives the reviewer’s work a durable place without turning an earlier correction into the extractor’s next unsupported guess.

Try the reviewer’s part

The downloadable checker keeps the successful amounts separate from the copied date and the other rejected proposals. Running them together matters: a repair that rejects every result would stop the wrong deadline, but also discard the supported total and explicit tax zero. The reviewer needs to receive the two unresolved fields without losing the values that can move into a draft. That combination tests the handoff the accounts team will actually use, rather than rewarding rejection alone.

To judge a real extractor, someone needs to label documents across the layouts and failures the business receives, then distinguish accepted wrong answers from cases held for review. Deferring nearly everything can make an error count look excellent. It can also leave the accounts team doing almost all the original work, so the review workload belongs beside the error count.

The fixture is intentionally easier than a real invoice. Exact labels will not handle arbitrary layouts, and OCR can turn a character into another plausible character. A quoted span can be correct but incomplete. A conflicting total may require interpreting a credit note, rather than choosing the larger amount. These cases need further rules or human judgement; the line checker is an explanation of a boundary, not production invoice software.

I would try the reviewer’s part before widening automatic processing: give them several awkward cases, let them correct one, then process it again. Can they reach the original region, understand why the candidate failed and tell whether their correction still applies? In this small invoice, the tax and total can move into a draft while the deadline stays unresolved. The result leaves one visible piece of work for a person, instead of concealing it inside a completed form.

Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.

See the Payment Operations Pilot

References & further reading

Primary sources behind this article. Living documentation may change.

  1. JSON Schema: null is different from an absent property
  2. JSON Schema: object properties, required fields and additional properties
  3. Python: Decimal construction and exact decimal values
Hamsa Jama

Practical engineering. A human perspective.

More about Hamsa Software and the person behind it. Editorial notes.

A few more things
worth a read.

Back to the reading room
AI tooling7 min read

The prompt did not change. The system did.

Version the resolved configuration around an AI response so comparisons, investigations and rollback refer to the system that actually ran.