/** * @fileoverview Portfolio Domain - Manual Investments & Static Wealth * Clean separation: Portfolio != Trading signals * Purpose: Track manual investments, portfolio snapshots (no auth bloat) */ import { z } from 'zod'; // Core primitives for portfolio domain export const GitHubUsername = z.string().min(1).regex(/^[a-z0-9-]+$/i); export const Currency = z.enum(['GBP', 'USD', 'EUR']); // Core currencies // Type exports for primitives export type GitHubUsernameType = z.infer; export type CurrencyType = z.infer; // Enhanced Manual Investment (Infrastructure Migration: 20250916000006) export const ManualInvestment = z.object({ id: z.string().uuid(), owner: GitHubUsername, // who owns it asset_type: z.enum(['stock','etf','fund','crypto','bond']).default('stock'), symbol: z.string().min(1), // "AAPL","VUSA.L","BTC-USD" // Enhanced fields from Infrastructure migration exchange: z.string().optional(), // Trading exchange (LSE, NYSE, HL) currency: Currency.default('GBP'), // Investment currency account_type: z.enum(['SIPP', 'ISA', 'FUND_SHARE', 'GENERAL']).optional(), // Account classification // Core tracking quantity: z.number().finite(), cost_basis: z.number().finite(), // per unit cost in currency acquisition_date: z.string().datetime().optional(), notes: z.string().max(2000).optional(), created_at: z.string().datetime(), updated_at: z.string().datetime() }).strict(); // Portfolio snapshot with accounts breakdown (Daily value tracking) export const PortfolioSnapshot = z.object({ id: z.string().uuid(), owner: GitHubUsername, snapshot_date: z.string(), // Date string (YYYY-MM-DD) total_value: z.number().finite(), total_cost: z.number().finite(), total_pl: z.number().finite(), total_pl_percent: z.number().finite(), currency: Currency.default('GBP'), accounts: z.array(z.object({ source: z.enum(['manual', 'hl', 'external']), account_name: z.string(), value: z.number().finite(), cost: z.number().finite(), pl: z.number().finite() }).strict()), created_at: z.string().datetime() }).strict(); // Portfolio summary (simplified view) export const PortfolioSummary = z.object({ owner: GitHubUsername, currency: Currency.default('GBP'), total_value: z.number().finite(), total_cost: z.number().finite(), total_pl: z.number().finite(), investments: z.array(ManualInvestment), snapshot_ts: z.string().datetime() }).strict(); // Request/Response schemas export const AddInvestmentRequest = z.object({ symbol: z.string(), asset_type: z.enum(['stock','etf','fund','crypto','bond']), quantity: z.number().finite(), cost_basis: z.number().finite(), acquisition_date: z.string().datetime().optional() }).strict(); export const PortfolioResponse = z.object({ success: z.literal(true), data: PortfolioSummary, requestId: z.string(), timestamp: z.string().datetime() }).strict(); // New envelope types for missing routes export const HoldingsResponse = z.object({ success: z.literal(true), data: z.object({ account_type: z.string(), items: z.array(z.object({ symbol: z.string(), quantity: z.number(), avg_price: z.number(), current_price: z.number(), value: z.number(), currency: Currency.optional() }).strict()) }).strict(), requestId: z.string(), timestamp: z.string().datetime() }).strict(); export const AccountsResponse = z.object({ success: z.literal(true), data: z.object({ accounts: z.array(z.object({ id: z.string(), type: z.string(), balance: z.number(), currency: Currency.default('GBP'), account_name: z.string().optional() }).strict()) }).strict(), requestId: z.string(), timestamp: z.string().datetime() }).strict(); export const ManualListResponse = z.object({ success: z.literal(true), data: z.object({ investments: z.array(ManualInvestment) }).strict(), requestId: z.string(), timestamp: z.string().datetime() }).strict(); // Type exports export type ManualInvestmentType = z.infer; export type PortfolioSnapshotType = z.infer; export type PortfolioSummaryType = z.infer; export type AddInvestmentRequestType = z.infer; export type PortfolioResponseType = z.infer; export type HoldingsResponseType = z.infer; export type AccountsResponseType = z.infer; export type ManualListResponseType = z.infer;