"""Demo seed — runs automatically on first startup when DEMO_MODE=true.""" from __future__ import annotations import hashlib import uuid from datetime import date, datetime, timezone from decimal import Decimal from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import encrypt_field, hash_password from app.db.models import ( Account, Asset, Budget, Category, InvestmentHolding, InvestmentTransaction, ManualCGTDisposal, NetWorthSnapshot, Payslip, TaxProfile, Transaction, User, ) DEMO_EMAIL = "demo@mymidas.app" DEMO_PASSWORD = "demo123" def _now() -> datetime: return datetime.now(timezone.utc) def _h(*parts) -> str: return hashlib.sha256("|".join(str(p) for p in parts).encode()).hexdigest() async def is_seeded(db: AsyncSession) -> bool: return bool(await db.scalar(select(User).where(User.email == DEMO_EMAIL))) async def seed_demo(db: AsyncSession) -> None: if await is_seeded(db): return now = _now() # ── User ────────────────────────────────────────────────────────────── user = User( id=uuid.uuid4(), email=DEMO_EMAIL, password_hash=hash_password(DEMO_PASSWORD), display_name="Alex Demo", base_currency="GBP", theme="obsidian", locale="en-GB", created_at=now, updated_at=now, ) db.add(user) await db.flush() uid = user.id # ── System categories ────────────────────────────────────────────────── res = await db.execute(select(Category).where(Category.is_system == True)) cats = {c.name: c for c in res.scalars().all()} def cid(name: str): return cats[name].id if name in cats else None # ── Accounts ────────────────────────────────────────────────────────── def mk_acc(name, institution, kind, balance, color, currency="GBP", credit_limit=None): return Account( id=uuid.uuid4(), user_id=uid, name_enc=encrypt_field(name), institution_enc=encrypt_field(institution) if institution else None, type=kind, currency=currency, current_balance=Decimal(str(balance)), credit_limit=Decimal(str(credit_limit)) if credit_limit else None, is_active=True, include_in_net_worth=True, color=color, meta={}, created_at=now, updated_at=now, ) monzo = mk_acc("Monzo Current Account", "Monzo", "checking", "0", "#ff6b35") marcus = mk_acc("Marcus Savings", "Goldman Sachs", "savings", "0", "#22c55e") amex = mk_acc("Amex Gold", "American Express", "credit_card", "0", "#f59e0b", credit_limit=5000) freetrade = mk_acc("Freetrade Stocks & Shares ISA", "Freetrade", "investment", "0", "#6366f1") for acc in [monzo, marcus, amex, freetrade]: db.add(acc) await db.flush() # ── Transaction helpers ──────────────────────────────────────────────── def txn(account, txn_type, amount, desc, merchant, cat_name, txn_date, is_recurring=False, recurring_rule=None): return Transaction( id=uuid.uuid4(), user_id=uid, account_id=account.id, type=txn_type, status="cleared", amount=Decimal(str(amount)), amount_base=Decimal(str(amount)), currency="GBP", base_currency="GBP", date=txn_date, description_enc=encrypt_field(desc), merchant_enc=encrypt_field(merchant) if merchant else None, category_id=cid(cat_name), tags=[], is_recurring=is_recurring, recurring_rule=recurring_rule, attachment_refs=[], import_hash=_h(uid, account.id, txn_date, amount, desc), meta={}, created_at=now, updated_at=now, ) def rr(freq, amount, next_exp, last_paid): return { "frequency": freq, "typical_amount": float(amount), "next_expected": next_exp, "last_paid": last_paid, "confidence": 0.97, "manually_set": False, "detected_at": now.isoformat(), } def next_month(y, m): return (y + 1, 1) if m == 12 else (y, m + 1) def d(y, m, day): return date(y, m, min(day, [31,28,31,30,31,30,31,31,30,31,30,31][m-1])) txns = [] # ── Monthly recurring transactions (Oct 2025 – Apr 2026) ────────────── months = [(2025, 10), (2025, 11), (2025, 12), (2026, 1), (2026, 2), (2026, 3), (2026, 4)] for year, month in months: ny, nm = next_month(year, month) n1 = d(ny, nm, 1).isoformat() this1 = d(year, month, 1).isoformat() # Salary txns.append(txn(monzo, "income", 3489.00, "Demo Corp Ltd - Salary", "Demo Corp Ltd", "Salary", d(year, month, 1), True, rr("monthly", 3489.00, n1, this1))) # Rent txns.append(txn(monzo, "expense", -1250.00, "DIRECT DEBIT ANYLETS PROPERTY MGMT", "Anylets", "Rent / Mortgage", d(year, month, 1), True, rr("monthly", -1250.00, n1, this1))) # Council tax txns.append(txn(monzo, "expense", -145.00, "DIRECT DEBIT LONDON BOROUGH COUNCIL TAX", "London Borough", "Council Tax", d(year, month, 1), True, rr("monthly", -145.00, n1, this1))) # Internet txns.append(txn(monzo, "expense", -35.00, "DIRECT DEBIT BT BROADBAND", "BT", "Internet", d(year, month, 2), True, rr("monthly", -35.00, d(ny, nm, 2).isoformat(), d(year, month, 2).isoformat()))) # Phone txns.append(txn(monzo, "expense", -25.00, "DIRECT DEBIT EE MOBILE", "EE", "Phone", d(year, month, 3), True, rr("monthly", -25.00, d(ny, nm, 3).isoformat(), d(year, month, 3).isoformat()))) # Energy (variable) energy = [-102, -98, -115, -108, -103, -95, -88][months.index((year, month))] txns.append(txn(monzo, "expense", energy, "DIRECT DEBIT OVO ENERGY", "OVO Energy", "Electricity", d(year, month, 5), True, rr("monthly", energy, d(ny, nm, 5).isoformat(), d(year, month, 5).isoformat()))) # Subscriptions for sub_desc, sub_merch, sub_amt in [ ("DIRECT DEBIT NETFLIX", "Netflix", -17.99), ("DIRECT DEBIT SPOTIFY", "Spotify", -11.99), ]: txns.append(txn(monzo, "expense", sub_amt, sub_desc, sub_merch, "Subscriptions", d(year, month, 5), True, rr("monthly", sub_amt, d(ny, nm, 5).isoformat(), d(year, month, 5).isoformat()))) for sub_desc, sub_merch, sub_amt in [ ("DIRECT DEBIT AMAZON PRIME", "Amazon Prime", -8.99), ("DIRECT DEBIT APPLE ICLOUD", "Apple iCloud", -2.99), ("DIRECT DEBIT GITHUB", "GitHub", -3.99), ]: txns.append(txn(monzo, "expense", sub_amt, sub_desc, sub_merch, "Subscriptions", d(year, month, 6), True, rr("monthly", sub_amt, d(ny, nm, 6).isoformat(), d(year, month, 6).isoformat()))) # Gym txns.append(txn(monzo, "expense", -35.00, "DIRECT DEBIT PUREGYM", "PureGym", "Gym", d(year, month, 7), True, rr("monthly", -35.00, d(ny, nm, 7).isoformat(), d(year, month, 7).isoformat()))) # TfL top-ups (2/month) tfl = [(-40, 17, -30, 29), (-35, 15, -40, 28), (-40, 16, -35, 30), (-40, 17, -30, 28), (-40, 14, -35, 27), (-40, 17, -30, 29), (-40, 10, -35, 22)][months.index((year, month))] txns.append(txn(monzo, "expense", tfl[0], "TfL Travel Top-Up", "Transport for London", "Public Transport", d(year, month, tfl[1]))) txns.append(txn(monzo, "expense", tfl[2], "TfL Travel Top-Up", "Transport for London", "Public Transport", d(year, month, tfl[3]))) # Transfer to savings out_id = uuid.uuid4() inn_id = uuid.uuid4() sav_rr = rr("monthly", -300.00, d(ny, nm, 15).isoformat(), d(year, month, 15).isoformat()) txns.append(Transaction( id=out_id, user_id=uid, account_id=monzo.id, transfer_account_id=marcus.id, type="transfer", status="cleared", amount=Decimal("-300.00"), amount_base=Decimal("-300.00"), currency="GBP", base_currency="GBP", date=d(year, month, 15), description_enc=encrypt_field("Transfer to Marcus Savings"), category_id=cid("Transfer"), tags=[], is_recurring=True, recurring_rule=sav_rr, attachment_refs=[], import_hash=_h(uid, monzo.id, year, month, "transfer_out"), meta={}, created_at=now, updated_at=now, )) txns.append(Transaction( id=inn_id, user_id=uid, account_id=marcus.id, transfer_account_id=monzo.id, type="transfer", status="cleared", amount=Decimal("300.00"), amount_base=Decimal("300.00"), currency="GBP", base_currency="GBP", date=d(year, month, 15), description_enc=encrypt_field("Transfer from Monzo"), category_id=cid("Transfer"), tags=[], is_recurring=True, recurring_rule=sav_rr, attachment_refs=[], import_hash=_h(uid, marcus.id, year, month, "transfer_in"), meta={}, created_at=now, updated_at=now, )) # ── Groceries ───────────────────────────────────────────────────────── groceries = [ (2025,10, 9, -71.43, "Tesco"), (2025,10, 16, -63.21, "Sainsbury's"), (2025,10, 23, -58.76, "Tesco"), (2025,10, 28, -47.30, "Lidl"), (2025,10, 30, -65.80, "Tesco"), (2025,11, 8, -68.92, "Tesco"), (2025,11, 15, -72.10, "Sainsbury's"), (2025,11, 21, -55.40, "Lidl"), (2025,11, 27, -61.33, "Tesco"), (2025,12, 6, -82.14, "Waitrose"), (2025,12, 13, -65.20, "Sainsbury's"), (2025,12, 20, -71.50, "Tesco"), (2025,12, 27, -90.30, "Waitrose"), (2026, 1, 10, -66.45, "Tesco"), (2026, 1, 17, -59.80, "Lidl"), (2026, 1, 24, -74.20, "Sainsbury's"), (2026, 1, 31, -68.90, "Tesco"), (2026, 2, 7, -73.15, "Tesco"), (2026, 2, 14, -61.40, "Sainsbury's"), (2026, 2, 21, -55.90, "Lidl"), (2026, 2, 28, -80.45, "Waitrose"), (2026, 3, 7, -69.30, "Tesco"), (2026, 3, 14, -58.75, "Lidl"), (2026, 3, 21, -76.20, "Sainsbury's"), (2026, 3, 28, -65.80, "Tesco"), (2026, 4, 5, -72.40, "Tesco"), (2026, 4, 12, -61.15, "Sainsbury's"), (2026, 4, 19, -55.90, "Lidl"), ] for y, m, day, amt, merch in groceries: txns.append(txn(monzo, "expense", amt, f"{merch} Groceries", merch, "Groceries", date(y, m, day))) # ── Eating out ──────────────────────────────────────────────────────── eating_out = [ (2025,10, 14, -42.00, "Wagamama"), (2025,10, 19, -28.50, "Deliveroo"), (2025,10, 26, -55.00, "Dishoom"), (2025,11, 8, -35.00, "Nando's"), (2025,11, 22, -48.50, "Carluccio's"), (2025,11, 28, -22.00, "Deliveroo"), (2025,12, 12, -65.00, "Gaucho"), (2025,12, 19, -38.00, "Pizza Express"), (2025,12, 23, -52.00, "Dishoom"), (2026, 1, 10, -29.00, "Deliveroo"), (2026, 1, 17, -45.00, "Wagamama"), (2026, 1, 25, -38.50, "Nando's"), (2026, 2, 6, -42.00, "Dishoom"), (2026, 2, 14, -78.00, "Restaurant"), (2026, 2, 22, -31.50, "Deliveroo"), (2026, 3, 8, -38.00, "Wagamama"), (2026, 3, 15, -25.00, "Deliveroo"), (2026, 3, 22, -47.00, "Nando's"), (2026, 4, 8, -44.00, "Dishoom"), (2026, 4, 18, -27.50, "Deliveroo"), ] for y, m, day, amt, merch in eating_out: txns.append(txn(monzo, "expense", amt, merch, merch, "Eating Out", date(y, m, day))) # ── Coffee ──────────────────────────────────────────────────────────── coffee = [ (2025,10, 12, -4.80, "Costa Coffee"), (2025,10, 17, -8.50, "Pret a Manger"), (2025,10, 24, -5.20, "Starbucks"), (2025,11, 5, -4.50, "Costa Coffee"), (2025,11, 13, -5.20, "Starbucks"), (2025,11, 20, -4.80, "Pret a Manger"), (2025,11, 27, -6.50, "Blank Street"), (2025,12, 4, -5.20, "Starbucks"), (2025,12, 11, -4.80, "Costa Coffee"), (2025,12, 18, -5.00, "Pret a Manger"), (2026, 1, 9, -4.50, "Costa Coffee"), (2026, 1, 16, -5.20, "Starbucks"), (2026, 1, 23, -4.80, "Pret a Manger"), (2026, 2, 5, -5.20, "Starbucks"), (2026, 2, 12, -4.80, "Costa Coffee"), (2026, 2, 20, -5.50, "Blank Street"), (2026, 2, 27, -4.80, "Pret a Manger"), (2026, 3, 5, -4.80, "Costa Coffee"), (2026, 3, 12, -5.20, "Starbucks"), (2026, 3, 19, -4.50, "Pret a Manger"), (2026, 4, 10, -4.80, "Costa Coffee"), (2026, 4, 17, -5.20, "Starbucks"), (2026, 4, 22, -4.50, "Pret a Manger"), ] for y, m, day, amt, merch in coffee: txns.append(txn(monzo, "expense", amt, merch, merch, "Coffee", date(y, m, day))) # ── Shopping / other ────────────────────────────────────────────────── shopping = [ (2025,10, 21, -34.99, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2025,10, 31, -12.99, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2025,10, 25, -89.00, monzo, "ASOS", "ASOS", "Clothing"), (2025,11, 14, -89.99, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2025,11, 18, -85.00, monzo, "Dental Practice", "Dental Practice", "Healthcare"), (2025,11, 28, -22.50, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2025,12, 3, -156.00, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2025,12, 5, -650.00, monzo, "EasyJet", "EasyJet", "Holidays"), (2025,12, 5, -380.00, monzo, "Booking.com", "Booking.com", "Holidays"), (2025,12, 15, -210.00, monzo, "Amazon", "AMAZON.CO.UK", "Gifts"), (2025,12, 16, -85.00, monzo, "John Lewis", "John Lewis", "Gifts"), (2025,12, 18, -45.00, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2025,12, 20, -120.00, monzo, "Airbnb", "Airbnb", "Holidays"), (2026, 1, 12, -28.99, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2026, 1, 26, -67.00, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2026, 2, 10, -44.99, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2026, 2, 18, -75.00, monzo, "Uniqlo", "Uniqlo", "Clothing"), (2026, 2, 25, -19.99, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2026, 3, 8, -38.50, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2026, 3, 22, -55.00, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2026, 4, 9, -29.99, monzo, "Amazon", "AMAZON.CO.UK", "Other Expense"), (2026, 4, 16, -42.00, monzo, "Boots", "Boots", "Personal Care"), # Car insurance (October, annual) (2025,10, 15, -485.00, monzo, "Aviva", "AVIVA CAR INSURANCE ANNUAL", "Car Insurance"), # Amex purchases (2025,10, 20, -320.00, amex, "M&S", "MARKS AND SPENCER", "Groceries"), (2025,11, 8, -145.00, amex, "Restaurant", "RESTAURANT GORDON RAMSAY", "Eating Out"), (2025,12, 22, -89.00, amex, "Apple", "APPLE STORE", "Other Expense"), (2026, 1, 25, -215.00, amex, "Sofitel", "SOFITEL HOTEL", "Holidays"), (2026, 2, 14, -178.50, amex, "Selfridges", "SELFRIDGES", "Clothing"), (2026, 3, 8, -95.00, amex, "Harvey Nichols", "HARVEY NICHOLS", "Personal Care"), # Savings interest (quarterly) (2025,10, 28, 12.50, marcus, "Goldman Sachs", "Marcus Savings Interest", "Investment Income"), (2026, 1, 28, 13.20, marcus, "Goldman Sachs", "Marcus Savings Interest", "Investment Income"), ] for row in shopping: y, m, day, amt, acc, merch, desc, cat = row txn_type = "income" if amt > 0 else "expense" txns.append(txn(acc, txn_type, amt, desc, merch, cat, date(y, m, day))) for t in txns: db.add(t) await db.flush() # ── Compute account balances from actual transactions ───────────────── for acc in [monzo, marcus, amex]: result = await db.execute( select(func.sum(Transaction.amount)).where( Transaction.account_id == acc.id, Transaction.deleted_at.is_(None), ) ) acc.current_balance = result.scalar() or Decimal("0") # Freetrade balance stays 0 — investment account value tracked via holdings # ── Budgets ─────────────────────────────────────────────────────────── budget_defs = [ ("Groceries", 300.00, "Groceries"), ("Eating Out", 200.00, "Eating Out"), ("Transport", 100.00, "Public Transport"), ("Entertainment", 80.00, "Entertainment"), ("Utilities", 180.00, "Electricity"), ("Subscriptions", 60.00, "Subscriptions"), ("Shopping", 200.00, "Clothing"), ] for bname, amount, cat_name in budget_defs: cat_id = cid(cat_name) if not cat_id: continue db.add(Budget( id=uuid.uuid4(), user_id=uid, category_id=cat_id, name=bname, amount=Decimal(str(amount)), currency="GBP", period="monthly", start_date=date(2025, 10, 1), rollover=False, alert_threshold=Decimal("80"), is_active=True, created_at=now, updated_at=now, )) await db.flush() # ── Assets ──────────────────────────────────────────────────────────── vwrp = Asset( id=uuid.uuid4(), symbol="VWRP.L", name="Vanguard FTSE All-World UCITS ETF", type="etf", currency="GBP", exchange="LSE", data_source="yahoo_finance", last_price=Decimal("107.50"), last_price_at=now, price_change_24h=Decimal("0.85"), is_active=True, created_at=now, updated_at=now, ) aapl = Asset( id=uuid.uuid4(), symbol="AAPL", name="Apple Inc.", type="stock", currency="USD", exchange="NASDAQ", data_source="yahoo_finance", last_price=Decimal("212.50"), last_price_at=now, price_change_24h=Decimal("-0.45"), is_active=True, created_at=now, updated_at=now, ) btc = Asset( id=uuid.uuid4(), symbol="BTC-USD", name="Bitcoin", type="crypto", currency="USD", exchange=None, data_source="coingecko", last_price=Decimal("84500.00"), last_price_at=now, price_change_24h=Decimal("2.30"), is_active=True, created_at=now, updated_at=now, ) for a in [vwrp, aapl, btc]: db.add(a) await db.flush() # ── Holdings ────────────────────────────────────────────────────────── # Quantities match investment transactions below — do not double-count. vwrp_h = InvestmentHolding( id=uuid.uuid4(), user_id=uid, account_id=freetrade.id, asset_id=vwrp.id, quantity=Decimal("50"), avg_cost_basis=Decimal("102.21"), currency="GBP", created_at=now, updated_at=now, ) aapl_h = InvestmentHolding( id=uuid.uuid4(), user_id=uid, account_id=freetrade.id, asset_id=aapl.id, quantity=Decimal("10"), avg_cost_basis=Decimal("228.50"), currency="USD", created_at=now, updated_at=now, ) btc_h = InvestmentHolding( id=uuid.uuid4(), user_id=uid, account_id=freetrade.id, asset_id=btc.id, quantity=Decimal("0.05"), avg_cost_basis=Decimal("69500.00"), currency="USD", created_at=now, updated_at=now, ) for h in [vwrp_h, aapl_h, btc_h]: db.add(h) await db.flush() # ── Investment transactions (history only — holding quantities already set) ── inv_txns = [ # VWRP: 3 buys totalling 50 shares InvestmentTransaction( id=uuid.uuid4(), user_id=uid, holding_id=vwrp_h.id, type="buy", quantity=Decimal("15"), price=Decimal("98.50"), fees=Decimal("0"), total_amount=Decimal("1477.50"), currency="GBP", date=date(2025, 10, 5), created_at=now, ), InvestmentTransaction( id=uuid.uuid4(), user_id=uid, holding_id=vwrp_h.id, type="buy", quantity=Decimal("20"), price=Decimal("102.30"), fees=Decimal("0"), total_amount=Decimal("2046.00"), currency="GBP", date=date(2025, 12, 10), created_at=now, ), InvestmentTransaction( id=uuid.uuid4(), user_id=uid, holding_id=vwrp_h.id, type="buy", quantity=Decimal("15"), price=Decimal("105.80"), fees=Decimal("0"), total_amount=Decimal("1587.00"), currency="GBP", date=date(2026, 2, 14), created_at=now, ), # VWRP dividend InvestmentTransaction( id=uuid.uuid4(), user_id=uid, holding_id=vwrp_h.id, type="dividend", quantity=Decimal("0"), price=Decimal("0"), fees=Decimal("0"), total_amount=Decimal("62.50"), currency="GBP", date=date(2025, 12, 20), created_at=now, ), # AAPL buy InvestmentTransaction( id=uuid.uuid4(), user_id=uid, holding_id=aapl_h.id, type="buy", quantity=Decimal("10"), price=Decimal("228.50"), fees=Decimal("0"), total_amount=Decimal("2285.00"), currency="USD", date=date(2025, 10, 15), created_at=now, ), # AAPL dividend InvestmentTransaction( id=uuid.uuid4(), user_id=uid, holding_id=aapl_h.id, type="dividend", quantity=Decimal("0"), price=Decimal("0"), fees=Decimal("0"), total_amount=Decimal("23.00"), currency="USD", date=date(2026, 2, 15), created_at=now, ), # BTC buy InvestmentTransaction( id=uuid.uuid4(), user_id=uid, holding_id=btc_h.id, type="buy", quantity=Decimal("0.05"), price=Decimal("69500.00"), fees=Decimal("0"), total_amount=Decimal("3475.00"), currency="USD", date=date(2025, 11, 1), created_at=now, ), ] for it in inv_txns: db.add(it) await db.flush() # ── Tax profile & payslips (2025/26 = tax_year 2026) ───────────────── tax_profile = TaxProfile( id=uuid.uuid4(), user_id=uid, tax_year=2026, employer_name_enc=encrypt_field("Demo Corp Ltd"), tax_code="1257L", is_cumulative=True, created_at=now, updated_at=now, ) db.add(tax_profile) await db.flush() # 6 payslips: April – September 2025 for month in range(4, 10): db.add(Payslip( id=uuid.uuid4(), user_id=uid, tax_profile_id=tax_profile.id, period_month=month, period_year=2025, gross_pay=Decimal("4500.00"), income_tax_withheld=Decimal("753.00"), ni_withheld=Decimal("258.00"), net_pay=Decimal("3489.00"), is_p60=False, created_at=now, )) # Manual CGT disposal — employee share scheme, gain exceeds annual exempt db.add(ManualCGTDisposal( id=uuid.uuid4(), user_id=uid, tax_year=2026, disposal_date=date(2025, 9, 15), asset_description_enc=encrypt_field("Tech Corp Ltd — Employee Share Scheme"), proceeds=Decimal("8500.00"), cost_basis=Decimal("4200.00"), created_at=now, )) await db.flush() # ── Historical net worth snapshots (weekly, Oct 2025 → present) ─────── # Plausible progression: cash + growing investment portfolio, salary in # each month, large purchases (car insurance Oct, Christmas Dec, etc.) nw_history = [ # (date, total_assets, total_liabilities) (date(2025, 9, 28), "8150.00", "0.00"), (date(2025, 10, 5), "8320.00", "220.00"), # VWRP+AAPL bought, Amex started (date(2025, 10, 12), "8050.00", "680.00"), # car insurance £485 hit (date(2025, 10, 19), "8750.00", "820.00"), # salary in, investments ticked up (date(2025, 10, 26), "9150.00", "820.00"), (date(2025, 11, 2), "9820.00", "820.00"), # salary + BTC purchase Nov 1 (date(2025, 11, 9), "9600.00", "820.00"), # slight market dip (date(2025, 11, 16), "9900.00", "820.00"), (date(2025, 11, 23), "10150.00", "820.00"), (date(2025, 11, 30), "10380.00", "820.00"), (date(2025, 12, 7), "10620.00", "820.00"), (date(2025, 12, 14), "10250.00", "1250.00"), # Christmas spending on Amex (date(2025, 12, 21), "10100.00", "1350.00"), # VWRP 2nd buy + holiday spend (date(2025, 12, 28), "10350.00", "1350.00"), # post-Xmas, markets recovering (date(2026, 1, 4), "10950.00", "1100.00"), # new year, paid some Amex, markets up (date(2026, 1, 11), "11350.00", "1100.00"), (date(2026, 1, 18), "11700.00", "1100.00"), (date(2026, 1, 25), "11500.00", "1100.00"), # small dip (date(2026, 2, 1), "12050.00", "1100.00"), # salary in (date(2026, 2, 8), "12450.00", "1100.00"), (date(2026, 2, 15), "13050.00", "1100.00"), # VWRP 3rd buy + market run (date(2026, 2, 22), "13500.00", "1100.00"), (date(2026, 3, 1), "14050.00", "1100.00"), # salary in (date(2026, 3, 8), "14450.00", "1100.00"), (date(2026, 3, 15), "15000.00", "1050.00"), (date(2026, 3, 22), "15500.00", "1042.50"), (date(2026, 3, 29), "15650.00", "1042.50"), (date(2026, 4, 5), "15680.00", "1042.50"), (date(2026, 4, 12), "15710.00", "1042.50"), (date(2026, 4, 19), "15740.00", "1042.50"), ] for snap_date, assets_str, liabs_str in nw_history: assets = Decimal(assets_str) liabs = Decimal(liabs_str) db.add(NetWorthSnapshot( id=uuid.uuid4(), user_id=uid, date=snap_date, total_assets=assets, total_liabilities=liabs, net_worth=assets - liabs, base_currency="GBP", breakdown={}, created_at=now, )) await db.flush()