Skip to content

Business validation

  • Lesson 14
  • 50 min
  • Module 5: Validate and review results

What you will be able to do

Check the values that the pipeline found against the rules of the business. A date must be real, and money has two decimals. The total equals the subtotal plus the tax, and an order number has Larkfield’s format. A receipt that breaks a rule goes to a person. So a total that does not add up never reaches an agent unchecked.

Before you startWhat to know and to have readyAbout 50 minutes

What you need to know

  • The extraction contract: its fields, formats and allowed reasons for an absent value, from Define the extraction contract.
  • How combine.py reads a receipt with the rules first and the model when needed, from Combine methods.
  • Checks on the meaning of a model’s answer, from Validate meaning in Building Reliable Applications with LLM APIs, if you took it. Here the same idea checks values read from a page.

What you need on your computer

  • Your project at the end of the previous module: python -m pytest -q prints 84 passed. If your project is broken, copy the snapshot end-of-m04, as the snapshots README says.
  • The labs repository in ~/projects/nextia-learning-labs, up to date.

Tested with Python 3.12.13 and RapidOCR 3.10.0 on macOS (Apple silicon). The Windows and Linux commands are not tested. Every document in this course is made by the course’s generator; no real customer or shop is in it.

  1. In the project folder, copy the module and its test from the snapshot:

    Terminal window
    S=../nextia-learning-labs/C13/snapshots/end-of-m05
    cp $S/doc_extract/validate.py doc_extract/
    cp $S/tests/test_validate.py tests/
  2. Run the tests:

    Terminal window
    python -m pytest -q

    The last line is 90 passed. The tests read a few photos with OCR, so the first run can take about 30 seconds.

A waiter brings the bill after a dinner. You read it correctly: every number you see is on the paper. Then you add the lines in your head, and the sum is ten dollars less than the total. You made no reading mistake. The bill itself is wrong. So you do not pay it yet. You ask a person to look.

You made two different checks. The first was “Did I read the paper correctly?” The second was “Does what I read make sense?” Module 4 was about the first check. This lesson is about the second one.

The problem: every value right, the receipt wrong

Section titled “The problem: every value right, the receipt wrong”

The receipt D050 is a digital PDF from Larkfield’s till. The pipeline reads it with the text layer and the rules:

Terminal window
python -m doc_extract.combine D050
D050 receipt-lk-762786.pdf: model not needed
vendor Larkfield from rules found ok
order_number LK-762786 from rules found ok
date 2026-09-26 from rules found ok
currency CAD from rules found ok
subtotal 449.00 from rules found ok
shipping (not_found) from rules absent ok
tax 58.37 from rules found ok
total 517.37 from rules found ok
payment_method Debit from rules found ok

Every field is ok: the pipeline read exactly what the receipt prints. (The shipping is absent as not_found: the rules’ reason when no line matches, as in Provenance representation. A till receipt prints no shipping line.) The course’s truth agrees, because the truth records what the paper shows. Now add the lines: 449.00 + 58.37 = 507.37. But the receipt prints TOTAL CAD 517.37, and the customer paid 517.37 by debit card.

So a measure of the reading gives 9 of 9 fields right, and the money is still wrong by ten dollars. Maybe the till added a line that it did not print. Maybe somebody changed the PDF before they sent it. The pipeline cannot know which. It can know only that these numbers do not fit together. If the receipt goes straight to an agent, the agent may refund 517.37 for a barbecue that cost 507.37 with tax.

Tomás is not worried: “The values are right. The reader did its job.” Grace answers: “My team does not refund values. It refunds receipts. A receipt that does not add up needs a person.”

Business validation checks the extracted values against the rules of the business. It does not look at the page. The page says what is printed. The business says what can be true. A Canadian sales tax has a few known rates. An order number is LK- and six digits. A purchase is not in the future. A total is the subtotal plus the shipping plus the tax.

A check that fails gives a problem with a severity:

  • An error means “do not trust this receipt without a person”. The document goes to review. A missing required field, a total that does not add up and an order number in the wrong format are errors.
  • A warning means “show this to the person who handles the receipt”. The document can still go through. A payment in US dollars is a warning: the agent must convert it before a refund in Canadian dollars.

The checks do not care which reader found a value. A total from the text layer, from OCR or from the model meets the same rules. This is why validation comes after combine.py, as one step in the life of a document:

