diff --git a/CLAUDE.md b/CLAUDE.md index 851181f..660d65d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,9 +13,33 @@ Personal finance management web app. cd frontend && pnpm install && pnpm run dev ``` +## Local Docker + +```bash +# Backend + DB containers +docker exec wealthysmart-db-dev psql -U wealthy_user -d wealthysmart -c 'SQL;' +``` + ## Deployment - Deployed via Gitea Actions (self-hosted runner on VPS) - Push to `main` triggers: GitHub → webhook → Gitea mirror sync → Actions workflow → Docker build & deploy - Domain: wealth.cescalante.dev - Reverse proxy: nginx-proxy + acme-companion (auto TLS) + +## Infrastructure + +- **Single server**: `ssh old-vps` — runs everything (WealthySmart, n8n, Forgejo, Vaultwarden, nginx-proxy) +- `ssh production` is **NOT valid** — do not use +- n8n UI: https://n8n.cescalante.dev — n8n DB queryable via `docker exec portfolio-db-prod psql -U portfolio_user -d n8n` + +## n8n Flows + +Four automated flows on old-vps feed data into WealthySmart: + +1. **BAC Credit Card** — Gmail trigger → POST /transactions/ +2. **Salary Deposits** — Gmail trigger → POST /transactions/ +3. **Municipal Receipts** — Cron trigger → POST /municipal-receipts/upload +4. **Pension PDFs** (`e88c3UhBeo9WCbcy`) — Gmail trigger (daily midnight) → POST /pensions/upload + +Flow export: `docs/WealthySmart_ BAC Pensions Statements parser.json` diff --git a/backend/app/api/v1/endpoints/transactions.py b/backend/app/api/v1/endpoints/transactions.py index 82a32a8..0bdd334 100644 --- a/backend/app/api/v1/endpoints/transactions.py +++ b/backend/app/api/v1/endpoints/transactions.py @@ -194,7 +194,8 @@ def create_transaction( session.refresh(tx) # Send push notification - symbol = "₡" if tx.currency == Currency.CRC else tx.currency.value + symbols = {Currency.CRC: "₡", Currency.USD: "$", Currency.EUR: "€"} + symbol = symbols.get(tx.currency, tx.currency.value) amount_str = f"{symbol}{tx.amount:,.0f}" if tx.currency == Currency.CRC else f"{symbol}{tx.amount:,.2f}" is_deposit = tx.transaction_type == TransactionType.DEPOSITO send_push_to_all( diff --git a/backend/app/db.py b/backend/app/db.py index 2eb4ae8..d3c7525 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -23,6 +23,12 @@ def run_migrations(): except Exception: conn.rollback() + try: + conn.execute(text("ALTER TYPE currency ADD VALUE IF NOT EXISTS 'EUR'")) + conn.commit() + except Exception: + conn.rollback() + def get_session(): with Session(engine) as session: diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 02165be..5d5edb4 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -35,6 +35,7 @@ class TransactionSource(str, enum.Enum): class Currency(str, enum.Enum): CRC = "CRC" USD = "USD" + EUR = "EUR" BTC = "BTC" XMR = "XMR" diff --git a/docs/WealthySmart_ BAC Pensions Statements parser.json b/docs/WealthySmart_ BAC Pensions Statements parser.json new file mode 100644 index 0000000..cda34a8 --- /dev/null +++ b/docs/WealthySmart_ BAC Pensions Statements parser.json @@ -0,0 +1,97 @@ +{ + "name": "WealthySmart: BAC Pensions Statements parser", + "nodes": [ + { + "parameters": { + "pollTimes": { + "item": [ + { + "hour": 0 + } + ] + }, + "simple": false, + "filters": { + "q": "Estado de cuenta Pensiones BAC CREDOMATIC -{tarjeta} " + }, + "options": { + "downloadAttachments": true + } + }, + "type": "n8n-nodes-base.gmailTrigger", + "typeVersion": 1.3, + "position": [ + 0, + 0 + ], + "id": "bc94143f-9980-4b94-9cd7-8e206dc1232e", + "name": "Gmail Trigger", + "credentials": { + "gmailOAuth2": { + "id": "LkAGMVgAqsiCkMFX", + "name": "Gmail account" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://wealth.cescalante.dev/api/v1/pensions/upload", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "parameterType": "formBinaryData", + "name": "files", + "inputDataFieldName": "attachment_0" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 208, + 0 + ], + "id": "b73c0182-8d56-48b3-b37e-c4dd9361a062", + "name": "HTTP Request", + "credentials": { + "httpBearerAuth": { + "id": "d4Le4M5bCuhEb2NN", + "name": "BAC Parser - wealth.cescalante.dev" + } + } + } + ], + "pinData": {}, + "connections": { + "Gmail Trigger": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "versionId": "f0bbb681-1cf3-49f4-b06e-de0218b7ff3e", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "f446bf52a953467f756f2c188ff6b09473528d6316ecb3b9568cdbfbaa3258f3" + }, + "id": "e88c3UhBeo9WCbcy", + "tags": [] +} \ No newline at end of file diff --git a/frontend/src/components/TransactionModal.tsx b/frontend/src/components/TransactionModal.tsx index 20ed3a4..237cd49 100644 --- a/frontend/src/components/TransactionModal.tsx +++ b/frontend/src/components/TransactionModal.tsx @@ -151,6 +151,7 @@ export default function TransactionModal({ transaction, source, onClose, onSaved CRC (₡) USD ($) + EUR (€) diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts index 894a9d3..3976610 100644 --- a/frontend/src/lib/format.ts +++ b/frontend/src/lib/format.ts @@ -5,6 +5,9 @@ export function formatAmount(amount: number, currency: string) { if (currency === 'USD') { return `$${abs.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } + if (currency === 'EUR') { + return `€${abs.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + } return `₡${abs.toLocaleString('es-CR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }