MyMidas/backend/app/demo/seed.py
megaproxy 664b530136 Seed historical net worth snapshots and trigger live price sync on demo startup
- seed.py: adds 30 weekly NetWorthSnapshot rows (Sep 2025 → Apr 2026) so the
  Net Worth chart has full history on first boot, not just today's value
- main.py: fires price_sync_job and fx_sync_job in the background immediately
  after the scheduler starts in demo mode, so portfolio valuations are live
  from the moment the container becomes healthy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 22:47:44 +00:00

556 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 Mar 2026) ──────────────
months = [(2025, 10), (2025, 11), (2025, 12), (2026, 1), (2026, 2), (2026, 3)]
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][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)][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"),
]
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"),
]
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"),
]
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"),
# 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()