One upload point, every applicable step. Persist the batch, classify each
file, ONE LiteLLM call (cloud-budget, temperature 0, strict json_schema; PDFs
as unpdf text, never pixels) with an order-item prompt that keeps names
verbatim, header (merchant, date, total), merchant pattern
(coles|woolworths|woolies|aldi|iga|foodworks|costco) -> grocery | food | ask,
exact (platform, merchant, date, total) match against transactions +
expense_metadata, then act:
- food + unique match -> replace line_items via the C2 path, 'enriched'
- food + miss/ambiguity -> 'candidates' (same merchant ±3d) with
intake_use_candidate / intake_create buttons; never auto-creates
- grocery -> original files to pantry POST /api/capture with
finance_target_transaction_id + finance_action enrich|create (C3);
409 -> 'duplicate'
- ask / needs_header / held / duplicate / unsupported as replies, not writes
Reconciliation is against the printed item subtotal (a delivery total is
items + fees - promotions: 18.94 of Coles lines settled at 5.74), or
2%, else 'held'. Originals are kept under INTAKE_FILE_DIR by sha256 so a later
'Groceries' tap can still forward them.
C2 is an idempotent replace on transaction_id = :id OR
matched_transaction_id = :id, accepts INTAKE_TOKEN or RECEIPT_INGEST_TOKEN,
creates the expense_metadata row when absent, 404 when the transaction does
not exist, returns {previous_line_items, line_items} so pantry's undo is the
same call with the previous list.
44 lines
1.2 KiB
JSON
44 lines
1.2 KiB
JSON
{
|
|
"name": "finance-app",
|
|
"version": "0.1.0",
|
|
"private": true,
|
|
"scripts": {
|
|
"dev": "next dev",
|
|
"build": "next build",
|
|
"start": "next start",
|
|
"lint": "eslint",
|
|
"test": "vitest run --config vitest.config.ts",
|
|
"test:watch": "vitest --config vitest.config.ts",
|
|
"test:setup": "bash scripts/setup-test-db.sh",
|
|
"pretest:integration": "bash scripts/resolve-test-db.sh",
|
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
"test:all": "npm test && npm run test:integration"
|
|
},
|
|
"dependencies": {
|
|
"@prisma/adapter-pg": "^7.4.2",
|
|
"@prisma/client": "^7.4.2",
|
|
"@tanstack/react-query": "^5.90.21",
|
|
"next": "16.1.6",
|
|
"pg": "^8.20.0",
|
|
"prisma": "^7.4.2",
|
|
"react": "19.2.3",
|
|
"react-dom": "19.2.3",
|
|
"recharts": "^3.8.0",
|
|
"unpdf": "^1.8.1"
|
|
},
|
|
"devDependencies": {
|
|
"@tailwindcss/postcss": "^4",
|
|
"@types/node": "^20",
|
|
"@types/pg": "^8.18.0",
|
|
"@types/react": "^19",
|
|
"@types/react-dom": "^19",
|
|
"@vitest/coverage-v8": "^4.1.2",
|
|
"eslint": "^9",
|
|
"eslint-config-next": "16.1.6",
|
|
"tailwindcss": "^4",
|
|
"typescript": "^5",
|
|
"vite-tsconfig-paths": "^6.1.1",
|
|
"vitest": "^4.1.2"
|
|
}
|
|
}
|