A state machine. A file arrives on a ticket: Received. Intake fails: Stopped at intake (damaged, locked, too large, not supported, exact copy). Intake passes: Extracted (rules first, the model when needed). The checks run: Validated (dates, money, totals, codes). An error, a conflict or an unverified value, 5 of 54 receipts: To review. No error, 49 of 54: Certainty measured. Certainty under the threshold, 6 at 0.90: To review. At or over the threshold, 43 at 0.90: To the agents. From To review: the reviewer accepts the values, to the agents; the reviewer adds a correction, Corrected (a new row, the extracted row kept), then the checks run again and pass, to the agents; the page does not show the value, Waiting for the customer, then the customer sends a better picture and the file is Received again.
The life of a receipt in the pipeline. Validation decides the first branch: a receipt with an error goes to a person at any threshold. The numbers are the course's 54 receipt documents.
Text description of the diagram

A file arrives on a ticket. Intake stops damaged, locked, too large and unsupported files, and exact copies. The other files are extracted, rules first and the model when needed. Then the checks run. A receipt with an error, a conflict or an unverified value goes to review: 5 of the 54 receipt documents. The other 49 get a certainty, which the next lesson explains. Under the threshold of 0.90, a receipt goes to review (6). At or over it, the receipt goes to the agents (43). In review, a person accepts the values or adds a correction. When the page does not show a value, the person asks the customer for a better picture. A corrected receipt is checked again before it goes to the agents.

This lesson builds the step “Validated”. The next lesson builds the certainty and the review decision. The last lesson of the module builds the corrections.

A tiny example: one receipt, typed by hand

Section titled “A tiny example: one receipt, typed by hand”

You can check values without a document. Write the values of a receipt as a dictionary, with a total that is ten dollars too high. Start python in the project folder and type:

from datetime import date
from doc_extract import validate
receipt = {"vendor": "Larkfield", "order_number": "LK-547562", "date": "2026-08-22",
"currency": "CAD", "subtotal": "189.00", "shipping": None, "tax": "22.68",
"total": "221.68", "payment_method": "Cash"}
for p in validate.validate(receipt, today=date(2026, 10, 10)):
print(p.severity, p.field, p.rule, "|", p.message)
error total adds_up | subtotal 189.00 + shipping 0 + tax 22.68 = 211.68, but the total is 221.68 (difference +10.00)

The check computes 189.00 + 0 + 22.68 = 211.68 and compares the sum with the total. The difference is +10.00. That is more than one cent, so it is an error. The one-cent margin exists because shops round each tax line, and a sum of rounded lines can differ by a cent.

The today= argument fixes the date of “today”, so you get the same output as the course. Now break four other rules at once:

from datetime import date
from doc_extract import validate
receipt = {"vendor": "Larkfield", "order_number": "0693-220", "date": "2026-12-01",
"currency": "USD", "subtotal": "189.00", "shipping": None, "tax": "22.68",
"total": "211.68", "payment_method": "Cheque"}
for p in validate.validate(receipt, today=date(2026, 10, 10)):
print(p.severity, p.field, p.rule, "|", p.message)
error date future | the date 2026-12-01 is after today (2026-10-10)
warning currency foreign | paid in US dollars: convert before a refund in CAD
error order_number format | '0693-220' is not LK- and 6 digits
error payment_method allowed | 'Cheque' is not a known payment method

Read each line. The date 1 December 2026 is after “today”, and nobody buys something in the future. 0693-220 is a till’s transaction number, not an order number. A cheque is not one of the five payment methods of the contract. The US dollars give only a warning, because a Larkfield order from the United States is normal: it needs care, not a stop. The total now adds up, so there is no total error.

Here are all the checks of validate.py. Each one comes from the contract or from how Larkfield works:

