fix(invoice-inbox): make the extraction prompt fill invoiceDate on receipts (#2429)
Receipts came back without a date on 46% of items in the last 30 days (75% via WhatsApp), while supplier invoices lost it on 0.4% and the purchaseTime on the very same receipts was filled almost every time. The prompt described invoice.invoiceDate as a bare ISO date under the invoice block, right beside a purchaseTime rule marked "receipts only", and the model read the asymmetry as "invoice-only". Describe the field as the invoice date or, on a receipt, the purchase date printed on it, and add an explicit receipts rule. Pin both in the extraction test. No schema or data-shape change; already-extracted items are not touched. Claude-Session: https://claude.ai/code/session_015bTBgZrofpGCgSUfAKN2H3 Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5.1
parent
26e29f47bc
commit
b2b714f829
@@ -238,6 +238,31 @@ describe('extractInvoiceFields', () => {
|
||||
expect(content[0].source.media_type).toBe('image/jpeg')
|
||||
})
|
||||
|
||||
it('tells the model that a receipt carries its purchase date in invoiceDate', async () => {
|
||||
// Prod, last 30 days as of 2026-09-08: supplier invoices lost invoiceDate
|
||||
// on 0.4% of items, receipts on 46% (75% via WhatsApp), while purchaseTime
|
||||
// was filled on nearly every one of those receipts. The field was described
|
||||
// as a bare ISO date under the invoice block, right next to a purchaseTime
|
||||
// rule marked "receipts only", and the model took the asymmetry literally.
|
||||
// Pin the receipt rule so a prompt edit cannot silently drop it again.
|
||||
mockCreate.mockReturnValueOnce(aiResponse(VALID_RESULT))
|
||||
await extractInvoiceFields({
|
||||
buffer: Buffer.from('JPEG'),
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'kvitto.jpg',
|
||||
})
|
||||
const call = mockCreate.mock.calls[0][0]
|
||||
const system: string =
|
||||
typeof call.system === 'string'
|
||||
? call.system
|
||||
: call.system.map((block: { text: string }) => block.text).join('\n')
|
||||
expect(system).toContain(
|
||||
'"invoiceDate": string | null, // ISO date YYYY-MM-DD: the invoice date, or on a receipt the purchase date printed on it'
|
||||
)
|
||||
expect(system).toContain('- invoiceDate on receipts: the purchase date printed on the receipt')
|
||||
expect(system).toContain('The field is NOT invoice-only')
|
||||
})
|
||||
|
||||
it('sends document content for a PDF upload', async () => {
|
||||
mockCreate.mockReturnValueOnce(aiResponse(VALID_RESULT))
|
||||
await extractInvoiceFields({
|
||||
|
||||
@@ -358,7 +358,7 @@ Return ONLY a single JSON object that matches this schema exactly. No prose, no
|
||||
},
|
||||
"invoice": {
|
||||
"invoiceNumber": string | null, // include any suffix, e.g. "06655767-0007"
|
||||
"invoiceDate": string | null, // ISO date YYYY-MM-DD
|
||||
"invoiceDate": string | null, // ISO date YYYY-MM-DD: the invoice date, or on a receipt the purchase date printed on it
|
||||
"dueDate": string | null, // ISO date YYYY-MM-DD
|
||||
"paymentReference": string | null, // OCR / payment reference
|
||||
"currency": string, // ISO 4217 (SEK, USD, EUR, ...). Default "SEK" only if truly indeterminate.
|
||||
@@ -399,6 +399,7 @@ Rules:
|
||||
- legibility: "good" = all key amounts and the merchant are readable. "partial" = some key fields are cut off, blurry, or unreadable. "unreadable" = the document is mostly illegible (too blurry/dark/small). Judge the IMAGE quality, not whether fields exist on the document.
|
||||
- payment: only for documents that show how payment was made. "card" for kort/VISA/Mastercard; cardLast4 only when a masked card number like ****1234 is printed. "invoice" means the document says it will be billed separately.
|
||||
- purchaseTime: the HH:MM time printed on a receipt. null when absent.
|
||||
- invoiceDate on receipts: the purchase date printed on the receipt (the "Datum"/"Date" line, usually right next to the time, or the date on the card slip). The field is NOT invoice-only: every receipt carries a date, so fill it whenever one is printed, and leave null only when no date is printed or it is unreadable.
|
||||
- Öresavrundning: Swedish receipts often show an "Avrundning"/"Öresavrundning" line. "total" is ALWAYS the amount actually paid AFTER rounding; put the rounding line in totals.roundingAmount (negative when rounded down). When present: subtotal + vatAmount + roundingAmount = total.
|
||||
- Currency: detect from the document (symbol $/€/kr or explicit code). Use the ISO 4217 code. Do NOT default to SEK if the document clearly shows another currency.
|
||||
- "total" is the amount the buyer must pay (look for "Att betala", "Total", "Amount paid", "Amount due", "Balance"). Prefer this over Subtotal.
|
||||
|
||||
Reference in New Issue
Block a user