MO-Fuel-Tax-Back/test/receipt_parser_test.dart

483 lines
13 KiB
Dart

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));
});
});
}