- DEMO_MODE=true env flag: disables password changes and backup endpoints (403), exposes GET /demo/status for frontend detection - Auto-seed on first startup: creates demo user (demo@mymidas.app / demo123) with 6 months of transactions, investments, budgets, subscriptions, and tax payslips; takes a pg_dump snapshot immediately after for hourly restore - Hourly reset: resetter Alpine container with cron restores DB from snapshot and purges uploaded attachments every hour on the hour - Frontend: amber demo banner on all pages, login page shows credentials, password change disabled with notice, backups section replaced with notice - demo/ directory: self-contained docker-compose.yml (ports 4001/8091), .env.example, reset.sh, and step-by-step Portainer DEPLOY.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
492 lines
23 KiB
Python
492 lines
23 KiB
Python
"""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 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, 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", "2847.32", "#ff6b35")
|
||
marcus = mk_acc("Marcus Savings", "Goldman Sachs", "savings", "6234.50", "#22c55e")
|
||
amex = mk_acc("Amex Gold", "American Express", "credit_card", "-342.18", "#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()
|
||
|
||
# ── 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("101.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()
|