You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PocketExpense+ — Production-Grade Offline-First Financial Tracking System
A full-stack financial tracking application featuring automated recurring transaction processing, category-based budgeting with threshold alerts, aggregation-driven analytics with anomaly detection, Android SMS-based automatic expense detection with 99% parsing accuracy, receipt scanning with OCR prefill, CSV/PDF export, an in-app notification feed, and rate-limited JWT-secured REST APIs — all built on an offline-first architecture.
Persistence: state is hydrated/persisted via persistMiddleware (src/store/persistMiddleware.ts) — persistence logic lives outside reducers (Redux middleware), keeping reducers pure and testable.
Create, read, update, delete expenses with full validation
Each expense has a localId for offline-first identification
syncStatus tracks whether an expense is synced, pending, or in conflict
New expenses are immediately added to Redux store and AsyncStorage
When online, pending queue is bulk-synced via POST /api/expenses/sync
Server returns localId → serverId mappings for reconciliation
NetInfo listener triggers automatic sync when connectivity is restored
Bulk delete — transactions screen has a selection mode; deleteExpenses(localIds) removes matching items from both items and pendingQueue, and offline deletes are tracked via tombstones so they propagate to the server
Users create monthly budgets per category (e.g., "Food: ₹5,000 for March 2026")
Compound unique index prevents duplicate budgets per user + category + month + year
Virtual fields compute percentageUsed and remainingAmount in real-time
Budget totalSpent is auto-recalculated on every expense create/update/delete
Recalculation uses MongoDB aggregation to sum expenses for the matching category/month/year
Frontend displays progress bars with color-coded over-budget warnings
Full CRUD: create, list (current month), update amount, delete
Budget Threshold Alerts (client-side, offline)
Pure threshold logic in src/services/budgetThresholds.ts (findCrossings) — unit-testable, no store/storage imports
budgetAlerts.ts (checkBudgets) evaluates the overall budget plus every category budget after any expense change and delivers configured local notifications
Thresholds: warning at ≥ 80% (configurable), exceeded at ≥ 100%
Alerts fire once per budget per threshold per month — a fired key set is persisted in AsyncStorage (overall:80, food:100, …) and reset automatically when the month rolls over
Feature 4 — Recurring Transactions Engine
Expenses can be marked as recurring with frequency: daily, weekly, or monthly
On creation, nextRunDate is computed based on frequency
node-cron runs hourly and finds all recurring expenses where nextRunDate ≤ now
For each due expense, the cron:
Creates a new expense entry (clone of the recurring template)
Updates nextRunDate to the next occurrence
Sets lastProcessedDate to current timestamp
Idempotency guard: lastProcessedDate prevents duplicate creation if cron fires twice
Recurring transactions also trigger budget recalculation
All computations use MongoDB aggregation pipelines (no in-memory processing)
Z-score formula: z = (value - mean) / stdDev
Anomalies include the raw amount, category, date, and z-score value
Home screen month stats are computed locally (src/utils/stats.ts) so they work offline and never disagree with the displayed total
Feature 7 — Receipt Scanning + OCR
Camera screen (app/expense/scan.tsx) uses expo-camera; on capture, the photo is passed into OCR and the parsed data prefills the Add Expense form
OCR architecture (src/services/receipt/ocr.ts): optional @react-native-ml-kit/text-recognition provider (fully offline, free — requires a custom dev build / EAS build; Expo Go falls back to attaching the photo and typing the amount). The layer isolates the provider so a cloud OCR can be swapped in by implementing one function
Heuristic parser (src/services/receipt/parseReceipt.ts): pure and synchronous
Extracts amount via weighted total labels (grand total > total payable > total > amount/paid), rejects subtotal/tax/discount/GSTIN/identifier lines
Extracts merchant (stopword-filtered) and date (from locale month names / numeric formats)
Returns confidence (0–1), candidateAmounts (largest first, for a picker), and guardrails (₹1 – ₹10,00,000)
Philosophy mirrors the SMS parser: prefer null over a confidently wrong number
On the add screen the receipt image is attached alongside the prefilled fields (amount, description, date, ocrConfidence)
An optional, privacy-first Android feature that automatically detects bank transactions from incoming SMS messages, extracts structured data locally on-device, and presents a confirmation modal before adding to expenses.
New: auto-add mode — when enabled, high-confidence transactions (≥ autoAddThreshold, default 0.9, deliberately stricter than the 0.75 that opens the sheet) are logged immediately without asking
Every auto-add is paired with a notification and an AutoAddToast with a 6-second Undo action (clearAutoAdded / deleteExpense) — logging without asking is only safe because reversing it is trivial
Auto-add state persists: autoAddEnabled, autoAddThreshold (clamped 0.5–1), autoAddCount; disabling SMS detection also disables auto-add
Reversing an auto-add (or any expense change) re-checks budget alerts
Rationale dialog explains: "Messages are parsed locally and never sent to any server"
If NEVER_ASK_AGAIN → alert with deep link to device Settings
On grant → listener starts, on deny → graceful fallback with explanation
Feature 9 — Export Transactions (CSV / PDF)
Export screen (app/export.tsx) with period presets: This month, Last month, Last 3 months, This year, All
CSV — RFC 4180 escaping (quotes values containing commas/quotes/newlines, doubles embedded quotes), expenses signed negative and income positive so the column sums to the balance, headers + trailing newline, resolved category/payment-method labels
PDF — rendered from an HTML template via expo-print (stylesheet-driven, bundle-safe), HTML-escaped so a description like <b>Lunch</b> cannot break the layout, with a summary block (total expense / income / balance / by-category table)
Both flows write to the cache directory and open the share sheet (expo-sharing), so files can be saved to Google Drive, Mail, Files, etc.
Testable design: all formatting logic lives in pure src/services/exportFormat.ts (no native modules imported), unit-tested in tests/export/exportFormat.test.ts
Local Notifications (src/services/notifications.ts)
expo-notifications with an explicit Android channel (budget-alerts, PRIVATE visibility, vibration pattern)
Expo Go guard: Expo Go (SDK 53+) drops remote notification support and expo-notifications logs errors on import, so the module is skipped entirely there and every call degrades to a graceful no-op
Prefs persisted in AsyncStorage (notificationPrefs): enabled, warnThreshold (default 80), notifyOnExceed, notifyOnAutoAdd — only requests permission when not already decided
In-App Notification Feed (notificationSlice)
Feed entries with kind: budget-warning, budget-exceeded, auto-added, info
Capped at 50 items so persisted storage cannot grow unbounded
Actions: addNotification, markRead, markAllRead, clearNotifications, hydrateNotifications; feed screen (app/notifications.tsx) with read/unread styling and mark-all-read
Alert Sources
Source
Where fired
Budget threshold crossed (80% / 100%)
budgetAlerts.checkBudgets() after expense changes
SMS auto-add logged
smsListener.autoAddTransaction()
Manual / system notices
info kind, via addNotification
Feature 11 — Theme System (Light / Dark)
Two palettes (src/theme/colors.ts): lightColors (violet glassmorphism) and darkColors — identical key sets so components never branch on mode
ThemeContext (ThemeProvider / useTheme): mode light | dark | system (follows OS via useColorScheme), choice persisted in AsyncStorage and restored on mount (isReady gate prevents a flash of the wrong scheme on startup)
makeStyles(colors => styles): builds a styles hook that defers StyleSheet.create into the component and memoizes per palette, so styles rebuild only when the scheme actually flips — made a runtime theme switch possible without tearing down the tree
Settings screen (app/settings/appearance.tsx) exposes Light / Dark / System; the tab bar, auth screens, shared components, and all feature screens are theme-aware
Feature 12 — Production Hardening
Input Validation (Joi)
Every route has a Joi schema for request body and/or query parameters
Validation middleware strips unknown fields and returns structured error messages
Separate validators for auth, expenses, and budgets
Error Handling
AppError class extends Error with statusCode and isOperational fields
All suites green: backend 96/96 — frontend 102/102.
Backend Tests (96 tests, 6 suites)
cd server
npm test# All tests
npm run test:unit # Service-level tests (48)
npm run test:integration # API-level tests (17)
npm run test:edge # Edge case tests (31)
npm run test:coverage # With coverage report
Layer
Tests
Coverage
Expense Service
15
91%
Budget Service
12
93%
Recurring Service
10
91%
Insight Service
11
91%
API Integration
17
100% routes
Edge Cases
31
Zero data, large volume, concurrency, float precision, timezone
Total
96
~82% overall
Frontend Tests (102 tests, 6 suites)
npm test# All frontend tests
npm run test:sms # SMS parser test suite only
Suite
Tests
Covers
tests/sms/smsParser.test.ts
42
normalizeMessage, sender detection, message classification, strict decimal parsing, field extraction, 200-sample full pipeline (accuracy ≥95%, FP ≤3%), edge cases
No SMS content logged or transmitted (Android feature)
SMS parsing is entirely local — raw message never leaves parser scope
CSV escaping prevents formula injection / column breakage in exports
HTML escaping prevents markup breaking PDFs generated from transaction data
Privacy & SMS Data Handling
This section documents the privacy architecture of the Android SMS detection feature.
Principles
Local-only parsing: All SMS parsing happens entirely on the user's device. No SMS content is transmitted to any server, ever.
No raw SMS storage: The raw SMS body is processed in-memory within the parseSms() function scope and immediately discarded. Only structured fields (amount, merchant name, type) are retained.
No SMS logging: Even in development mode, no SMS body content appears in logs. The parser returns only structured data. Debug logging is stripped in production builds.
User-controlled: The feature is disabled by default. Users must explicitly navigate to Settings → SMS Detection and toggle it on. A permission rationale is displayed before the Android permission dialog.
Revocable: Users can disable the feature at any time. Disabling clears all stored detection data including the deduplication cache and disables auto-add.
Minimal permissions: Only READ_SMS and RECEIVE_SMS are requested. No access to contacts, call logs, or other sensitive data.
Backend isolation: The backend API has no endpoint for receiving SMS data. The /api/expenses endpoint receives the same structured expense object regardless of whether it was manually entered or auto-detected.
Deduplication hashes: Transaction hashes stored in AsyncStorage contain only a numeric fingerprint derived from amount + merchant + type. The hash cannot be reversed to reconstruct the original SMS.
cd server
cp .env.example .env
# Edit .env with your MongoDB URI and JWT secret
npm install
npm run dev # Development with nodemon
Frontend
cd ..
npm install
npx expo start
Scan the QR code with Expo Go (Android) or the Camera app (iOS).
If your Mac and phone are on different networks:
npx expo start --tunnel
Run Tests
# Backend (96 tests)cd server && npm test# Frontend (102 tests)cd .. && npm test
Deployment
Backend — Render
Push to GitHub
Connect repo to Render
Set environment variables (see .env.example)
Deploy uses render.yaml config
Backend — Docker
cd server
docker build -t pocketexpense-api .
docker run -p 5001:5001 --env-file .env pocketexpense-api
Frontend — Expo EAS
npx eas build --platform android
npx eas build --platform ios
Build profiles defined in eas.json:
development — Debug build with dev client
preview — Internal distribution
production — Store-ready build
Note: receipt OCR (@react-native-ml-kit/text-recognition) requires a custom dev build (Expo Go falls back to photo attach + manual amount). SMS detection requires a development/production build on Android — it will not work in Expo Go.