MyMidas/backend/app/services/category_service.py
megaproxy 61a7884ee5 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>
2026-04-21 11:56:10 +00:00

135 lines
5.9 KiB
Python

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}