mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 08:48:48 +02:00
Add EUR currency support for international transactions
All checks were successful
Deploy to VPS / deploy (push) Successful in 50s
All checks were successful
Deploy to VPS / deploy (push) Successful in 50s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
24
CLAUDE.md
24
CLAUDE.md
@@ -13,9 +13,33 @@ Personal finance management web app.
|
|||||||
cd frontend && pnpm install && pnpm run dev
|
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
|
## Deployment
|
||||||
|
|
||||||
- Deployed via Gitea Actions (self-hosted runner on VPS)
|
- Deployed via Gitea Actions (self-hosted runner on VPS)
|
||||||
- Push to `main` triggers: GitHub → webhook → Gitea mirror sync → Actions workflow → Docker build & deploy
|
- Push to `main` triggers: GitHub → webhook → Gitea mirror sync → Actions workflow → Docker build & deploy
|
||||||
- Domain: wealth.cescalante.dev
|
- Domain: wealth.cescalante.dev
|
||||||
- Reverse proxy: nginx-proxy + acme-companion (auto TLS)
|
- 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`
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ def create_transaction(
|
|||||||
session.refresh(tx)
|
session.refresh(tx)
|
||||||
|
|
||||||
# Send push notification
|
# 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}"
|
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
|
is_deposit = tx.transaction_type == TransactionType.DEPOSITO
|
||||||
send_push_to_all(
|
send_push_to_all(
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ def run_migrations():
|
|||||||
except Exception:
|
except Exception:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.execute(text("ALTER TYPE currency ADD VALUE IF NOT EXISTS 'EUR'"))
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
|
||||||
|
|
||||||
def get_session():
|
def get_session():
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class TransactionSource(str, enum.Enum):
|
|||||||
class Currency(str, enum.Enum):
|
class Currency(str, enum.Enum):
|
||||||
CRC = "CRC"
|
CRC = "CRC"
|
||||||
USD = "USD"
|
USD = "USD"
|
||||||
|
EUR = "EUR"
|
||||||
BTC = "BTC"
|
BTC = "BTC"
|
||||||
XMR = "XMR"
|
XMR = "XMR"
|
||||||
|
|
||||||
|
|||||||
97
docs/WealthySmart_ BAC Pensions Statements parser.json
Normal file
97
docs/WealthySmart_ BAC Pensions Statements parser.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
@@ -151,6 +151,7 @@ export default function TransactionModal({ transaction, source, onClose, onSaved
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="CRC">CRC (₡)</SelectItem>
|
<SelectItem value="CRC">CRC (₡)</SelectItem>
|
||||||
<SelectItem value="USD">USD ($)</SelectItem>
|
<SelectItem value="USD">USD ($)</SelectItem>
|
||||||
|
<SelectItem value="EUR">EUR (€)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export function formatAmount(amount: number, currency: string) {
|
|||||||
if (currency === 'USD') {
|
if (currency === 'USD') {
|
||||||
return `$${abs.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
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 })}`;
|
return `₡${abs.toLocaleString('es-CR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user