Check Rule Severity
required vendor, date, currency, subtotal, tax and total are present error
future, format (date) a real date, not after today error
too_old (date) a date before 2020 is unusual warning
format (money) an amount, not negative, with 2 decimals error
adds_up (total) subtotal + shipping + tax = total, within 0.01 error
rate (tax) for CAD: tax ÷ (subtotal + shipping) is 0, 5, 12, 13, 14.975 or 15% warning
allowed, foreign (currency) CAD or USD; USD needs a conversion error; warning for USD
format (order number) LK- and 6 digits error
allowed (payment) Visa, Mastercard, Amex, Debit or Cash error
adds_up, line (items) the item lines add up to the subtotal; quantity × unit price = line total warning
  1. Validate the receipt from the problem:

    Terminal window
    python -m doc_extract.validate D050
    D050 receipt-lk-762786.pdf:
    error total adds_up subtotal 449.00 + shipping 0 + tax 58.37 = 507.37, but the total is 517.37 (difference +10.00)

    One error, with the sum written out, so a person sees the reason at once.

  2. Validate the scan D051:

    Terminal window
    python -m doc_extract.validate D051
    D051 scan0099.jpg:
    error total adds_up subtotal 528.00 + shipping 0 + tax 68.64 = 596.64, but the total is 595.74 (difference -0.90)

    This total is 0.90 too low. OCR read the scan correctly: the course made this receipt wrong on purpose, as it made D050. A small difference is still an error, because only a person can decide if 0.90 matters.

  3. Validate a normal receipt and a US receipt:

    Terminal window
    python -m doc_extract.validate D011
    python -m doc_extract.validate D052
    D011 receipt-lk-103119.pdf: no problem
    D052 order-us-lk-159455.pdf:
    warning currency foreign paid in US dollars: convert before a refund in CAD
  4. Validate every receipt document:

    Terminal window
    python -m doc_extract.validate --all

    The command reads every scan and photo, so the first run takes about 30 seconds. It prints only the documents with a problem. The course counted the problems in its output:

    error currency required 1
    error date required 2
    error subtotal required 1
    error tax required 1
    error total adds_up 2
    error total required 2
    error vendor required 2
    warning currency foreign 2
    warning items adds_up 3
    warning items line 18
    documents with an error: ['D045', 'D046', 'D047', 'D050', 'D051']
    documents with only warnings: ['D001', 'D003', 'D005', 'D007', 'D009', 'D013', 'D015', 'D022', 'D027', 'D028', 'D031', 'D036', 'D037', 'D039', 'D040', 'D043', 'D052', 'D053', 'D058']
Two receipts with boxes drawn from the pipeline's evidence. Left, D050, a digital PDF: blue boxes around Subtotal 449.00 and HST 13% 58.37, a red box around TOTAL CAD 517.37; under it, the line Debit ****3310 517.37. Below: subtotal 449.00 + shipping 0 + tax 58.37 = 507.37; printed total 517.37; difference +10.00: error total adds_up. Right, D051, a scan: blue boxes around Subtotal 528.00 and HST 13% 68.64, a red box around TOTAL CAD 595.74. Below: 528.00 + 0 + 68.64 = 596.64; printed total 595.74; difference -0.90. Every value is read right. The receipt itself does not add up.
The two receipts of the course whose printed total does not add up. The boxes come from the evidence of each value, so a reviewer sees the three lines that disagree.
Text description of the diagram

Two receipts, with the boxes that the pipeline stored as evidence. On the digital PDF D050, blue boxes mark the subtotal 449.00 and the tax 58.37, and a red box marks the total 517.37. The sum is 507.37, so the difference is +10.00. On the scan D051, the subtotal 528.00 and the tax 68.64 make 596.64, but the printed total is 595.74: a difference of -0.90. In both receipts, every value is read right, and the receipt itself does not add up.

Five documents have an error, and each one needs a person:

  • D050 and D051: totals that do not add up.
  • D045: the total is missing, because the bottom of the photo is cut off.
  • D046: six required fields are missing, because nobody can read the photo.
  • D047: the vendor and the date are missing, because the top of the photo is cut off.

Nineteen documents have only warnings. Two are the US receipts. Three are photos where OCR found fewer item lines than the receipt has, so the lines add up to less than the subtotal. The other warnings say, for example, 3 x 74.85 is not 74.85 (Pruning shears) on D001. Look at the receipt’s text layer:

Terminal window
python -m doc_extract.textlayer D001
[ 14.2 226.8 89.8 235.8] Pruning shears
[ 25.0 239.3 73.6 248.3] 3 x 24.95
[ 185.6 239.3 212.6 248.3] 74.85

The lines Pruning shears, 3 x 24.95 and 74.85 are there. So the receipt is right, and the warning is right too: the rules read the unit price as 74.85 instead of 24.95. The pattern for a quantity line allows spaces inside an amount, for French amounts such as 1 011,78. So it takes the price and the line total together, and keeps the last number. Every item with a quantity of 2 or more on a till receipt gets this wrong unit price.

