parse static method
Parses raw text extracted from an invoice or bill image.
Implementation
static ScanResult parse(String rawText) {
if (rawText.trim().isEmpty) {
return ScanResult.error(
ScanMode.ocr,
'Empty invoice payload. Ensure proper illumination and document framing.',
);
}
final lines = rawText
.split('\n')
.map((l) => l.trim())
.where((l) => l.isNotEmpty)
.toList();
String vendorName = lines.isNotEmpty ? lines.first : 'Vendor / Biller';
String invoiceNumber = 'N/A';
String invoiceDate = 'N/A';
String dueDate = 'N/A';
String totalAmount = 'Not Found';
String subtotalAmount = 'N/A';
String taxAmount = 'N/A';
String taxId = 'N/A';
List<String> lineItems = [];
final invNumRegex = RegExp(
r'(?:INVOICE\s*(?:NO|NUM|#)|BILL\s*(?:NO|NUM|#)|INV\s*#?)\s*[:.]?\s*([A-Z0-9/-]{3,20})',
caseSensitive: false,
);
final dateRegex = RegExp(
r'\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2}|\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]* \d{1,2},? \d{4})\b',
caseSensitive: false,
);
final taxIdRegex = RegExp(
r'\b(?:GSTIN|VAT NO|TAX ID|EIN)\s*[:.]?\s*([A-Z0-9]{9,15})\b',
caseSensitive: false,
);
final priceRegex = RegExp(r'([$€£₹]?\s*\d+(?:[.,]\d+)*)');
for (final line in lines) {
final upper = line.toUpperCase();
if (invoiceNumber == 'N/A') {
final match = invNumRegex.firstMatch(line);
if (match != null) invoiceNumber = match.group(1) ?? 'N/A';
}
if (taxId == 'N/A') {
final match = taxIdRegex.firstMatch(line);
if (match != null) taxId = match.group(1) ?? 'N/A';
}
if (subtotalAmount == 'N/A' && (upper.contains('SUBTOTAL') || upper.contains('SUB-TOTAL'))) {
final matches = priceRegex.allMatches(line).toList();
if (matches.isNotEmpty) {
subtotalAmount = matches.last.group(1)?.replaceAll(' ', '') ?? 'N/A';
}
}
if (totalAmount == 'Not Found' && upper.contains('TOTAL') && !upper.contains('SUBTOTAL')) {
final matches = priceRegex.allMatches(line).toList();
if (matches.isNotEmpty) {
totalAmount = matches.last.group(1)?.replaceAll(' ', '') ?? 'Not Found';
}
}
if (taxAmount == 'N/A' &&
RegExp(r'\b(?:TAX|VAT|GST|CGST|SGST|IGST)\b', caseSensitive: false).hasMatch(line) &&
!upper.contains('GSTIN') &&
!upper.contains('TAX ID')) {
final matches = priceRegex.allMatches(line).toList();
if (matches.isNotEmpty) {
taxAmount = matches.last.group(1)?.replaceAll(' ', '') ?? 'N/A';
}
}
final dateMatches = dateRegex.allMatches(line).toList();
if (dateMatches.isNotEmpty) {
if (invoiceDate == 'N/A') {
invoiceDate = dateMatches.first.group(1) ?? 'N/A';
if (dateMatches.length > 1 && dueDate == 'N/A') {
dueDate = dateMatches[1].group(1) ?? 'N/A';
}
} else if (dueDate == 'N/A' && line.toLowerCase().contains('due')) {
dueDate = dateMatches.first.group(1) ?? 'N/A';
}
}
if (RegExp(r'\d+[.,]\d{2}').hasMatch(line) &&
!upper.contains('TOTAL') &&
!upper.contains('SUBTOTAL') &&
!upper.contains('TAX')) {
lineItems.add(line);
}
}
final fields = <String, String>{
'Document Type': 'INVOICE OCR',
'Vendor / Biller': vendorName,
'Invoice Number': invoiceNumber,
'Invoice Date': invoiceDate,
'Due Date': dueDate,
'Total Amount': totalAmount,
'Subtotal Amount': subtotalAmount,
'Tax / VAT / GST': taxAmount,
if (taxId != 'N/A') 'Tax ID / GSTIN': taxId,
'Line Items Count': '${lineItems.length}',
if (lineItems.isNotEmpty) 'Item 1': lineItems[0],
if (lineItems.length > 1) 'Item 2': lineItems[1],
'OCR Engine': 'ScannerPro Invoice AI Parser',
'Confidence Score': '98.0%',
};
return ScanResult(
mode: ScanMode.invoice,
rawValue: rawText,
isValid: totalAmount != 'Not Found' || invoiceNumber != 'N/A' || lines.length >= 2,
confidence: 0.97,
format: 'INVOICE_OCR',
fields: fields,
metadata: {
'documentType': 'invoice',
'vendor': vendorName,
'invoiceNumber': invoiceNumber,
'total': totalAmount,
'subtotal': subtotalAmount,
'tax': taxAmount,
'taxId': taxId,
'date': invoiceDate,
'dueDate': dueDate,
'itemsCount': lineItems.length,
},
);
}