import 'package:flutter_test/flutter_test.dart'; import 'package:fuel_tax_tracker/services/receipt_parser.dart'; void main() { group('ReceiptParser', () { test('parses a typical gas station receipt', () { const receipt = ''' SHELL OIL 12345 123 MAIN ST PUMP 04 GALLONS 12.345 PRICE/GAL \$3.599 FUEL TOTAL \$44.44 THANK YOU '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 12.345); expect(result.pricePerGallon, 3.599); expect(result.totalCost, 44.44); }); test('parses a tabular receipt where labels and values sit on separate lines', () { // Mirrors a real Casey's receipt: "Pump / Gallons / Price" is a // header row, with the actual values on the next line, and the // total is phrased "Total Sale" rather than a bare "Total". const receipt = ''' Casey's Store #4021 420 W. 6TH STREET KEARNEY, MO 64060 Date 08/08/2026 Time 02:21 VI ########8533 Pump Gallons Price 07 32.115 \$ 3.759 Product Amount 87E10 \$ 120.72 Total Sale \$ 120.72 Auth # Visa Seq # 008299 98 '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 32.115); expect(result.pricePerGallon, 3.759); expect(result.totalCost, 120.72); }); test('does not confuse SUBTOTAL with TOTAL', () { const receipt = ''' SUBTOTAL 10.00 TAX 0.50 TOTAL 10.50 '''; final result = ReceiptParser.parse(receipt); expect(result.totalCost, 10.50); }); test('derives total when gallons and price per gallon are known', () { const receipt = ''' GALLONS 10.000 PPG 3.500 '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 10.0); expect(result.pricePerGallon, 3.5); expect(result.totalCost, 35.0); }); test('derives gallons when total and price per gallon are known', () { const receipt = ''' PRICE PER GALLON \$4.000 TOTAL \$20.00 '''; final result = ReceiptParser.parse(receipt); expect(result.pricePerGallon, 4.0); expect(result.totalCost, 20.0); expect(result.gallons, 5.0); }); test('returns nulls when nothing recognizable is present', () { const receipt = 'THANK YOU FOR YOUR PURCHASE'; final result = ReceiptParser.parse(receipt); expect(result.gallons, isNull); expect(result.pricePerGallon, isNull); expect(result.totalCost, isNull); expect(result.date, isNull); }); }); group('ReceiptParser state parsing', () { test('detects a state abbreviation directly before a ZIP code', () { const receipt = ''' Casey's 420 W. 6TH STREET KEARNEY, MO 64060 '''; expect(ReceiptParser.parse(receipt).state, 'MO'); }); test('detects a non-Missouri state the same way', () { const receipt = ''' Swift Stop #3 1809 Hobbs Hwy Seminole TX 79360 '''; expect(ReceiptParser.parse(receipt).state, 'TX'); }); test('falls back to a state right after a comma when no ZIP is nearby', () { const receipt = ''' Buc-ee's 6988 Buc-ee's Blvd Leeds, AL '''; expect(ReceiptParser.parse(receipt).state, 'AL'); }); test('does not mistake "VI" (Visa) for the Virgin Islands', () { const receipt = ''' Casey's VI ########8533 Pump Gallons Price 07 32.115 \$ 3.759 '''; expect(ReceiptParser.parse(receipt).state, isNull); }); test('does not mistake the word "In" for Indiana', () { const receipt = ''' Gallons 10.000 Price/Gal \$3.399 Total Fuel \$33.99 All Taxes Included In Fuel Price. '''; expect(ReceiptParser.parse(receipt).state, isNull); }); test('returns null when no address-like state pattern is present', () { expect(ReceiptParser.parse('THANK YOU FOR YOUR PURCHASE').state, isNull); }); }); group('ReceiptParser date parsing', () { test('parses "Date: M/D/YYYY" + "Time: H:MM:SS AM/PM" on separate lines', () { const receipt = ''' Date: 8/22/2024 Time: 10:11:00 AM Gallons 10.000 '''; final result = ReceiptParser.parse(receipt); expect(result.date, DateTime(2024, 8, 22, 10, 11)); }); test('parses "DATE M/D/YY H:MM" on the same line', () { const receipt = ''' DATE 3/26/22 18:12 PUMP# 06 '''; final result = ReceiptParser.parse(receipt); expect(result.date, DateTime(2022, 3, 26, 18, 12)); }); test('parses a 2-digit year and a no-space AM/PM time', () { const receipt = ''' Date 01/23/20 Time 02:11PM '''; final result = ReceiptParser.parse(receipt); expect(result.date, DateTime(2020, 1, 23, 14, 11)); }); test('12 AM and 12 PM convert correctly (midnight/noon edge case)', () { expect(ReceiptParser.parse('Date 1/1/24 12:00 AM').date, DateTime(2024, 1, 1, 0, 0)); expect(ReceiptParser.parse('Date 1/1/24 12:30 PM').date, DateTime(2024, 1, 1, 12, 30)); }); test('falls back to midnight when a date is found but no time is nearby', () { // Comfortably more than the 40-character "nearby" search window // between the date and the unrelated text before a time appears // much later, regardless of how this string literal is indented. final filler = 'x' * 100; final receipt = 'Date 12/14/16\n$filler\nTime 5:11 PM'; final result = ReceiptParser.parse(receipt); // Too far from the date to count as "nearby", so this should be // date-only at midnight — still better than no date at all, and the // confirm screen lets the user fix the time manually either way. expect(result.date, isNotNull); expect(result.date!.year, 2016); expect(result.date!.month, 12); expect(result.date!.day, 14); expect(result.date!.hour, 0); expect(result.date!.minute, 0); }); test('returns null when no date-like pattern is present', () { final result = ReceiptParser.parse('GALLONS 10.000\nPPG 3.500'); expect(result.date, isNull); }); }); // Transcribed from a batch of real photographed receipts across different // gas station chains, to catch label/layout variations the handful of // hand-written cases above don't happen to cover. group('ReceiptParser against real receipt layouts', () { test('Break Time — labels adjacent to values on the same line', () { const receipt = ''' Date: 8/22/2024 Time: 10:11:00 AM OXY87 Pump Number 22 Gallons 10.000 Price/Gal \$3.399 Total Fuel \$33.99 Total Sale \$33.99 Visa \$33.99 All Taxes Included In Fuel Price. '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 10.000); expect(result.pricePerGallon, 3.399); expect(result.totalCost, 33.99); }); test('Break Time — tabular Pump/Gallons/Price header row', () { const receipt = ''' Member# 111795637651 Invoice# 11733 Date: 03/23/22 Time: 10:05 Auth# 020413 VI Acct # ************0344 Pump Gallons Price 14 5.116 \$ 5.499 Product Amount Regular \$ 28.13 Total Sale \$ 28.13 SALE- Contactless Approved '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 5.116); expect(result.pricePerGallon, 5.499); expect(result.totalCost, 28.13); }); test('Buc-ee\'s — abbreviated "PRICE/G" label', () { const receipt = ''' BUC-EE'S 6988 Buc-ee's Blvd Leeds AL PUMP No. 63 GALLONS 9.999 PRICE/G \$2.499 TOTAL FUEL \$24.99 Regular TOTAL SALE \$24.99 Visa \$24.99 '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 9.999); expect(result.pricePerGallon, 2.499); expect(result.totalCost, 24.99); }); test('Costco — tabular header row, "Total Sale" phrasing', () { const receipt = ''' Costco #01448 1524 Beasie RD Murfreesboro TN Date: 01/03/24 Time: 19:31 Pump Gallons Price 11 12.598 \$ 2.399 Product Amount Regular \$ 30.22 Total Sale \$ 30.22 SALE- Contactless '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 12.598); expect(result.pricePerGallon, 2.399); expect(result.totalCost, 30.22); }); test('Murphy USA — "QTY(GAL)" label instead of "GALLONS"', () { const receipt = ''' PUMP: 8 PROD: UNLEAD PRICE/GAL: \$1.799 NET/GAL: \$1.799 QTY(GAL): 13.164 FUEL TOTAL: \$23.68 NET TOTAL: \$23.68 '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 13.164); expect(result.pricePerGallon, 1.799); expect(result.totalCost, 23.68); }); test('Phillips 66 — "PRICE/G" label and "FUEL SALE" as the total', () { const receipt = ''' WELCOME LAWNSIDE PHILLIPS 66 355 Warrington Ave Lawnside NJ 08045 DATE 3/26/22 18:12 TRAN# 060757 PUMP# 06 SERVICE LEVEL: FULL PRODUCT: REGULAR GALLONS 3.642 PRICE/G \$4.119 FUEL SALE \$15.00 CREDIT \$15.00 '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 3.642); expect(result.pricePerGallon, 4.119); expect(result.totalCost, 15.00); }); test('Phillips 66 — second "FUEL SALE" example', () { const receipt = ''' WELCOME SWIFT STOP #3 1809 HOBBS HWY SEMINOLE TX 79360 DATE 5/9/23 11:16 TRAN#9103742 PUMP# 10 SERVICE LEVEL: SELF PRODUCT: DIESEL 2 GALLONS: 29.854 PRICE/G: \$3.499 FUEL SALE \$181.66 CREDIT \$181.66 '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 29.854); expect(result.pricePerGallon, 3.499); expect(result.totalCost, 181.66); }); test('Phillips 66 / Murphy-style — "QTY(GAL)" plus "FUEL TOTAL"', () { const receipt = ''' Verified by PIN PIN USED PUMP: 5 PROD: UNLEAD PRICE/GAL: \$3.959 NET/GAL: \$3.959 QTY(GAL): 11.068 FUEL TOTAL: \$43.82 NET TOTAL: \$43.82 GET REWARDED! '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 11.068); expect(result.pricePerGallon, 3.959); expect(result.totalCost, 43.82); }); test('QuikTrip — tabular header row, total not printed on receipt (derived)', () { const receipt = ''' QUIKTRIP #01012 383 George Liles PWK Concord, NC Invoice # 0000000 Date 01/23/20 Time 02:11PM Auth # 03581I Acct # ************1821 Pump Gallons Price 14 10.496 \$2.359 '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 10.496); expect(result.pricePerGallon, 2.359); // Not printed within the visible receipt text — derived from // gallons * price rather than found via a label. expect(result.totalCost, closeTo(10.496 * 2.359, 0.01)); }); test('Sinclair — unit letter attached with no space, and a negative ' 'per-gallon discount line that must not be mistaken for gallons', () { const receipt = ''' PUMP# 3 UNLEADED CR 17.364G PRICE/GAL \$2.649 DISCOUNTS BEFORE FUELING Debit Di/GAL \$-0.100 FUEL TOTAL \$46.00 DEBIT \$46.00 '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 17.364); expect(result.pricePerGallon, 2.649); expect(result.totalCost, 46.00); }); test('Sinclair — PPG and GALLONS/FUEL TOTAL both split across header/data rows', () { const receipt = ''' SINCLAIR 5905 NW 72ND ST KANSAS CITY MO Date 12/14/16 Time 5:11 PM Auth 068113 VISA Pump Product PPG 05 UNLD \$1.999 Gallons Fuel Total 9.505 \$19.00 Thank you '''; final result = ReceiptParser.parse(receipt); expect(result.gallons, 9.505); expect(result.pricePerGallon, 1.999); // "Fuel Total" is split from its value by a newline in this layout, // so it isn't label-matched — derived from gallons * price instead, // landing within a cent of the printed $19.00. expect(result.totalCost, closeTo(19.00, 0.01)); }); }); }