This is a real bug in the course’s rules. The course found it with this check, while this lesson was written. A warning is not only about the customer’s paper: it can also point at your own reader. Here the bug changes none of the nine fields that review and the agents use, and a warning does not stop a receipt. You fix such a bug in the reader. You never fix it by removing the check.

Some checks need more than one document. A customer may attach the same receipt to two tickets, or scan it twice. Intake stops an exact copy by its bytes, but a second scan has other bytes. And Input handling showed that pictures of different receipts can look almost the same. So the project compares the content: the same order number, or the same shop, date and total.

validate --all looks only at the 54 receipt documents. D060, a second scan of D020, is in the set as a problem file, and the whole pipeline checks it in the next module. You can run the check by hand now:

from doc_extract import combine, documents, validate
records = {}
for doc in ("D020", "D021", "D060"):
ex, _ = combine.combine(documents.get(doc))
records[doc] = validate.values_of(ex)
v = records[doc]
print(doc, v["vendor"], v["order_number"], v["date"], v["total"])
print(validate.duplicates(records))
D020 Larkfield LK-243824 2026-06-25 416.80
D021 Larkfield LK-373435 2026-07-09 28.69
D060 Larkfield LK-243824 2026-06-25 416.80
{'D060': 'D020'}

D060 has the order number and the total of D020, so it is “the same receipt as D020”. D021 is another receipt. A duplicate is not an error in one receipt: both copies can be perfect. It is a reason for a person to check that nobody pays the refund twice.

Validation does not know the true values. It knows only rules. So it catches a wrong value only when the wrong value breaks a rule. The course checked this on the model’s recorded answers, where the truth is known. For each document with a wrong value, did validation give an error?

v1 D003 wrong: order_number | errors: order_number format
v1 D007 wrong: order_number | errors: order_number format
v1 D013 wrong: order_number | errors: order_number format
v1 D017 wrong: order_number | errors: order_number format
v1 D018 wrong: order_number | errors: order_number format
v1 D021 wrong: date | errors: -
v1 D025 wrong: order_number | errors: order_number format
v1 D028 wrong: order_number | errors: order_number format
v1 D033 wrong: order_number | errors: order_number format
v1 D034 wrong: order_number | errors: order_number format
v1 D036 wrong: date | errors: -
v1 D042 wrong: order_number | errors: order_number format
v1 D046 wrong: vendor, order_number, date, currency, subtotal, tax, total, payment_method | errors: currency required, date required, subtotal required, tax required, total required, vendor required
v1 D048 wrong: order_number | errors: order_number format
v1 D049 wrong: order_number | errors: order_number format
== v1 full: 54 documents, 15 with a wrong value, 13 of them with a validation error
v2 D009 wrong: vendor | errors: -
v2 D045 wrong: tax | errors: tax required, total required
v2 D046 wrong: vendor, order_number, date, currency, subtotal, tax, total, payment_method | errors: currency required, date required, subtotal required, tax required, total required, vendor required
== v2 full: 54 documents, 3 with a wrong value, 2 of them with a validation error
v2-quarter D020 wrong: date | errors: -
v2-quarter D022 wrong: vendor, order_number, date, currency, subtotal, tax, total, payment_method | errors: currency required, date required, subtotal required, tax required, total required, vendor required
v2-quarter D038 wrong: tax | errors: tax required
v2-quarter D040 wrong: order_number | errors: -
v2-quarter D044 wrong: order_number, subtotal, shipping, tax, total | errors: -
== v2 quarter: 6 documents, 5 with a wrong value, 2 of them with a validation error

With the first, plain prompt (v1), the model wrote a wrong order number on 12 documents. Most of them are the till’s transaction number or the partner shop’s invoice number. Every one fails the format check. So validation catches 13 of the 15 documents with a wrong value. The two it misses are dates: a misread French date is still a real date.

With a quarter-size picture, the model misread the subtotal, shipping, tax and total of D044. The four wrong amounts still add up, so no check fails. And D009’s vendor LARKEFIELD breaks no rule, because the vendor list is not a check yet. You add it in the module practice.

So validation is a net with holes of a known shape. It catches values that break a rule: wrong formats, missing fields, sums that do not match. It cannot catch a value that is wrong but plausible. This is why the next lesson adds a second signal.

  • Validate every extracted record, after combine.py and before anyone uses the values. The checks take milliseconds and need no model.
  • Make an error only of what a person must decide. Too many errors fill the review queue with receipts that are fine. A US receipt is normal at Larkfield, so it is a warning.
  • Write every rule down with its reason. Tax rates and order-number formats change with the business. Each change is a code change, with a test.
  • Do not use validation as a measure of accuracy. A receipt with no problem can still have a wrong value that fits the rules.

