Evaluation / data specialist · Banking & fintech (external)
A complete transaction search can still select the wrong month
A timezone mutation leaves every request check passing while two transaction rows change sides.
Key takeaways (3)
- A complete result can still contain the wrong transactions.
- Test the boundary row identities as well as counts and totals.
- Use the date basis supplied by the transaction service.
Every request check passes. The search still puts a February transaction into March and leaves a March transaction out. This is the actual output from a temporary timezone mutation of the Northgate Bank fixture:
{"bank_date_membership_check": "FAIL", "count": 24, "facts": {"date_range_confirmed": true, "posting_status_explicit": true, "result_scope_complete": true}, "reason": "verified_fixture_receipt", "request_guard": "succeeded", "total_debits": "$2,603.84", "variant": "utc_date"}
The fixture’s three checks confirm the requested range, posting status, and complete pagination. None independently checks whether the returned rows belong to that range. We changed only the conversion from stored timestamps to dates. The request checks stayed green; the membership check failed.
These are offline Python calls over invented customers and transactions. No model or real bank ran. The mutation is deliberate, not a reported bank defect.
Assert which rows belong, not just that all pages arrived
The customer asks for March 2026 in the bank’s Eastern timezone. The two boundary rows explain the failed membership check:
| Row | Stored UTC timestamp | Bank date | Original search | UTC-date mutation |
|---|---|---|---|---|
| TX-0004 | 1 March, 04:30 | 28 February | Excluded | Included |
| TX-0028 | 1 April, 01:45 | 31 March | Included | Excluded |
A completeness flag answers whether every page of the selected query arrived. It cannot prove that the query selected the right population. Keep an independent row-membership assertion beside that flag. The check below requires TX-0028 and excludes TX-0004; it does not infer correctness from a plausible total.
Use the pinned transaction-search example. Our selected SearchTests, PeriodTests, and GroundingTests receipt contains 20 passing checks without skips. test_dates_are_eastern_time_not_utc tests row membership; test_statements_run_on_billing_cycles tests a separate period distinction.
Save this as /tmp/banking-statement-search-check.py. From the example’s tests directory, run PYTHONPATH=. python3 -B /tmp/banking-statement-search-check.py:
import json
from datetime import datetime
import test_guard as t
h = t.nb
original = h.local_date
for variant in ("bank_timezone", "utc_date"):
h.local_date = original if variant == "bank_timezone" else lambda s: datetime.fromisoformat(s.replace("Z", "+00:00")).date()
try:
_, result = t.search(["checking, March 2026, everything"], "March 2026", "everything", "checking")
accepted, reason = t.lab_outcome(result["facts"], h.load_contract())
membership_ok = "TX-0028" in t.ids(result) and "TX-0004" not in t.ids(result)
print(json.dumps({"variant": variant, "request_guard": accepted, "reason": reason, "facts": result["facts"], "bank_date_membership_check": "PASS" if membership_ok else "FAIL", "count": result["count"], "total_debits": result["total_debits"]}, sort_keys=True))
finally:
h.local_date = original
Actual offline output:
{"bank_date_membership_check": "PASS", "count": 25, "facts": {"date_range_confirmed": true, "posting_status_explicit": true, "result_scope_complete": true}, "reason": "verified_fixture_receipt", "request_guard": "succeeded", "total_debits": "$2,620.88", "variant": "bank_timezone"}
{"bank_date_membership_check": "FAIL", "count": 24, "facts": {"date_range_confirmed": true, "posting_status_explicit": true, "result_scope_complete": true}, "reason": "verified_fixture_receipt", "request_guard": "succeeded", "total_debits": "$2,603.84", "variant": "utc_date"}
The baseline and mutant both return succeeded from the lab’s request guard. Only the membership result distinguishes them. The changed total is a consequence of selecting different rows, not evidence of an arithmetic error. This one boundary assertion is not a general oracle for every possible date range.
Put the date basis inside the filter
The pinned conversion converts an instant into the bank’s timezone. The filter then compares that date:
def local_date(stamp: str) -> date:
return datetime.fromisoformat(stamp.replace("Z", "+00:00")).astimezone(TZ).date()
# Inside _matching, after account, owner and posting-status checks:
d = local_date(t["at"])
if not (query.start <= d <= query.end):
continue
Converting only for display, after filtering, cannot restore a discarded row. Deriving dates from instants requires the service owner to name the timezone and date meaning. A change to either needs new boundary tests, including daylight-saving transitions. Those extensions were not run here.
Ask the service owner whether the contract supplies an instant or a date-only field, and whether it means purchase, posting, or settlement date. Keep independent membership checks whichever contract you choose. This is setup guidance; no date-only adapter was tested here.
:::
Do not substitute a search for a statement
The fixture’s March checking statement covers 6 February through 5 March. Its March transaction search covers 1–31 March. Correct timezone conversion does not make those requests equivalent. Ask which document or range the customer wants, and preserve that scope in the receipt.