diff --git a/MoneyMap/Pages/ViewReceipt.cshtml.cs b/MoneyMap/Pages/ViewReceipt.cshtml.cs
index 1fe381f..b657018 100644
--- a/MoneyMap/Pages/ViewReceipt.cshtml.cs
+++ b/MoneyMap/Pages/ViewReceipt.cshtml.cs
@@ -1,7 +1,3 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
@@ -88,7 +84,7 @@ namespace MoneyMap.Pages
return File(fileBytes, receipt.ContentType);
}
- public async Task OnPostParseAsync(long id, string parser)
+ public async Task OnPostParseAsync(long id, string parser, string? model = null)
{
var selectedParser = _parsers.FirstOrDefault(p => p.GetType().Name == parser);
@@ -98,7 +94,7 @@ namespace MoneyMap.Pages
return RedirectToPage(new { id });
}
- var result = await selectedParser.ParseReceiptAsync(id);
+ var result = await selectedParser.ParseReceiptAsync(id, model);
if (result.IsSuccess)
{
diff --git a/MoneyMap/Prompts/ReceiptParserPrompt.txt b/MoneyMap/Prompts/ReceiptParserPrompt.txt
index 784faed..15c1866 100644
--- a/MoneyMap/Prompts/ReceiptParserPrompt.txt
+++ b/MoneyMap/Prompts/ReceiptParserPrompt.txt
@@ -10,19 +10,38 @@ Analyze this receipt image and extract the following information as JSON:
"lineItems": [
{
"description": "item name",
- "quantity": 1.0 (or null),
+ "upc": "1234567890123" (or null if not found),
+ "quantity": 1.0,
"unitPrice": 0.00 (or null),
- "lineTotal": 0.00
+ "lineTotal": 0.00,
+ "voided": false
}
]
}
Extract all line items you can see on the receipt. For each item:
-- description: The item or service name
-- quantity: Only include if this is an actual countable product (like groceries). For services, fees, charges, or taxes, set to null.
-- unitPrice: Price per unit if quantity applies, otherwise null
-- lineTotal: The total amount for this line (required)
+- description: The item or service name (include any count/size info in the description itself, like "4CT" or "12 OZ")
+- upc: The UPC/barcode number if visible (usually a 12-13 digit number near the item). This helps track price changes over time. Set to null if not found.
+- quantity: ALWAYS set to 1.0 for ALL retail products (groceries, goods, merchandise, etc.) - this is the default. ONLY use null for utility bills, service fees, or taxes (non-product items). If it's a physical item on a retail receipt, use 1.0.
+- unitPrice: Calculate as lineTotal divided by quantity (so usually equals lineTotal for retail items). Set to null only if quantity is null.
+- lineTotal: The total amount for this line (the price shown on the receipt, or 0.00 if voided)
+- voided: Set to true if this item appears immediately after a "** VOIDED ENTRY **" marker or similar void indicator. Set to false for all other items.
-For utility bills, service charges, fees, and taxes - these are NOT products with quantities, so set quantity and unitPrice to null.
+CRITICAL - HANDLING VOIDED ITEMS:
+- NEVER skip or ignore ANY line items on the receipt
+- When you see "** VOIDED ENTRY **" or similar void markers, the item immediately after it is voided
+- For voided items: set "voided": true and "lineTotal": 0.00
+- For all other items: set "voided": false
+- CONTINUE reading and extracting ALL items that appear after void markers - do NOT stop parsing
+- The receipt may have many items listed after a void marker - you MUST include every single one
+- Include EVERY line item you can see, whether voided or not
+
+OTHER IMPORTANT RULES:
+- Quantity MUST be 1.0 for ALL physical retail items (groceries, food, household goods, etc.) - do NOT leave it null
+- Every item on a grocery/retail receipt gets quantity: 1.0 unless you see explicit indicators like "2 @" or "QTY 3"
+- Only utility bills, service charges, fees, or taxes (non-product line items) should have null quantity
+- Don't confuse product descriptions (like "4CT BLUE MUF" meaning 4-count muffin package) with quantity fields (like "2 @ $3.99")
+- Extract UPC/barcode numbers when visible - they're usually long numeric codes (12-13 digits)
+- Read through the ENTIRE receipt from top to bottom - don't stop early
If this is a bill (utility, credit card, etc.), look for a due date, payment due date, or deadline and extract it as dueDate. For regular receipts, dueDate should be null.
\ No newline at end of file
diff --git a/MoneyMap/Services/OpenAIReceiptParser.cs b/MoneyMap/Services/OpenAIReceiptParser.cs
index fb1e715..3bfb7d6 100644
--- a/MoneyMap/Services/OpenAIReceiptParser.cs
+++ b/MoneyMap/Services/OpenAIReceiptParser.cs
@@ -1,23 +1,15 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net.Http;
-using System.Text;
-using System.Text.Json;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Hosting;
+using ImageMagick;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Configuration;
using MoneyMap.Data;
using MoneyMap.Models;
-using ImageMagick;
+using System.Text;
+using System.Text.Json;
namespace MoneyMap.Services
{
public interface IReceiptParser
{
- Task ParseReceiptAsync(long receiptId);
+ Task ParseReceiptAsync(long receiptId, string? model = null);
}
public class OpenAIReceiptParser : IReceiptParser
@@ -46,7 +38,7 @@ namespace MoneyMap.Services
_serviceProvider = serviceProvider;
}
- public async Task ParseReceiptAsync(long receiptId)
+ public async Task ParseReceiptAsync(long receiptId, string? model = null)
{
var receipt = await _db.Receipts
.Include(r => r.Transaction)
@@ -67,11 +59,14 @@ namespace MoneyMap.Services
if (!File.Exists(filePath))
return ReceiptParseResult.Failure("Receipt file not found on disk.");
+ // Default to gpt-4o-mini if no model specified
+ var selectedModel = model ?? "gpt-4o-mini";
+
var parseLog = new ReceiptParseLog
{
ReceiptId = receiptId,
Provider = "OpenAI",
- Model = "gpt-4o-mini",
+ Model = selectedModel,
StartedAtUtc = DateTime.UtcNow,
Success = false
};
@@ -97,7 +92,7 @@ namespace MoneyMap.Services
// Call OpenAI Vision API with transaction name context
var transactionName = receipt.Transaction?.Name;
- var parseData = await CallOpenAIVisionAsync(apiKey, base64Data, mediaType, transactionName);
+ var parseData = await CallOpenAIVisionAsync(apiKey, base64Data, mediaType, selectedModel, transactionName);
// Update receipt with parsed data
receipt.Merchant = parseData.Merchant;
@@ -128,9 +123,11 @@ namespace MoneyMap.Services
ReceiptId = receiptId,
LineNumber = index + 1,
Description = item.Description,
+ Sku = item.Upc,
Quantity = item.Quantity,
UnitPrice = item.UnitPrice,
- LineTotal = item.LineTotal
+ LineTotal = item.LineTotal,
+ Voided = item.Voided
}).ToList();
_db.ReceiptLineItems.AddRange(lineItems);
@@ -220,7 +217,7 @@ namespace MoneyMap.Services
return _promptTemplate;
}
- private async Task CallOpenAIVisionAsync(string apiKey, string base64Image, string mediaType, string? transactionName = null)
+ private async Task CallOpenAIVisionAsync(string apiKey, string base64Image, string mediaType, string model, string? transactionName = null)
{
// Load the prompt template from file
var promptText = await LoadPromptTemplateAsync();
@@ -235,7 +232,7 @@ namespace MoneyMap.Services
var requestBody = new
{
- model = "gpt-4o-mini",
+ model = model,
messages = new[]
{
new
@@ -317,9 +314,11 @@ namespace MoneyMap.Services
public class ParsedLineItem
{
public string Description { get; set; } = "";
+ public string? Upc { get; set; }
public decimal? Quantity { get; set; }
public decimal? UnitPrice { get; set; }
public decimal LineTotal { get; set; }
+ public bool Voided { get; set; }
}
public class ReceiptParseResult