Common mistake

The total that fixes itself

Symptom: A teammate wants fewer receipts in review: “If the total does not add up, set it to subtotal + shipping + tax. Then the check passes.” The queue gets shorter. Weeks later, a customer complains: they paid 517.37 for the barbecue of D050, and the refund was 507.37.

Cause: The repair changed a value so that a rule passes. The stored total no longer matches the printed line in its evidence box, and nobody looked at the receipt. This is a constructed case, but the numbers are D050’s: the debit line on the paper says 517.37.

Diagnose: Compare each stored value with the text in its evidence. A stored 507.37 with the evidence TOTAL CAD 517.37 is a value that the page does not print. In the project, python -m doc_extract.provenance D050 prints the evidence of each value.

Fix: Never change a value so that a check passes. The validator reports; it does not repair. The receipt goes to a person with the reason. A correction, if one is needed, is a separate record with a name and a note. The last lesson of this module builds it.

Consequence in AI work: a check that repairs data hides the case that it was written to find. The same is true for a model’s output: a validator that “fixes” a wrong answer turns a visible error into an invisible one.

Change one condition: two photos instead of the digital receipt. D045 is cut off at the bottom, and D047 at the top. Predict the problems of each and their severity. Then run:

Terminal window
python -m doc_extract.validate D045
python -m doc_extract.validate D047
Show the answer
D045 IMG_4880.jpg:
error total required total is missing
D047 IMG_4894.jpg:
error vendor required vendor is missing
error date required date is missing
warning items line 2 x 108.00 is not 108.00 (Garden chair)

D045 has one error: the total is missing, because it is below the cut edge. The subtotal and the tax are there, but the “adds up” check cannot run, because it needs all three values. D047 has two errors, the vendor and the date, which were above the cut edge. Both receipts go to a person, who can ask the customer for a new photo. D047 also has a warning about an item line. It comes from the unit-price bug of the rules (2 garden chairs at 54.00), not from the receipt.

Check your understanding

Explain why D050 needs a person, although the pipeline read all nine fields right.

Show the answer

Reading right and making sense are two different checks. The pipeline read exactly what D050 prints, so the reading is correct. But the printed values do not fit together: 449.00 + 0 + 58.37 = 507.37, and the printed total is 517.37. Only a person can find out why: a line that was not printed, a till error, or a changed file. The adds_up check gives an error, so the receipt goes to review, and no agent refunds a wrong amount unchecked.

Check your understanding

Predict: the model reads a receipt and gives subtotal 189.00, tax 24.57 and total 213.57. The paper says 189.00, 22.68 and 211.68. Will validation catch it?

Show the answer

No. The sum still works: 189.00 + 24.57 = 213.57, so adds_up passes. The tax rate is 24.57 ÷ 189.00 = 13.0%, the HST rate, so the rate check passes too. The values are wrong but consistent, as D044’s four amounts were at a quarter size. Validation catches values that break a rule. A plausible wrong value needs another signal, such as a low reading score or a second reader.

Check your understanding

Practical check: run python -m doc_extract.validate --all. Which documents have an error, and why does no warning stop a receipt?

Show the answer

Five documents have an error. D045 has no total, D046 misses six required fields, and D047 misses the vendor and the date. D050 and D051 have totals that do not add up. The warnings are the US currency on D052 and D053, missing item lines on three photos, and the unit-price bug of the rules. A warning tells the person who handles the receipt to take care. It does not stop the receipt, because the receipt can still be right. A US order is normal, and the item lines are not one of the nine fields.

Recap

  • Reading right is not enough. D050’s nine values are all read right, and its total is ten dollars more than the sum.
  • Business validation checks the values against the rules of the business. Examples: required fields, real dates, totals that add up, tax rates, currencies, order numbers.
  • An error sends the document to a person; a warning goes with it. Five of the 54 receipt documents have an error.
  • Duplicates are found by content: D060 has D020’s order number and total.
  • Validation catches wrong values that break a rule, such as all 12 wrong order numbers of prompt v1. It cannot catch a wrong value that fits the rules.
  • A check reports; it never repairs. Here the warnings also found a real bug in the course’s own rules.

Next, you combine the errors with a measured certainty, and decide which documents a person checks.

Next: Confidence and review routing