Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 2026-04-05 — MoneyMirror Phase 2 Linear + project-state

**What:** Recorded MoneyMirror Phase 2 (dashboard roadmap) delivery in **Linear** and refreshed [project-state.md](project-state.md).

**Linear (under [VIJ-11](https://linear.app/vijaypmworkspace/issue/VIJ-11/issue-009-moneymirror-ai-powered-personal-finance-coach-for-gen-z)):**

- [VIJ-23](https://linear.app/vijaypmworkspace/issue/VIJ-23/moneymirror-phase-2-shipped-dashboard-roadmap-a-h-baseline) — **Done** — Phase 2 shipped in repo (epics A–H baseline).
- [VIJ-24](https://linear.app/vijaypmworkspace/issue/VIJ-24/moneymirror-ops-run-neon-alter-for-statement-label-columns-nickname) — **Todo** — run Neon `ALTER TABLE` for label columns if missing (`apps/money-mirror/schema.sql`).
- [VIJ-25](https://linear.app/vijaypmworkspace/issue/VIJ-25/moneymirror-backlog-post-roadmap-f3-g2g3-h3) — **Backlog** — F3, G2–G3, H3 follow-ups.

**Repo:** [experiments/linear-sync/issue-009.json](experiments/linear-sync/issue-009.json) — `tasks` map + `last_sync_mode` updated for manual Phase 2 issue creation (not a full `/linear-sync` pipeline run).

**Update (same day):** Full **Sprint 1–3** (VIJ-26–28, Done), **Sprint 4 / Backlog** (VIJ-25), **Epics A–H** (VIJ-29–36, Done) created in Linear and mirrored in [project-state.md](project-state.md) § MoneyMirror PM roadmap — Linear map.

---

## 2026-04-04 — GitHub PR #15 + project state (repo hygiene)

**What:** Pushed `feat/linear-workflow-sync` (commits `9f483ed`, `dced451`) and opened [**PR #15**](https://github.com/shadowdevcode/ai-product-os/pull/15) for review: Neon MCP secret removal, `.codex/config.toml`, CHANGELOG updates.
Expand Down
36 changes: 20 additions & 16 deletions apps/money-mirror/CODEBASE-CONTEXT.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# Codebase Context: MoneyMirror

Last updated: 2026-04-03
Last updated: 2026-04-04

## What This App Does

MoneyMirror is a mobile-first PWA AI financial coach for Gen Z Indians (₹20K–₹80K/month). Users sign in with Neon Auth email OTP, upload a password-free Indian bank-account or credit-card statement PDF, and Gemini 2.5 Flash parses and categorizes each transaction into needs/wants/investment/debt/other. The "Mirror moment" reveals the gap between self-reported spend from onboarding and actual spend from the statement. An advisory engine fires up to 5 consequence-first nudges, and a weekly recap email is sent by a Vercel cron fan-out every Monday at 8:00 AM IST. The primary North Star proxy is second-month statement upload rate (≥60%).

## Architecture Overview

- **Frontend**: Next.js 16 App Router (RSC by default, `"use client"` for interactive panels). Key pages: `/` (landing), `/onboarding` (5-question flow), `/score` (Money Health Score reveal), `/dashboard` (Mirror + advisory feed + upload).
- **Frontend**: Next.js 16 App Router (RSC by default, `"use client"` for interactive panels). Key pages: `/` (landing), `/onboarding` (5-question flow), `/score` (Money Health Score reveal), `/dashboard` (Overview with perceived vs actual + categories, **Insights** tab for AI nudges, **Upload** tab, statement picker + month filter, URL `?statement_id=`).
- **Backend**: Next.js API routes under `src/app/api/`. Neon Auth for session auth, Neon Postgres for persistence, Gemini 2.5 Flash for PDF parse + categorization, Resend for weekly recap emails, PostHog for server-side telemetry.
- **Database**: Neon Postgres. 4 tables: `profiles`, `statements`, `transactions`, `advisory_feed`. `profiles` persists monthly income and perceived spend; `statements` now tracks `institution_name`, `statement_type`, and optional credit-card due metadata. All monetary values are stored as `BIGINT` in paisa (₹ × 100) to avoid float precision errors.
- **Database**: Neon Postgres. 4 tables: `profiles`, `statements`, `transactions`, `advisory_feed`. `profiles` persists monthly income and perceived spend; `statements` tracks `institution_name`, `statement_type`, optional credit-card due metadata, optional `nickname`, `account_purpose`, `card_network` for multi-account labelling. All monetary values are stored as `BIGINT` in paisa (₹ × 100) to avoid float precision errors.
- **AI Integration**: Gemini 2.5 Flash via `@google/genai`. Used for: (1) PDF text → structured bank-account or credit-card statement JSON, (2) transaction category normalization. The statement-parse route currently enforces a 25s timeout and returns JSON 504 on timeout.
- **Analytics**: PostHog (server-side only, `posthog-node`). 10 events tracked: `onboarding_completed`, `statement_parse_started/rate_limited/success/timeout/failed`, `weekly_recap_triggered/completed`, `weekly_recap_email_sent/failed`. All calls fire-and-forget (`.catch(() => {})`).
- **Error tracking**: Sentry via `@sentry/nextjs` (`sentry.server.config.ts`, `sentry.edge.config.ts`, `src/instrumentation.ts`, `src/instrumentation-client.ts`). Uses `NEXT_PUBLIC_SENTRY_DSN` plus org/project/auth token vars as in app `README.md` / `.env.local.example`.

## Key Files

Expand All @@ -24,7 +25,9 @@ MoneyMirror is a mobile-first PWA AI financial coach for Gen Z Indians (₹20K
| `src/app/api/dashboard/advisories/route.ts` | Authenticated GET — returns advisory_feed rows for the current user via the active Neon session cookie. |
| `src/app/api/cron/weekly-recap/route.ts` | Master cron: scheduled GET entrypoint for Vercel Cron; accepts Bearer `CRON_SECRET` or local `x-cron-secret`, paginates users in 1000-row batches, and fans out to the worker via Promise.allSettled. |
| `src/app/api/cron/weekly-recap/worker/route.ts` | Worker: sends Resend email per user. Returns HTTP 502 on failure so master counts it correctly. |
| `src/lib/advisory-engine.ts` | Fires 5 advisory types based on spend ratios and thresholds. Writes to `advisory_feed` table. |
| `src/lib/advisory-engine.ts` | Rule-based advisories (perception gap, subscriptions, food, no investment, debt ratio, high Other bucket, discretionary mix, avoidable estimate, CC minimum-due risk). |
| `src/lib/format-date.ts` | UTC-safe date labels for statement periods (no raw ISO in UI). |
| `src/app/api/statements/route.ts` | Authenticated GET — list processed statements for picker + month filter. |
| `src/lib/scoring.ts` | Computes Money Health Score (0–100) from 5 onboarding question responses. |
| `src/lib/statements.ts` | Defines statement types, parser prompts, metadata validation, and shared display labels for bank-account and credit-card uploads. |
| `src/lib/pdf-parser.ts` | Extracts raw text from PDF buffer using `pdf-parse`. Uses `result.total` (not `result.pages?.length`) for page count — v2 API. |
Expand All @@ -33,21 +36,22 @@ MoneyMirror is a mobile-first PWA AI financial coach for Gen Z Indians (₹20K
## Data Model

- **profiles**: One row per user. `id` = Neon Auth user id (TEXT). Stores `monthly_income_paisa`, `perceived_spend_paisa`, `target_savings_rate`, `money_health_score`.
- **statements**: One per uploaded PDF. Tracks `institution_name`, `statement_type` (`bank_account` or `credit_card`), statement period, optional card due metadata, and `status`. Status never set to `processed` before `transactions` child insert succeeds.
- **statements**: One per uploaded PDF. Tracks `institution_name`, `statement_type` (`bank_account` or `credit_card`), statement period, optional card due metadata, optional `nickname` / `account_purpose` / `card_network`, and `status`. Status never set to `processed` before `transactions` child insert succeeds.
- **transactions**: Many per statement. All amounts in paisa (BIGINT). `category` CHECK: `needs | wants | investment | debt | other` (lowercase).
- **advisory_feed**: Advisory nudges generated per statement. `trigger` identifies which advisory type fired.

## API Endpoints

| Method | Path | Auth | Purpose |
| ------ | ------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| POST | `/api/statement/parse` | Neon session cookie | Upload PDF plus `statement_type`, parse with Gemini, persist to DB, return mirror data |
| GET | `/api/dashboard` | Neon session cookie | Rehydrate latest processed statement + advisory feed (refresh/deep link path) |
| GET | `/api/dashboard/advisories` | Neon session cookie | Fetch advisory_feed rows for user |
| POST | `/api/onboarding/complete` | Neon session cookie | Save onboarding income, score, and perceived spend to profiles |
| GET | `/api/cron/weekly-recap` | `authorization: Bearer <CRON_SECRET>` or local `x-cron-secret` | Scheduled master fan-out |
| POST | `/api/cron/weekly-recap/worker` | `x-cron-secret` header | Worker: send one recap email; returns 502 on failure |
| ALL | `/api/auth/[...path]` | — | Neon Auth passthrough |
| Method | Path | Auth | Purpose |
| ------ | ------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| POST | `/api/statement/parse` | Neon session cookie | Upload PDF plus `statement_type` and optional `nickname`, `account_purpose`, `card_network` |
| GET | `/api/dashboard` | Neon session cookie | Rehydrate processed statement + advisories; optional `?statement_id=` for a specific upload |
| GET | `/api/statements` | Neon session cookie | List all processed statements for the user |
| GET | `/api/dashboard/advisories` | Neon session cookie | Fetch advisory_feed rows for user |
| POST | `/api/onboarding/complete` | Neon session cookie | Save onboarding income, score, and perceived spend to profiles |
| GET | `/api/cron/weekly-recap` | `authorization: Bearer <CRON_SECRET>` or local `x-cron-secret` | Scheduled master fan-out |
| POST | `/api/cron/weekly-recap/worker` | `x-cron-secret` header | Worker: send one recap email; returns 502 on failure |
| ALL | `/api/auth/[...path]` | — | Neon Auth passthrough |

## Things NOT to Change Without Reading First

Expand All @@ -61,11 +65,11 @@ MoneyMirror is a mobile-first PWA AI financial coach for Gen Z Indians (₹20K

## Known Limitations

- Statement history browsing not yet implemented — dashboard always shows the latest processed statement. `GET /api/dashboard` accepts `?statement_id=` as a future extension point.
- Cross-month trend comparison and aggregated “all accounts” rollups are not implemented (single-statement view with picker only).
- Password removal stays manual outside the app. Password-protected PDFs are rejected with a clear retry message.
- Inbox ingestion from email is not implemented. Users must manually download the PDF and upload it.
- PDF parsing reliability depends on the PDF being text-based (not scanned/image). Scanned PDFs return 400.
- Rate limit for uploads is 3/day per user (in-memory, resets on server restart) — not durable across deployments.
- Weekly recap email only triggers if the user has at least one processed statement. New users without statements are silently skipped.
- Share button (`navigator.share`) is hidden on desktop browsers — only rendered when Web Share API is available.
- Current automated validation count is 45 tests across route and library coverage.
- Run `npm test` in `apps/money-mirror` for current library and API test counts.
7 changes: 5 additions & 2 deletions apps/money-mirror/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ AI-powered personal finance coach for Gen Z Indians that uses Neon Auth plus Neo

**Sign in with your email, upload a statement, and get a brutally honest view of where your money actually goes.**

For AI- and agent-oriented architecture, data model, and API reference, see [`CODEBASE-CONTEXT.md`](./CODEBASE-CONTEXT.md).

## What it does

1. User signs in with Neon Auth email OTP and lands in a private onboarding flow.
Expand All @@ -22,6 +24,7 @@ AI-powered personal finance coach for Gen Z Indians that uses Neon Auth plus Neo
| Database | Neon Postgres (`@neondatabase/serverless`) |
| AI | Google Gemini 2.5 Flash |
| Analytics | PostHog (`posthog-node`) |
| Errors | Sentry (`@sentry/nextjs`) |
| Email | Resend |
| Hosting | Vercel |

Expand Down Expand Up @@ -70,7 +73,7 @@ Fill in these values:

### 4. Apply database schema

Run the full contents of [`schema.sql`](/Users/vijaysehgal/Downloads/02-Portfolio/ai-product-os/apps/money-mirror/schema.sql) against your Neon database.
Run the full contents of [`schema.sql`](./schema.sql) against your Neon database.

Tables created:

Expand Down Expand Up @@ -170,7 +173,7 @@ Returns the advisory subset for the authenticated user and statement.

### `GET /api/cron/weekly-recap`

Fan-out master route that finds all users with processed statements and triggers worker jobs. This is the scheduled entrypoint configured in [`vercel.json`](/Users/vijaysehgal/Downloads/02-Portfolio/ai-product-os/apps/money-mirror/vercel.json).
Fan-out master route that finds all users with processed statements and triggers worker jobs. This is the scheduled entrypoint configured in [`vercel.json`](./vercel.json).

**Auth**: `authorization: Bearer <CRON_SECRET>` from Vercel Cron. Local/manual triggering may also use `x-cron-secret: <CRON_SECRET>`.

Expand Down
39 changes: 39 additions & 0 deletions apps/money-mirror/__tests__/lib/format-date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import {
formatMonthKeyForDisplay,
formatPeriodRange,
formatStatementDate,
monthKeyFromPeriodEnd,
statementMonthLabel,
} from '@/lib/format-date';

describe('format-date', () => {
it('formats ISO date strings without raw T00:00:00', () => {
expect(formatStatementDate('2026-04-11T00:00:00.000Z')).toMatch(/11/);
expect(formatStatementDate('2026-04-11T00:00:00.000Z')).toMatch(/2026/);
expect(formatStatementDate('2026-04-11T00:00:00.000Z')).not.toContain('T');
});

it('formats YYYY-MM-DD', () => {
expect(formatStatementDate('2026-03-22')).toMatch(/22/);
});

it('formatPeriodRange joins start and end', () => {
const r = formatPeriodRange('2026-02-23', '2026-03-22');
expect(r).toContain('–');
expect(r).not.toContain('T');
});

it('statementMonthLabel uses period end', () => {
expect(statementMonthLabel('2026-03-22')).toMatch(/March/);
expect(statementMonthLabel('2026-03-22')).toMatch(/2026/);
});

it('monthKeyFromPeriodEnd', () => {
expect(monthKeyFromPeriodEnd('2026-03-22')).toBe('2026-03');
});

it('formatMonthKeyForDisplay', () => {
expect(formatMonthKeyForDisplay('2026-03')).toMatch(/March/);
});
});
29 changes: 29 additions & 0 deletions apps/money-mirror/docs/COACHING-TONE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# MoneyMirror coaching tone

Educational, India-first personal finance copy. Used for in-app insights and any future LLM prompts.

## Principles

1. **Clarity over jargon** — Explain the “why” in one sentence before the number.
2. **No shame** — Frame behaviour as common and fixable; avoid moralising.
3. **Consequence-first** — Tie spend to a concrete tradeoff (e.g. annualised food delivery, EMI load).
4. **Not advice** — Insights are observations from statement data, not buy/sell or tax guidance.

## Disclaimers (always available in UI)

- MoneyMirror does not provide personalised investment, tax, or legal advice.
- Users should consult a qualified professional for regulated decisions.
- Do **not** impersonate real individuals or creators; use archetypes (educator, myth-buster) if needed, not names.

## Prompt templates (if using Gemini for narrative)

- Structure: **Observation** → **Why it matters** → **One practical next step** (optional).
- Append: “This is general information based on your uploaded statement, not a recommendation.”

## Archetypes (optional user preference)

- **Educator** — Neutral, step-by-step.
- **Myth-buster** — Short reframe of a common money myth tied to their data.
- **Long-term** — Emphasise compounding and small recurring cuts.

Do not label these after real influencers or YouTubers.
8 changes: 8 additions & 0 deletions apps/money-mirror/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ CREATE TABLE IF NOT EXISTS public.statements (
minimum_due_paisa BIGINT,
credit_limit_paisa BIGINT,
status TEXT NOT NULL CHECK (status IN ('processing', 'processed', 'failed')) DEFAULT 'processing',
nickname TEXT,
account_purpose TEXT,
card_network TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Expand Down Expand Up @@ -66,3 +69,8 @@ CREATE INDEX IF NOT EXISTS idx_transactions_user_statement

CREATE INDEX IF NOT EXISTS idx_advisory_feed_user_created_at
ON public.advisory_feed(user_id, created_at DESC);

-- Existing Neon DBs: add statement label columns (idempotent)
ALTER TABLE public.statements ADD COLUMN IF NOT EXISTS nickname TEXT;
ALTER TABLE public.statements ADD COLUMN IF NOT EXISTS account_purpose TEXT;
ALTER TABLE public.statements ADD COLUMN IF NOT EXISTS card_network TEXT;
Loading
Loading