|
1 | | -# Ledger Sync -- Backend |
2 | | - |
3 | | -FastAPI backend powering the Ledger Sync personal finance dashboard. Handles Excel import, transaction reconciliation, financial analytics, exchange rate proxying, user preferences, AI assistant configuration with encrypted key storage, and the Bedrock Converse proxy. |
4 | | - |
5 | | -## Features |
6 | | - |
7 | | -- OAuth authentication (Google, GitHub) with JWT tokens |
8 | | -- Excel file ingestion with duplicate detection |
9 | | -- SHA-256 based transaction reconciliation |
10 | | -- Financial analytics and calculations |
11 | | -- SQLite (dev) / PostgreSQL (prod) with SQLAlchemy ORM |
12 | | -- Alembic database migrations |
13 | | -- AI assistant config with AES-256-GCM encrypted API keys (PBKDF2 + per-ciphertext random salt) |
14 | | -- Bedrock Converse proxy via boto3 (SigV4 or Bedrock API-key auth, JSON response) |
15 | | - |
16 | | -## Tech Stack |
17 | | - |
18 | | -| Component | Technology | |
19 | | -| ---------- | -------------- | |
20 | | -| Language | Python 3.13+ | |
21 | | -| Framework | FastAPI | |
22 | | -| ORM | SQLAlchemy 2.0 | |
23 | | -| Database | SQLite (dev) / PostgreSQL (prod) | |
24 | | -| Migrations | Alembic | |
25 | | -| Testing | pytest | |
26 | | -| Linting | Ruff | |
27 | | -| Type Check | mypy | |
28 | | -| Packaging | uv | |
| 1 | +# Ledger Sync Backend |
| 2 | + |
| 3 | +FastAPI backend for OAuth authentication, JSON transaction ingestion, reconciliation, financial analytics, preferences, planning data, external rate proxies, and the AI assistant. |
| 4 | + |
| 5 | +## Stack |
| 6 | + |
| 7 | +| Area | Technology | |
| 8 | +| --- | --- | |
| 9 | +| Runtime | Python 3.13+ | |
| 10 | +| API | FastAPI | |
| 11 | +| ORM | SQLAlchemy 2 | |
| 12 | +| Validation | Pydantic 2 | |
| 13 | +| Migrations | Alembic | |
| 14 | +| Local database | SQLite | |
| 15 | +| Production database | Neon PostgreSQL 17 | |
| 16 | +| Serverless adapter | Mangum on Vercel | |
| 17 | +| Tests | pytest | |
| 18 | +| Quality | Ruff and mypy | |
| 19 | +| Packaging | uv | |
29 | 20 |
|
30 | 21 | ## Quick Start |
31 | 22 |
|
32 | 23 | ```bash |
33 | | -# Install dependencies (including dev tools) |
34 | 24 | uv sync --group dev |
35 | | - |
36 | | -# Initialize database |
37 | 25 | uv run alembic upgrade head |
38 | | - |
39 | | -# Start development server |
40 | 26 | uv run uvicorn ledger_sync.api.main:app --reload --port 8000 |
41 | 27 | ``` |
42 | 28 |
|
43 | | -Backend available at http://localhost:8000 |
44 | | - |
45 | | -## Project Structure |
46 | | - |
| 29 | +Local endpoints: |
| 30 | + |
| 31 | +- API: `http://localhost:8000` |
| 32 | +- Swagger UI: `http://localhost:8000/docs` |
| 33 | +- ReDoc: `http://localhost:8000/redoc` |
| 34 | +- Health: `http://localhost:8000/health` |
| 35 | +- Database health: `http://localhost:8000/health/db` |
| 36 | + |
| 37 | +## Responsibilities |
| 38 | + |
| 39 | +- Google and GitHub OAuth with 10-minute HMAC-signed state tokens. |
| 40 | +- JWT access and refresh tokens with server-side `token_version` invalidation. |
| 41 | +- Authenticated JSON ingestion at `/api/upload`. |
| 42 | +- User-scoped transaction reconciliation, tags, saved views, and categorization rules. |
| 43 | +- On-demand calculations plus precomputed analytics rollups. |
| 44 | +- Preferences, account classifications, budgets, goals, recurring items, and anomaly review. |
| 45 | +- Exchange-rate, instrument-rate, and stock-price proxies. |
| 46 | +- Fifteen read-only AI tools and AI usage accounting. |
| 47 | +- Bedrock Converse proxy for server-side Bedrock authentication. |
| 48 | + |
| 49 | +## Web Import Contract |
| 50 | + |
| 51 | +The browser parses Excel and CSV files. The backend receives: |
| 52 | + |
| 53 | +```json |
| 54 | +{ |
| 55 | + "file_name": "statement.xlsx", |
| 56 | + "file_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", |
| 57 | + "force": false, |
| 58 | + "rows": [ |
| 59 | + { |
| 60 | + "date": "2026-07-01", |
| 61 | + "amount": 85000, |
| 62 | + "currency": "INR", |
| 63 | + "type": "Income", |
| 64 | + "account": "HDFC Bank", |
| 65 | + "category": "Salary", |
| 66 | + "subcategory": "Monthly", |
| 67 | + "note": "July salary" |
| 68 | + } |
| 69 | + ] |
| 70 | +} |
47 | 71 | ``` |
48 | | -backend/ |
49 | | -├── src/ledger_sync/ |
50 | | -│ ├── api/ # FastAPI routers (one file per resource) |
51 | | -│ │ ├── main.py # Application entry point |
52 | | -│ │ ├── auth.py # Token refresh, logout, profile |
53 | | -│ │ ├── oauth.py # Google/GitHub OAuth login |
54 | | -│ │ ├── analytics.py # On-the-fly analytics |
55 | | -│ │ ├── analytics_v2.py # Pre-aggregated analytics |
56 | | -│ │ ├── calculations.py # Financial calculation endpoints |
57 | | -│ │ ├── preferences.py # User preferences (incl. AI config) |
58 | | -│ │ ├── ai_chat.py # Bedrock Converse proxy |
59 | | -│ │ ├── account_classifications.py |
60 | | -│ │ ├── exchange_rates.py, stock_price.py |
61 | | -│ │ └── meta.py, reports.py, transactions.py, upload.py |
62 | | -│ ├── core/ # Business logic |
63 | | -│ │ ├── reconciler.py # Transaction reconciliation |
64 | | -│ │ ├── calculator.py # Financial calculations |
65 | | -│ │ ├── analytics_engine.py # Heavy analytics computation |
66 | | -│ │ ├── _analytics_helpers.py # Module-level helpers for analytics_engine |
67 | | -│ │ ├── encryption.py # AES-256-GCM for API keys |
68 | | -│ │ ├── sync_engine.py # Upload orchestration |
69 | | -│ │ ├── query_helpers.py # Shared SQL aggregation helpers |
70 | | -│ │ ├── time_filter.py, insights.py, report_generator.py |
71 | | -│ │ └── auth/ # JWT token creation/verification |
72 | | -│ ├── db/ # Database layer |
73 | | -│ │ ├── models.py # 21-line facade that re-exports from _models/ |
74 | | -│ │ ├── _models/ # Split by bounded context |
75 | | -│ │ │ ├── __init__.py, _constants.py, enums.py |
76 | | -│ │ │ ├── user.py, transactions.py |
77 | | -│ │ │ └── investments.py, analytics.py, planning.py |
78 | | -│ │ ├── session.py # Database session |
79 | | -│ │ ├── base.py |
80 | | -│ │ └── migrations/versions/ # Alembic migrations |
81 | | -│ ├── schemas/ # Pydantic request/response models |
82 | | -│ ├── services/ # Cross-cutting services |
83 | | -│ ├── ingest/ # Data ingestion (CLI path only) |
84 | | -│ │ ├── excel_loader.py, csv_loader.py |
85 | | -│ │ ├── normalizer.py, validator.py, hash_id.py |
86 | | -│ ├── config/ # Configuration |
87 | | -│ │ └── settings.py |
88 | | -│ └── utils/ # Utilities |
89 | | -├── tests/ # Test suite |
90 | | -│ ├── unit/ # Unit tests |
91 | | -│ └── integration/ # Integration tests |
92 | | -└── pyproject.toml # Dependencies (uv) |
93 | | -``` |
94 | | - |
95 | | -## API Endpoints |
96 | | - |
97 | | -### Upload |
98 | 72 |
|
99 | | -| Method | Endpoint | Description | |
100 | | -| ------ | ------------- | ----------------- | |
101 | | -| POST | `/api/upload` | Upload Excel file | |
102 | | - |
103 | | -**Response includes:** |
104 | | - |
105 | | -- `processed` - Total rows processed |
106 | | -- `inserted` - New transactions |
107 | | -- `updated` - Modified transactions |
108 | | -- `deleted` - Soft-deleted transactions |
109 | | -- `unchanged` - Skipped (no changes) |
110 | | - |
111 | | -### Analytics |
| 73 | +Requirements: |
| 74 | + |
| 75 | +- JWT bearer authentication. |
| 76 | +- Exactly 64 hexadecimal characters in `file_hash`. |
| 77 | +- Between 1 and 100,000 rows. |
| 78 | +- Non-negative amounts and non-empty account and category values. |
| 79 | +- `force=true` only when intentionally reprocessing an already imported file. |
| 80 | + |
| 81 | +The CLI still supports direct Excel and CSV loading through `SyncEngine.import_file()`. |
| 82 | + |
| 83 | +## Source Layout |
| 84 | + |
| 85 | +```text |
| 86 | +src/ledger_sync/ |
| 87 | + api/ FastAPI routers and dependencies |
| 88 | + analytics_v2_impl/ Split analytics v2 routers |
| 89 | + ai_tools_impl/ AI tool registry and implementations |
| 90 | + core/ Business rules and calculations |
| 91 | + analytics/ Analytics engine mixins and rollup builders |
| 92 | + auth/ JWT helpers |
| 93 | + db/ |
| 94 | + _models/ SQLAlchemy models by domain |
| 95 | + migrations/ Alembic environment and revisions |
| 96 | + models.py Public model facade |
| 97 | + session.py Engine and session configuration |
| 98 | + ingest/ CLI loaders, normalization, validation, hashing |
| 99 | + schemas/ Pydantic request and response models |
| 100 | + services/ Cross-cutting services |
| 101 | + config/ Runtime settings |
| 102 | + utils/ Logging and helpers |
| 103 | +``` |
112 | 104 |
|
113 | | -| Method | Endpoint | Description | |
114 | | -| ------ | ------------------------- | -------------------------- | |
115 | | -| GET | `/api/analytics/overview` | Financial overview | |
116 | | -| GET | `/api/analytics/kpis` | Key performance indicators | |
117 | | -| GET | `/api/analytics/trends` | Financial trends | |
| 105 | +`core/analytics_engine.py` is a compatibility facade. The active analytics implementation lives under `core/analytics/`. |
118 | 106 |
|
119 | | -### Calculations |
| 107 | +## Security |
120 | 108 |
|
121 | | -- `GET /api/calculations/totals` - Income/expense totals |
122 | | -- `GET /api/calculations/monthly-aggregation` - Monthly data |
123 | | -- `GET /api/calculations/category-breakdown` - Category analysis |
124 | | -- `GET /api/calculations/insights` - Financial insights |
| 109 | +- All financial queries and mutations are scoped by `user_id`. |
| 110 | +- OAuth callbacks require a valid, unexpired signed state. |
| 111 | +- Logout increments `token_version`, invalidating outstanding access and refresh tokens. |
| 112 | +- API responses include security headers and authenticated data uses `Cache-Control: no-store`. |
| 113 | +- CORS uses an explicit allowlist. |
| 114 | +- Upload is limited to 10 requests per user per minute and 50 per IP per minute. |
| 115 | +- Bedrock chat is limited to 30 requests per user per minute and 60 per IP per minute. |
| 116 | +- Current BYOK ciphertexts use AES-256-GCM with HKDF-SHA256 and `LEDGER_SYNC_ENCRYPTION_KEY`. |
| 117 | +- Legacy PBKDF2 ciphertexts are read-only compatibility data and are upgraded when revealed. |
125 | 118 |
|
126 | | -## Development |
| 119 | +## Configuration |
127 | 120 |
|
128 | | -### Running Tests |
| 121 | +Environment variables use their full `LEDGER_SYNC_` names. |
| 122 | + |
| 123 | +| Variable | Default | Purpose | |
| 124 | +| --- | --- | --- | |
| 125 | +| `LEDGER_SYNC_ENVIRONMENT` | `development` | Runtime mode | |
| 126 | +| `LEDGER_SYNC_DATABASE_URL` | `sqlite:///./ledger_sync.db` | SQLAlchemy database URL | |
| 127 | +| `LEDGER_SYNC_DATABASE_ECHO` | `false` | SQL logging | |
| 128 | +| `LEDGER_SYNC_LOG_LEVEL` | `INFO` | Application log level | |
| 129 | +| `LEDGER_SYNC_FRONTEND_URL` | `http://localhost:5173` | OAuth callback base | |
| 130 | +| `LEDGER_SYNC_CORS_ORIGINS` | Local origins | JSON allowlist | |
| 131 | +| `LEDGER_SYNC_JWT_SECRET_KEY` | Generated in development | JWT and OAuth state signing | |
| 132 | +| `LEDGER_SYNC_ENCRYPTION_KEY` | JWT key fallback | Dedicated BYOK encryption key | |
| 133 | +| `LEDGER_SYNC_GOOGLE_CLIENT_ID` | Empty | Enable Google OAuth | |
| 134 | +| `LEDGER_SYNC_GOOGLE_CLIENT_SECRET` | Empty | Google OAuth secret | |
| 135 | +| `LEDGER_SYNC_GITHUB_CLIENT_ID` | Empty | Enable GitHub OAuth | |
| 136 | +| `LEDGER_SYNC_GITHUB_CLIENT_SECRET` | Empty | GitHub OAuth secret | |
| 137 | +| `LEDGER_SYNC_BEDROCK_API_KEY` | Empty | App-mode Bedrock credential | |
| 138 | +| `LEDGER_SYNC_DB_POOL_SIZE` | `5` | PostgreSQL pool size | |
| 139 | +| `LEDGER_SYNC_DB_MAX_OVERFLOW` | `3` | PostgreSQL overflow connections | |
| 140 | + |
| 141 | +See [.env.example](../.env.example) for the complete template. |
| 142 | + |
| 143 | +## Quality Checks |
129 | 144 |
|
130 | 145 | ```bash |
131 | | -# Run all tests |
| 146 | +uv run ruff check src/ tests/ |
| 147 | +uv run ruff format --check src/ tests/ |
| 148 | +uv run mypy src/ |
132 | 149 | uv run pytest tests/ -v |
133 | | - |
134 | | -# Run with coverage |
135 | | -uv run pytest --cov=ledger_sync tests/ |
136 | | - |
137 | | -# Run specific test file |
138 | | -uv run pytest tests/unit/test_hash_id.py -v |
139 | 150 | ``` |
140 | 151 |
|
141 | | -### Linting & Type Checking |
142 | | - |
143 | | -```bash |
144 | | -# Lint |
145 | | -uv run ruff check . |
146 | | - |
147 | | -# Auto-fix lint issues |
148 | | -uv run ruff check --fix . |
149 | | - |
150 | | -# Type check |
151 | | -uv run mypy src/ |
152 | | -``` |
| 152 | +The current backend suite contains 328 tests. |
153 | 153 |
|
154 | | -### Database Migrations |
| 154 | +## Migrations |
155 | 155 |
|
156 | 156 | ```bash |
157 | | -# Apply migrations |
| 157 | +uv run alembic revision --autogenerate -m "describe change" |
158 | 158 | uv run alembic upgrade head |
159 | | - |
160 | | -# Create new migration |
161 | | -uv run alembic revision --autogenerate -m "description" |
162 | | - |
163 | | -# Rollback one migration |
164 | | -uv run alembic downgrade -1 |
165 | 159 | ``` |
166 | 160 |
|
167 | | -## API Documentation |
168 | | - |
169 | | -- Interactive docs: http://localhost:8000/docs |
170 | | -- ReDoc: http://localhost:8000/redoc |
171 | | - |
172 | | -## Configuration |
173 | | - |
174 | | -Environment variables (prefix: `LEDGER_SYNC_`): |
| 161 | +Read [MIGRATION_NOTES.md](src/ledger_sync/db/migrations/MIGRATION_NOTES.md) before attempting a downgrade. Several recent revisions intentionally have no schema-reversing downgrade. |
175 | 162 |
|
176 | | -| Variable | Default | Description | |
177 | | -|----------|---------|-------------| |
178 | | -| `DATABASE_URL` | `sqlite:///./ledger_sync.db` | Database connection string | |
179 | | -| `LOG_LEVEL` | `INFO` | Python log level | |
180 | | -| `GOOGLE_CLIENT_ID` | - | Google OAuth client ID | |
181 | | -| `GOOGLE_CLIENT_SECRET` | - | Google OAuth client secret | |
182 | | -| `GITHUB_CLIENT_ID` | - | GitHub OAuth client ID | |
183 | | -| `GITHUB_CLIENT_SECRET` | - | GitHub OAuth client secret | |
184 | | -| `FRONTEND_URL` | `http://localhost:5173` | Frontend URL for OAuth redirects | |
185 | | -| `JWT_SECRET_KEY` | dev default | JWT signing secret (required in production) | |
186 | | -| `ENVIRONMENT` | `development` | `development` or `production` | |
| 163 | +## Deployment |
187 | 164 |
|
188 | | -## License |
| 165 | +Production runs on Vercel through `api/index.py` and `vercel.json`, with Neon PostgreSQL behind the PgBouncer pooler. Database migrations are applied by `.github/workflows/migrate.yml`. |
189 | 166 |
|
190 | | -MIT |
| 167 | +See [docs/DEPLOYMENT.md](../docs/DEPLOYMENT.md) and [docs/API.md](../docs/API.md). |
0 commit comments