diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..05e3141 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +Application code lives in `src/`. Next.js App Router pages and API route handlers belong in `src/app/`; reusable UI components are in `src/components/`; database access, query functions, hooks, authentication, and domain helpers are in `src/lib/`. Tests are separated into `src/__tests__/unit/` and `src/__tests__/integration/`. PostgreSQL schema and numbered SQL migrations live under `prisma/`, static assets under `public/`, operational scripts under `scripts/`, and design notes under `docs/`. + +Keep data flow consistent: API routes call query functions in `src/lib/queries.ts`, which use `queryRaw()` from `src/lib/db.ts`; client components access APIs through TanStack Query hooks in `src/lib/hooks.ts`. + +## Build, Test, and Development Commands + +- `npm ci` installs the locked dependency set (Node 22 is used in CI). +- `npm run dev` starts the local Next.js development server. +- `npm run build` creates a production build; `npm start` serves it. +- `npm run lint` runs the Next.js ESLint configuration. Existing lint debt makes CI lint advisory, but new code should pass. +- `npm test` runs fast unit tests. +- `npm run test:setup` prepares the PostgreSQL test database using `.env.test`. +- `npm run test:integration` runs database-backed tests. +- `npm run test:all` runs both test suites. + +## Coding Style & Naming Conventions + +Use strict TypeScript, two-space indentation, semicolons, and double quotes, matching existing files. Name React components and types in PascalCase, functions and variables in camelCase, and files/routes in kebab-case. Use the `@/` alias for imports from `src/`. Preserve owner scoping and prefer transaction overrides with `COALESCE` in financial queries. Every API route must authenticate before accessing data. + +## Testing Guidelines + +Vitest is the test framework. Name tests `*.test.ts` and place pure logic tests under `unit/`; put PostgreSQL-dependent behavior under `integration/`. Add regression coverage for query, rule, reconciliation, and category changes. No numeric coverage threshold is configured; focus on meaningful edge cases and run `npm run test:all` before submitting database-related changes. + +## Database, Security & Configuration + +Add schema changes as the next numbered `prisma/migrations/NNNN_description/migration.sql`. Never commit `.env`, `.env.test`, raw statements in `dump/`, or other financial data. Consult `CLAUDE.md` and relevant `docs/` notes before changing splits, settlements, loans, reconciliation, or statement accounting. + +## Commits & Pull Requests + +History follows concise Conventional Commit-style subjects such as `feat(rules): preview rule changes`, `fix(trips): ...`, and `docs: ...`. Keep commits focused. Pull requests should explain behavior and data-model impact, link related issues, list validation commands, and include screenshots for UI changes. Ensure unit tests and the production build pass; call out any known lint warnings or migration steps. diff --git a/docs/ui-information-architecture-review.md b/docs/ui-information-architecture-review.md new file mode 100644 index 0000000..ca94b95 --- /dev/null +++ b/docs/ui-information-architecture-review.md @@ -0,0 +1,661 @@ +# UI and information architecture review + +**Date:** 2026-07-26 +**Status:** Review and redesign proposal — nothing implemented + +## Executive summary + +The July 19 UI refresh gave the app a cohesive and distinctive visual identity. +The ink-and-copper palette, typography, financial number treatment, month spine, +and transaction drill-downs are all strong foundations. + +The larger remaining issue is not appearance. It is information hierarchy. +Analytics and Insights contain useful data, but they are reporting-heavy rather +than decision-oriented. Shared communicates the immediate running balance, but +the current settlement model prevents it from answering which expenses a payment +settled, whether a trip is closed, or how the shared loan should be represented. + +The product should make four questions easy to answer: + +1. Am I financially okay? +2. What changed and why? +3. What needs my attention? +4. Who owes what, and for which expenses? + +Today there is no single page that answers the first three. The app opens on +Transactions and presents ten equally weighted navigation items. + +The recommended direction is: + +- Add an Overview as the default landing page. +- Keep Analytics focused on historical exploration: **what happened?** +- Rebuild Insights around decisions and attention: **what should I know or do?** +- Rebuild Shared around settlement contexts: **who owes what, and why?** +- Keep the shared loan as a separate ledger from shared consumption expenses. +- Fix calculation and coverage inconsistencies before adding more visualisations. + +## Context reviewed + +This review covered: + +- The current Next.js pages and shared components. +- Analytics SQL and API calculations. +- Shared-expense balance and transaction queries. +- `CLAUDE.md`. +- `docs/shared-expenses-design.md`. +- `docs/expense-baseline.md`. +- Recent repository history. +- Recent finance-app memories retrieved from OpenViking. + +The OpenViking history confirmed: + +- The July 19 redesign intentionally introduced the ink-and-copper theme, + Fraunces display type, month-spine navigation, top movers, category + sparklines, and heat-tinted ledger tables. +- The user prefers a modern, high-fidelity interface and actionable analytics. +- Later July 25–26 work changed the financial meaning under those screens: + split-aware personal spend, AUD-aware settlement, refund netting, loan + principal/interest separation, rule previews, and the proposed contextual + settlement model. +- The preferred settlement model links payments to real transactions, separates + Household, Trip, and Historical contexts, and keeps the shared loan separate. + +## What already works + +### Visual system + +- The dark ink-and-copper theme is coherent and distinctive. +- Serif headings and mono financial figures create useful hierarchy. +- The copper accent is used consistently for selection and emphasis. +- The design feels like one application rather than a collection of unrelated + pages. + +### Analytics interactions + +- The month spine is an effective year-at-a-glance navigation control. +- “What changed” is more useful than a generic category chart. +- Category sparklines make direction visible without creating a large + multi-series chart. +- Category rows can be expanded into their transactions. +- Inline recategorisation allows users to correct the data while investigating + it. + +### Shared workflow + +- “Owes you,” “you owe,” and “all square” communicate the immediate relationship + balance clearly. +- Payment history is preserved rather than reducing settlement to a boolean. +- Participant and tag filters support practical investigation. +- Split transactions can be edited without returning to the main transaction + page. + +## App-wide information architecture + +### Current problem + +The app redirects `/` to `/transactions`. This makes the operational ledger the +default product surface. Transactions are important, but they do not tell the +user whether anything needs attention or what the current financial position +means. + +The sidebar also gives equal weight to: + +- operational screens such as Reconcile; +- analytical screens such as Analytics; +- configuration screens such as Rules; +- organisational screens such as Tags. + +This makes the product feel like a database administration interface even when +the individual pages are well designed. + +The `/budget` route is labelled Analytics in navigation. This is a leftover from +an older product concept and should become `/analytics`. + +### Recommended navigation + +Group navigation by intent: + +**Overview** + +- Overview + +**Money** + +- Transactions +- Statements +- Reconcile + +**Understand** + +- Analytics +- Insights +- Merchants + +**Shared** + +- Shared +- Trips +- Loan + +**Organise** + +- Tags +- Rules + +Lower-frequency configuration items can be visually separated or collapsed. + +### Recommended Overview + +The default landing page should be a concise status and attention surface, not +another full analytics dashboard. + +Suggested structure: + +1. **This month** + - Personal spend to date + - Expected baseline at this point in the month + - Income + - Net cash + +2. **Financial resilience** + - Realistic monthly baseline + - Cash coverage in months + - Redraw shown separately from cash + +3. **Needs attention** + - Uncategorised or `other` transactions + - Unreconciled transactions + - Statements failing balance assertions + - New or unusual recurring charges + - Shared expenses added since the last settlement + +4. **Shared** + - Current balances by person and context + - Loan contribution shortfall shown separately + +5. **Recent change** + - The two or three categories that explain the largest movement + +The Overview should link into Analytics, Insights, Shared, and Reconcile rather +than reproduce their complete tables. + +## Analytics review + +### What the current page does + +The current Analytics page includes: + +- selected-month spend hero; +- twelve-month month spine; +- income, expenses, invested, and net-cash strip; +- top category movers; +- eight category sparkline cards; +- spend-concentration Pareto chart; +- cumulative spend pace; +- expandable category table; +- six-month heat-tinted category ledger. + +Each component is defensible in isolation. Together, they create too many +competing summaries of the same category data. + +### What Analytics should answer + +Analytics should answer: + +> What happened during this period, how does it compare, and what explains the +> difference? + +Recommended primary structure: + +1. Period and comparison controls. +2. Personal spend, income, invested, and net cash. +3. Explanation of the change versus the selected comparison. +4. One main category/trend visualisation. +5. Category breakdown with transaction drill-down. +6. An optional Explore section for detailed tables. + +### Recommended removals and consolidation + +- Keep either category sparklines or the six-month ledger as the primary + category-trend representation, not both. +- Move the Pareto chart behind an Explore section. It describes concentration + but rarely produces an immediate decision. +- Retain “What changed,” but make each item clickable and explain which + transactions caused the movement. +- Avoid comparing a partial current month with full prior months unless values + are projected or compared through the same day. +- Add gross-versus-personal-share switching only if it is clearly labelled. + Personal share should remain the default. + +### Calculation and trust issues + +#### Reconciled source rows can be double-counted + +`/api/analytics/monthly` does not currently exclude manual source rows where +`reconciled_with_id IS NOT NULL`. The baseline analysis identified 48 +double-counted rows. + +The analytics query should apply the same reconciled-row exclusion used by the +main transaction queries. + +#### Spend pace compares unlike numbers + +The Analytics headline uses: + +- split-adjusted personal share; +- fees and interest; +- refund and credit netting; +- loan interest rather than principal; +- non-spend-category exclusions. + +The cumulative spend-pace chart uses only `transaction_type === "debit"` and +adds gross `amount_aud ?? amount`. It does not use personal share and does not +apply the same refund, fee, interest, or loan semantics. + +The chart can therefore disagree with the headline while both appear to +represent “spend.” The cumulative series should be produced by the same +server-side spend semantics as the monthly total. + +#### Split coverage changes mid-series + +Reliable in-app split data begins on 2026-01-09. A trailing twelve-month personal +series currently combines older gross spending with newer split-adjusted +spending. + +Until historical splits are restored: + +- default personal trend analysis to February–June 2026; +- visibly mark periods with incomplete split coverage; or +- offer gross-only twelve-month comparison separately. + +Do not present the mixed series as one comparable personal-spend trend. + +#### Comparison baseline is too naive + +The selected month is compared against the average of all other months with +data. That average can include travel, annual fees, tax payments, incomplete +current periods, and months with incompatible split coverage. + +Better comparison choices: + +- previous month; +- same month last year; +- median of comparable complete months; +- recurring baseline; +- user-selected comparison. + +### Data trust indicator + +Analytics should include a compact methodology and coverage indicator: + +> Personal share · refunds netted · investments excluded · split coverage +> reliable from Feb 2026 · 12 transactions need classification + +This makes the meaning of the numbers inspectable without overwhelming the page. + +## Insights review + +### Current problem + +The current Insights page contains: + +- Regular versus occasional spending; +- another monthly category breakdown; +- recurring charges; +- fees and interest. + +The monthly breakdown duplicates Analytics. The page does not yet surface the +most decision-relevant findings already known from the data: sustainable monthly +cost, liquidity, the loan-overpayment lever, data-quality weaknesses, or unusual +changes requiring attention. + +### “Regular” is not the same as committed or essential + +`REGULAR_CATEGORIES` includes: + +- groceries; +- dining; +- transport; +- health; +- personal care; +- government; +- charity; +- pets. + +These may recur, but they have very different flexibility and obligation. +“Regular” describes transaction behaviour, not financial necessity. + +The current chart therefore cannot answer: + +- What is the minimum monthly cost? +- What can be cut? +- What is contractually committed? +- What is lifestyle spending? +- What is a one-off? + +### Recommended model + +Replace Regular versus Occasional with: + +1. **Fixed commitments** + - Contracted loan repayment + - Insurance + - Rates and registration + - Known annual fees + - Contractual subscriptions + +2. **Essential variable spending** + - Utilities + - Groceries + - Transport + - Health + +3. **Lifestyle and discretionary** + - Dining + - Shopping + - Entertainment + - Personal care + +4. **One-offs and travel** + +5. **Investments and transfers** + - Shown for cashflow context, excluded from spending + +This should support scenario views rather than claiming there is one true +baseline. + +### Recommended Insights structure + +#### 1. Financial baseline + +Show the scenarios already established by the expense-baseline analysis: + +- Survival: contracted loan repayment and essentials only. +- Realistic: contracted loan repayment plus ordinary dining and charity. +- Status quo: current loan overpayment and normal life excluding travel. + +For each scenario show: + +- monthly amount; +- six-month reserve; +- twelve-month reserve. + +#### 2. Liquidity and resilience + +Show: + +- cash available; +- redraw available separately; +- months covered under each baseline; +- a warning that redraw is lender-controlled and not equivalent to cash. + +#### 3. Biggest flexible levers + +Examples: + +- voluntary loan overpayment; +- dining; +- shopping; +- subscriptions; +- travel. + +The loan should always show both the contracted floor and actual repayment. + +#### 4. Attention and anomalies + +Examples: + +- a new recurring charge; +- a charge larger than its prior range; +- a category materially above baseline; +- a fee increase; +- an unexpected incoming credit categorised as spend; +- a merchant still classified as `other`; +- an investment incorrectly counted as spending. + +Each insight should link directly to the affected transactions. + +#### 5. Data-quality work queue + +The baseline analysis found that data quality is currently a larger blocker than +visualisation: + +- `other` remains a large unresolved category; +- Raiz, Vanguard Super, and moomoo need investment classification; +- incoming `other` credits can make spending negative; +- `government` conflates tax with rates and registration; +- annual fees distort short-window monthly averages. + +Insights should make these visible as fixable tasks. + +### Recurring charges + +The current detector identifies merchants with regular transaction intervals. +That does not necessarily mean a subscription or commitment. Weekly grocery +shopping can look recurring. + +Recommended changes: + +- Rename the section **Recurring patterns** unless contractual charges can be + distinguished. +- Show confidence and the basis for classification. +- Show the next expected charge date. +- Separate likely subscriptions from recurring merchants. +- Allow dismissing or confirming a detected pattern. +- Highlight price changes. +- Collapse inactive patterns by default. + +The current eight-column table is also too wide for a primary page. Put secondary +fields such as first seen, total paid, and count into an expandable detail row. + +### Fees and interest + +The current fees query aggregates statement summary values across all available +statements without a date filter. The UI does not label the period, so the total +looks like a current-period figure even though it is effectively lifetime to +date. + +Recommended presentation: + +- Explicit date range. +- Avoidable fees. +- Known annual fees. +- Credit-card interest. +- Loan interest. +- Change versus prior comparable period. +- Drill-down transactions. + +Loan interest should remain spending, but appear under fixed or +non-discretionary costs rather than being hidden. + +## Shared review + +### What the current page answers well + +The unfiltered balance cards correctly implement a running ledger: + +> splits minus payments + +This is coherent and should not be changed to exclude splits marked `settled`. +The `transaction_splits.settled` field is dead data and must not be used for new +UI claims. + +### What the current model cannot answer + +- Which split expenses did a payment settle? +- Is a particular trip settled? +- Can a trip be closed without closing Household? +- Is an imported offset-account credit already represented by a manual payment? +- What remains open inside one settlement context? +- How should the shared-loan contribution shortfall be shown? + +The page should not imply answers that the data model cannot support. + +### Tag-filtered balance cards are semantically misleading + +When a tag filter is active, participant balance queries intentionally stop +subtracting payments because payments are not attributable to a tag. The cards +then show raw split totals for the tag. + +This behavior is explained in small text, but the card still says “owes you” or +“you owe.” That looks like a real payable balance when it is not. + +When filtered, relabel the cards: + +> Split total in Europe 2026 + +Do not show payment or settlement actions from that state. + +### Recommended settlement-context design + +Use explicit settlement contexts: + +- Household +- Individual trips +- Historical / Pre-2026 +- Closed contexts + +Recommended Shared navigation: + +- All +- Household +- Trips +- Closed + +Within a context show: + +1. Net balance and direction. +2. Expenses added since the last settlement. +3. Payments attributed to that context. +4. A chronological activity ledger combining expenses and payments. +5. Context status: running, ready to settle, or closed. +6. Settlement action. + +### Payments should link to transactions + +An offset-account credit and a manual `split_payments` row can represent the same +money. The page should: + +- propose matching an imported credit to a settlement; +- display the linked transaction; +- prevent silent duplication; +- allow a manual payment only when no matching transaction exists. + +“Record Payment” should become a context-aware settlement flow: + +1. Choose what is being settled. +2. Match an existing incoming transaction where possible. +3. Confirm amount and residual balance. +4. Preserve an auditable history. + +### Shared transaction table + +The table currently shows raw `tx.amount` with a dollar sign and no currency +indicator. Participant balances correctly convert to AUD. + +For foreign transactions, show: + +- the native amount and currency; +- the AUD equivalent; +- splits based on the AUD settlement amount. + +This prevents a visible mismatch between transaction rows and participant +balances. + +### Shared loan + +The loan is not a shared-expense settlement context. It funds an asset rather +than consumption, and a loan contribution must never settle a dinner or utility +bill. + +Give it a separate page or clearly separated ledger showing: + +- expected contribution by period; +- actual contribution; +- running shortfall or receivable; +- principal reduction; +- interest expense; +- contracted repayment; +- actual repayment; +- voluntary overpayment; +- redraw movement. + +The partner obligation is a fixed 50% of the repayment schedule, not a percentage +inferred from actual contributions. + +## Responsive and interaction improvements + +- Replace wide eight-column primary tables with compact rows and expandable + details. +- Keep financial summaries readable at mobile widths without horizontal + scrolling. +- Add explicit loading skeletons rather than only text. +- Add error states for failed analytics requests. +- Ensure chart meaning is not conveyed by colour alone. +- Give interactive chart regions keyboard-accessible equivalents. +- Confirm material deletions, including payment-history deletion. +- Make expandable table rows use buttons with appropriate accessibility state. +- Use consistent labels for personal share, gross amount, native currency, and + AUD equivalent. + +## Recommended implementation order + +### Priority 0 — metric integrity + +1. Exclude reconciled source rows from monthly analytics. +2. Make spend pace use the same spend semantics as the headline. +3. Add date ranges to fees and interest. +4. Add split-coverage warnings to historical personal-share analysis. +5. Fix Shared foreign-currency presentation. +6. Avoid partial-month versus full-month comparisons. + +### Priority 1 — product hierarchy + +1. Add Overview and make it the default route. +2. Group sidebar navigation by user intent. +3. Rename `/budget` to `/analytics`. +4. Add consistent methodology and coverage indicators. + +### Priority 2 — Analytics and Insights + +1. Simplify Analytics around period, comparison, change explanation, trend, and + drill-down. +2. Remove the duplicate monthly breakdown from Insights. +3. Add baseline scenarios and liquidity coverage. +4. Add flexible-spending levers, anomalies, and a data-quality work queue. +5. Rework recurring patterns and fees into decision-oriented summaries. + +### Priority 3 — Shared + +1. Add settlement contexts. +2. Link payments to real transactions. +3. Add context activity ledgers and closeable trip contexts. +4. Introduce the separate loan contribution ledger. +5. Backfill historical closed-context splits so long-range personal analytics + become comparable. + +### Priority 4 — polish + +1. Improve mobile layouts. +2. Add accessibility semantics. +3. Add richer loading, error, and empty states. +4. Consolidate repeated card, table, filter, and page-header patterns into shared + components. + +## Proposed success criteria + +The redesign is successful when: + +- The first page explains current status and outstanding actions without opening + multiple screens. +- Analytics can explain why one comparable period differs from another. +- Insights identifies baseline cost, financial resilience, flexible levers, and + data-quality problems. +- Every displayed total states or clearly implies its period and whether it is + gross or personal share. +- Historical charts do not silently combine incompatible split coverage. +- Shared can distinguish Household, Trip, and Historical balances. +- A settlement can be traced to both the obligation it reduces and the real + transaction representing the payment. +- Loan contributions cannot affect ordinary shared-expense balances. + diff --git a/src/__tests__/fixtures/orders/real/ue-mixed.html b/src/__tests__/fixtures/orders/real/ue-mixed.html new file mode 100644 index 0000000..7f9885d --- /dev/null +++ b/src/__tests__/fixtures/orders/real/ue-mixed.html @@ -0,0 +1,250 @@ + +
\ No newline at end of file diff --git a/src/__tests__/unit/order-ingestion.test.ts b/src/__tests__/unit/order-ingestion.test.ts index c51c44d..a617d9a 100644 --- a/src/__tests__/unit/order-ingestion.test.ts +++ b/src/__tests__/unit/order-ingestion.test.ts @@ -164,3 +164,20 @@ describe("order_reference anchoring", () => { expect(p.flags).toContain("order_uuid_ambiguous"); }); }); + +describe("mixed Uber payment (issuer-named card leg)", () => { + it("captures both legs when the card is labelled by issuer, not brand", () => { + // Real receipt: Uber Cash $1.17 + Westpac ••••8032 $15.33 = $16.50. + // A brand allowlist (Visa|MasterCard|Amex) misses "Westpac" and drops the + // card half, leaving payments that do not account for the total. + const p = parseOrderHTML( + readFileSync(resolve(dir, "ue-mixed.html"), "utf-8"), + meta({ subject: "Your Friday morning order with Uber Eats", sender: "uber.com", receivedAt: "2026-01-09T09:26:44Z" }) + ); + expect(p.totals.total_charged).toBeCloseTo(16.50, 2); + expect(p.payment.credits_amount).toBeCloseTo(1.17, 2); + expect(p.payment.card_amount).toBeCloseTo(15.33, 2); + expect(p.payment.card_last4).toBe("8032"); + expect(validateOrderTotals(p).ok).toBe(true); + }); +}); diff --git a/src/lib/order-parse.ts b/src/lib/order-parse.ts index 8dec904..dfd9c8d 100644 --- a/src/lib/order-parse.ts +++ b/src/lib/order-parse.ts @@ -226,8 +226,14 @@ function parsePayment(platform: string, html: string, text: string): PaymentBrea const cash = text.match(/Uber Cash\s*(?:[A-Z]{3})?\s*\$?([\d,]+\.\d{2})/i); if (cash) out.credits_amount = money(cash[1]); + // Anchor on the masking, not on a list of card brands. Uber labels the card + // leg with whatever the issuer is called — "Westpac ••••8032 $15.33", + // "Mastercard ••••3893 (CBA Ultimate) CHF 51.23" — so a brand allowlist + // silently drops the card half of a mixed payment. Found in the backfill + // dry-run: Uber Cash $1.17 + Westpac ••••8032 $15.33 against a $16.50 total, + // which validateOrderTotals correctly refused rather than under-recording. const card = text.match( - /(?:Visa|MasterCard|American Express|Amex)[^\d]*(\d{4})[^\d]*(?:[A-Z]{3})?\s*\$?([\d,]+\.\d{2})/i + /(?:••••|\*{4}|\u2022{4})\s*(\d{4})[^\d]{0,40}?\$?\s*([\d,]+\.\d{2})/ ); if (card) { out.card_last4 = card[1];