The capstone combines a sales CSV with a customer JSON export. It is small enough to inspect by hand, but deliberately contains the errors that make real pipelines misleading: inconsistent whitespace, bad dates, malformed currency, repeated ids, conflicting customer names and sales whose customer is absent. The goal is a useful report plus enough evidence to explain its scope.
The supplied helpers handle record boundaries and strict value parsing. You compose them into a deterministic policy called sales-v1. Successful output is JSON-serializable and preserves source order. It does not fetch files, call an API or establish the factual truth of the sales. The exact same local inputs should produce the exact same decisions and totals on every run.
Separate envelope errors from record errors
A ragged CSV record or duplicate JSON member makes the document ambiguous and
raises ValueError. A well-formed row with an impossible date is different:
append its source position and first error code to sales_errors, then continue.
Use data-record positions excluding the header. A quoted newline does not create
a second data record, so physical line numbers are the wrong provenance here.
Normalize and validate a row before reserving its id. An invalid early sale must not prevent a later valid sale with that id from being kept. Compare repeated valid ids using the complete normalized business payload: amount spellings 2, 2.0 and $2.00 all become 200 cents and can be duplicates. Adding source_row before comparison would make every repetition appear different.
sales = [{'cents': 500, 'customer_name': 'Ada'},
{'cents': 225, 'customer_name': None},
{'cents': -100, 'customer_name': 'Ada'}]
total = sum(row['cents'] for row in sales)
matched = sum(row['cents'] for row in sales if row['customer_name'] is not None)
print(total, matched)The total is 625 cents; matched sales total 400 cents. The unmatched amount is valid evidence even though enrichment failed. Negative sales are refunds under this contract. Observed zero amounts must remain in the rows and accepted count.
Make the report reviewable
Return customer and sale ledgers separately, keep original sale positions in
source_row, and list unmatched source positions. Summary counts must reconcile
accepted and rejected rows for each source independently. Do not use the
post-filtered output index as a source position: after a rejected row, those
numbers diverge. The policy identifier helps a reviewer distinguish future rule
changes from changed source data.
Serialize with json.dumps(report, sort_keys=True, allow_nan=False) when an
external representation is needed. Exact integer cents avoid rounding in totals.
A production tool would additionally identify immutable input bytes, document
sensitive-data handling, version the code and record an output destination. Those
extensions require their own tests; a local fixture is evidence only for the
contracts it exercises.
Before submitting, trace a bad first occurrence followed by a valid repeat, a customer conflict, an unmatched refund and a quoted multiline field. Explain which source row each ledger entry refers to. Then verify both count equations and independently recompute total and matched cents from the returned rows.