Repository navigation
fix(invoices): keep the full Anthropic receipt number - #130
Conversation
Ireland receipts split the invoice number on a space, and the parser stored only the 8-character prefix, so later ingests all collided. Keep the canonical PREFIX SUFFIX form and reject a missing suffix. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
🟡 Changes recommended
Duplicate detection can miss legacy invoice numbers and manual re-uploads, allowing duplicate invoices and billed costs, while Anthropic-specific parsing can reject or alter other suppliers’ invoice numbers.
3 open issues
What changed in this PR
This PR updates the Hub’s invoice ingestion to preserve full Anthropic receipt numbers and distinguish receipts that share a prefix.
Changes:
- Adds receipt-number parsing, canonicalization, duplicate matching, and regression tests.
- Treats Anthropic’s PBC and Ireland names as equivalent in vendor filters and credit-purchase queries.
- Adds migration 0034 to normalize dashed numbers and repair invoice 216, leaving invoice 209 for manual correction.
| File | Description |
|---|---|
tests/unit/vendor-identity.test.ts |
Tests Anthropic vendor aliases and filtering. |
tests/unit/invoice-number.test.ts |
Tests number formats and missing suffixes. |
tests/unit/invoice-extraction.test.ts |
Tests extraction overrides and partial-number rejection. |
tests/unit/invoice-duplicate.test.ts |
Tests canonical and truncated-number conflicts. |
tests/unit/api/invoice-ingest.test.ts |
Tests distinct receipts, reposts, and extraction errors. |
src/lib/vendor-identity.ts |
Centralizes Anthropic vendor comparisons. |
src/lib/mcp/data.ts |
Expands Anthropic invoice filtering across aliases. |
src/lib/invoice-number.ts |
Adds receipt parsing and canonicalization helpers. |
src/lib/invoice-extraction.ts |
Integrates full-number parsing into PDF extraction. |
src/lib/invoice-duplicate.ts |
Adds shared invoice conflict detection. |
src/lib/ingestion-filters.ts |
Uses vendor equivalence for ingestion rules. |
src/lib/db/migrations/meta/0034_snapshot.json |
Records the migration’s schema snapshot. |
src/lib/db/migrations/meta/_journal.json |
Registers migration 0034. |
src/lib/db/migrations/0034_fix_truncated_invoice_numbers.sql |
Normalizes stored references and repairs invoice 216. |
src/app/api/invoices/ingest/route.ts |
Uses shared conflict detection and extraction errors. |
src/actions/invoices.ts |
Uses shared conflict detection for manual checks. |
src/actions/credits.ts |
Includes Ireland receipts in credit-purchase candidates. |
Review effort: Balanced
| invoiceDate: "", | ||
| amountCents: -1, | ||
| vendor: null, |
There was a problem hiding this comment.
Fixed in c5e2f48. checkInvoiceDuplicate now takes the invoice number, date, amount, and vendor. The upload form passes those after extraction and again when the invoice number blurs. saveInvoice runs the same identity check before insert, so a re-upload of invoice 209 (K6EV5YRI) is rejected when vendor, date, and amount match instead of creating another invoice and billed cost. The truncated-prefix match only runs when those three fields are actually present; the empty date / -1 / null sentinels are gone.
| like(invoices.invoiceNumber, `${prefix} %`), | ||
| like(invoices.invoiceNumber, `${prefix}-%`), |
There was a problem hiding this comment.
Fixed in c5e2f48. The candidate query now also loads separator-free stored numbers with invoice_number ~* '^PREFIX[0-9]+$' (for example K6EV5YRI0095, which migration 0034 does not rewrite). chooseConflict already treats that as the same receipt as K6EV5YRI 0095, so a re-ingest matches the existing row instead of inserting another invoice and billed cost.
| const parsedNumber = parseInvoiceNumberFromText(text); | ||
| if (parsedNumber.status === "error") { | ||
| return { success: false, error: parsedNumber.error }; |
There was a problem hiding this comment.
Fixed in c5e2f48. The strict prefix/suffix parser and the canonical rewrite run only after the receipt is identified as Anthropic: the PDF text contains "Anthropic", or the extracted vendor is an Anthropic legal name. Another supplier's 12345678 stays on the generic path and is not a 422, and 1234567890 is left as-is instead of being rewritten to 12345678 90. A missing Anthropic suffix is still a 422.
Manual re-uploads pass vendor, date, and amount, so a truncated row such as invoice 209 is still a duplicate, and save rejects that identity. The lookup also loads separator-free stored numbers. Strict prefix/suffix parsing runs only once the receipt is identified as Anthropic. Co-authored-by: Cursor <cursoragent@cursor.com>


Summary
9BF0758D-10147934stored as9BF0758D), so every later ingest returned 409 against the first row. The parser now keeps an 8-character prefix plus a numeric suffix of any length and stores the canonicalPREFIX SUFFIXform already used by older rows (K6EV5YRI 0101).0034rewrites a dashed number to the single-space form and corrects invoice 216 to9BF0758D 10147934. It does not guess a suffix for invoice 209. Not applied to production from this branch.Test plan
pnpm test— parser fixtures (dash, space, no separator, missing suffix), ingest of two9BF0758Dreceipts with different suffixes (both 200) and a re-post (409), 422 when the suffix is missingpnpm typecheckandpnpm lintpnpm db:migrateand confirm invoice 216 is9BF0758D 10147934only when id, amount ($67.08), and date (2026-10-24) still match the truncated rowK6EV5YRI, $4.27, 2026-08-11) and correct the suffix by hand