Business validation
- Lesson 14
- 50 min
- Module 5: Validate and review results
Lesson 14 of 19 · Document and Multimodal AIBusiness validation
1. Understand document inputs
2. Prepare and extract content
3. Recover structure and tables
4. Use multimodal models deliberately
5. Validate and review results
6. Evaluate and deliver the pipeline
7. Case studies
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 ready
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.pyreads 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 -qprints84 passed. If your project is broken, copy the snapshotend-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.
Get the lesson’s file
Section titled “Get the lesson’s file”-
In the project folder, copy the module and its test from the snapshot:
Terminal window S=../nextia-learning-labs/C13/snapshots/end-of-m05cp $S/doc_extract/validate.py doc_extract/cp $S/tests/test_validate.py tests/Terminal window $S = "..\nextia-learning-labs\C13\snapshots\end-of-m05"Copy-Item "$S\doc_extract\validate.py" doc_extract\Copy-Item "$S\tests\test_validate.py" tests\ -
Run the tests:
Terminal window python -m pytest -qThe last line is
90 passed. The tests read a few photos with OCR, so the first run can take about 30 seconds.
Check the bill before you pay
Section titled “Check the bill before you pay”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:
python -m doc_extract.combine D050D050 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 okEvery 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.”
The idea: rules that the values must obey
Section titled “The idea: rules that the values must obey”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:
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 datefrom doc_extract import validatereceipt = {"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 datefrom doc_extract import validatereceipt = {"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 CADerror order_number format | '0693-220' is not LK- and 6 digitserror payment_method allowed | 'Cheque' is not a known payment methodRead 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 |
Validate the course’s receipts
Section titled “Validate the course’s receipts”-
Validate the receipt from the problem:
Terminal window python -m doc_extract.validate D050D050 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.
-
Validate the scan D051:
Terminal window python -m doc_extract.validate D051D051 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.
-
Validate a normal receipt and a US receipt:
Terminal window python -m doc_extract.validate D011python -m doc_extract.validate D052D011 receipt-lk-103119.pdf: no problemD052 order-us-lk-159455.pdf:warning currency foreign paid in US dollars: convert before a refund in CAD -
Validate every receipt document:
Terminal window python -m doc_extract.validate --allThe 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 1error date required 2error subtotal required 1error tax required 1error total adds_up 2error total required 2error vendor required 2warning currency foreign 2warning items adds_up 3warning items line 18documents 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']

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:
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.85The 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.
The same receipt twice
Section titled “The same receipt twice”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, validaterecords = {}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.80D021 Larkfield LK-373435 2026-07-09 28.69D060 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.
What validation can and cannot catch
Section titled “What validation can and cannot catch”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 formatv1 D007 wrong: order_number | errors: order_number formatv1 D013 wrong: order_number | errors: order_number formatv1 D017 wrong: order_number | errors: order_number formatv1 D018 wrong: order_number | errors: order_number formatv1 D021 wrong: date | errors: -v1 D025 wrong: order_number | errors: order_number formatv1 D028 wrong: order_number | errors: order_number formatv1 D033 wrong: order_number | errors: order_number formatv1 D034 wrong: order_number | errors: order_number formatv1 D036 wrong: date | errors: -v1 D042 wrong: order_number | errors: order_number formatv1 D046 wrong: vendor, order_number, date, currency, subtotal, tax, total, payment_method | errors: currency required, date required, subtotal required, tax required, total required, vendor requiredv1 D048 wrong: order_number | errors: order_number formatv1 D049 wrong: order_number | errors: order_number format== v1 full: 54 documents, 15 with a wrong value, 13 of them with a validation errorv2 D009 wrong: vendor | errors: -v2 D045 wrong: tax | errors: tax required, total requiredv2 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 errorv2-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 requiredv2-quarter D038 wrong: tax | errors: tax requiredv2-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 errorWith 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.
When to validate, and what it costs
Section titled “When to validate, and what it costs”- Validate every extracted record, after
combine.pyand 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.
Your turn: two cut-off photos
Section titled “Your turn: two cut-off photos”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:
python -m doc_extract.validate D045python -m doc_extract.validate D047Show the answer
D045 IMG_4880.jpg: error total required total is missingD047 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.
Knowledge check
Section titled “Knowledge check”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.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
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.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
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.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
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.