Initial commit: MyMidas personal finance tracker
Full-stack self-hosted finance app with FastAPI backend and React frontend. Features: - Accounts, transactions, budgets, investments with GBP base currency - CSV import with auto-detection for 10 UK bank formats - ML predictions: spending forecast, net worth projection, Monte Carlo - 7 selectable themes (Obsidian, Arctic, Midnight, Vault, Terminal, Synthwave, Ledger) - Receipt/document attachments on transactions (JPEG, PNG, WebP, PDF) - AES-256-GCM field encryption, RS256 JWT, TOTP 2FA, RLS, audit log - Encrypted nightly backups + key rotation script - Mobile-responsive layout with bottom nav Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
61a7884ee5
127 changed files with 13323 additions and 0 deletions
135
backend/app/services/category_service.py
Normal file
135
backend/app/services/category_service.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models.category import Category
|
||||
|
||||
SYSTEM_CATEGORIES = [
|
||||
# Income
|
||||
{"name": "Salary", "type": "income", "icon": "briefcase", "color": "#22c55e"},
|
||||
{"name": "Freelance", "type": "income", "icon": "laptop", "color": "#22c55e"},
|
||||
{"name": "Investment Income", "type": "income", "icon": "trending-up", "color": "#22c55e"},
|
||||
{"name": "Rental Income", "type": "income", "icon": "home", "color": "#22c55e"},
|
||||
{"name": "Benefits", "type": "income", "icon": "shield", "color": "#22c55e"},
|
||||
{"name": "Other Income", "type": "income", "icon": "plus-circle", "color": "#22c55e"},
|
||||
# Expenses — Housing
|
||||
{"name": "Rent / Mortgage", "type": "expense", "icon": "home", "color": "#6366f1"},
|
||||
{"name": "Council Tax", "type": "expense", "icon": "landmark", "color": "#6366f1"},
|
||||
{"name": "Home Insurance", "type": "expense", "icon": "shield", "color": "#6366f1"},
|
||||
{"name": "Home Maintenance", "type": "expense", "icon": "wrench", "color": "#6366f1"},
|
||||
# Utilities
|
||||
{"name": "Electricity", "type": "expense", "icon": "zap", "color": "#f59e0b"},
|
||||
{"name": "Gas", "type": "expense", "icon": "flame", "color": "#f59e0b"},
|
||||
{"name": "Water", "type": "expense", "icon": "droplets", "color": "#f59e0b"},
|
||||
{"name": "Internet", "type": "expense", "icon": "wifi", "color": "#f59e0b"},
|
||||
{"name": "Phone", "type": "expense", "icon": "smartphone", "color": "#f59e0b"},
|
||||
# Food
|
||||
{"name": "Groceries", "type": "expense", "icon": "shopping-cart", "color": "#ec4899"},
|
||||
{"name": "Eating Out", "type": "expense", "icon": "utensils", "color": "#ec4899"},
|
||||
{"name": "Coffee", "type": "expense", "icon": "coffee", "color": "#ec4899"},
|
||||
{"name": "Takeaway", "type": "expense", "icon": "package", "color": "#ec4899"},
|
||||
# Transport
|
||||
{"name": "Fuel", "type": "expense", "icon": "fuel", "color": "#0ea5e9"},
|
||||
{"name": "Public Transport", "type": "expense", "icon": "bus", "color": "#0ea5e9"},
|
||||
{"name": "Car Insurance", "type": "expense", "icon": "car", "color": "#0ea5e9"},
|
||||
{"name": "Car Maintenance", "type": "expense", "icon": "wrench", "color": "#0ea5e9"},
|
||||
{"name": "Parking", "type": "expense", "icon": "parking-circle", "color": "#0ea5e9"},
|
||||
{"name": "Taxi / Ride share", "type": "expense", "icon": "map-pin", "color": "#0ea5e9"},
|
||||
# Health
|
||||
{"name": "Healthcare", "type": "expense", "icon": "heart-pulse", "color": "#ef4444"},
|
||||
{"name": "Pharmacy", "type": "expense", "icon": "pill", "color": "#ef4444"},
|
||||
{"name": "Gym", "type": "expense", "icon": "dumbbell", "color": "#ef4444"},
|
||||
# Personal
|
||||
{"name": "Clothing", "type": "expense", "icon": "shirt", "color": "#a855f7"},
|
||||
{"name": "Personal Care", "type": "expense", "icon": "sparkles", "color": "#a855f7"},
|
||||
{"name": "Subscriptions", "type": "expense", "icon": "repeat", "color": "#a855f7"},
|
||||
{"name": "Entertainment", "type": "expense", "icon": "tv", "color": "#a855f7"},
|
||||
{"name": "Holidays", "type": "expense", "icon": "plane", "color": "#a855f7"},
|
||||
# Finance
|
||||
{"name": "Loan Repayment", "type": "expense", "icon": "credit-card", "color": "#64748b"},
|
||||
{"name": "Mortgage Payment", "type": "expense", "icon": "building", "color": "#64748b"},
|
||||
{"name": "Bank Charges", "type": "expense", "icon": "landmark", "color": "#64748b"},
|
||||
{"name": "Interest Paid", "type": "expense", "icon": "percent", "color": "#64748b"},
|
||||
# Savings
|
||||
{"name": "Savings", "type": "expense", "icon": "piggy-bank", "color": "#10b981"},
|
||||
{"name": "Investments", "type": "expense", "icon": "trending-up", "color": "#10b981"},
|
||||
# Other
|
||||
{"name": "Gifts", "type": "expense", "icon": "gift", "color": "#f97316"},
|
||||
{"name": "Education", "type": "expense", "icon": "graduation-cap", "color": "#f97316"},
|
||||
{"name": "Other Expense", "type": "expense", "icon": "more-horizontal", "color": "#64748b"},
|
||||
# Transfers
|
||||
{"name": "Transfer", "type": "transfer", "icon": "arrow-left-right", "color": "#94a3b8"},
|
||||
]
|
||||
|
||||
|
||||
async def seed_system_categories(db: AsyncSession) -> None:
|
||||
existing = await db.scalar(
|
||||
select(Category).where(Category.is_system == True).limit(1)
|
||||
)
|
||||
if existing:
|
||||
return
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
for i, cat in enumerate(SYSTEM_CATEGORIES):
|
||||
db.add(Category(
|
||||
user_id=None,
|
||||
name=cat["name"],
|
||||
type=cat["type"],
|
||||
icon=cat.get("icon"),
|
||||
color=cat.get("color"),
|
||||
is_system=True,
|
||||
sort_order=i,
|
||||
created_at=now,
|
||||
))
|
||||
await db.flush()
|
||||
|
||||
|
||||
async def list_categories(db: AsyncSession, user_id: uuid.UUID) -> list[dict]:
|
||||
result = await db.execute(
|
||||
select(Category).where(
|
||||
(Category.user_id == user_id) | (Category.user_id.is_(None))
|
||||
).order_by(Category.type, Category.sort_order, Category.name)
|
||||
)
|
||||
cats = result.scalars().all()
|
||||
return [
|
||||
{
|
||||
"id": str(c.id),
|
||||
"name": c.name,
|
||||
"type": c.type,
|
||||
"icon": c.icon,
|
||||
"color": c.color,
|
||||
"is_system": c.is_system,
|
||||
"parent_id": str(c.parent_id) if c.parent_id else None,
|
||||
"sort_order": c.sort_order,
|
||||
}
|
||||
for c in cats
|
||||
]
|
||||
|
||||
|
||||
async def create_category(
|
||||
db: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
name: str,
|
||||
type_: str,
|
||||
icon: str | None = None,
|
||||
color: str | None = None,
|
||||
parent_id: uuid.UUID | None = None,
|
||||
) -> dict:
|
||||
now = datetime.now(timezone.utc)
|
||||
cat = Category(
|
||||
user_id=user_id,
|
||||
name=name,
|
||||
type=type_,
|
||||
icon=icon,
|
||||
color=color,
|
||||
parent_id=parent_id,
|
||||
is_system=False,
|
||||
created_at=now,
|
||||
)
|
||||
db.add(cat)
|
||||
await db.flush()
|
||||
return {"id": str(cat.id), "name": cat.name, "type": cat.type, "icon": cat.icon, "color": cat.color, "is_system": False}
|
||||
Loading…
Add table
Add a link
Reference in a new issue