-- Migration 0040: Portfolio Reconciliation Schema (VS-14) -- Creates tables for holdings tracking, cost basis, and reconciliation logs CREATE SCHEMA IF NOT EXISTS portfolio_management; CREATE TABLE IF NOT EXISTS portfolio_management.holdings ( id UUID PRIMARY KEY, security_id UUID NOT NULL, quantity INT NOT NULL DEFAULT 0, weighted_avg_cost DECIMAL(15,2) NOT NULL DEFAULT 0, total_cost_basis DECIMAL(18,2) NOT NULL DEFAULT 0, market_value DECIMAL(18,2), unrealized_gain_loss DECIMAL(18,2), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), correlation_id UUID NOT NULL, revision INT NOT NULL DEFAULT 1, CONSTRAINT chk_quantity_non_negative CHECK (quantity >= 0), CONSTRAINT chk_cost_basis_non_negative CHECK (total_cost_basis >= 0) ); CREATE INDEX IF NOT EXISTS idx_holdings_security_id ON portfolio_management.holdings(security_id); CREATE INDEX IF NOT EXISTS idx_holdings_correlation_id ON portfolio_management.holdings(correlation_id); CREATE INDEX IF NOT EXISTS idx_holdings_updated_at ON portfolio_management.holdings(updated_at DESC); CREATE TABLE IF NOT EXISTS portfolio_management.reconciliation_logs ( id UUID PRIMARY KEY, trade_id UUID NOT NULL, holding_id UUID NOT NULL REFERENCES portfolio_management.holdings(id), quantity_before INT, quantity_after INT, cost_basis_delta DECIMAL(18,2), unrealized_gain_loss_delta DECIMAL(18,2), mismatch_detected BOOLEAN DEFAULT FALSE, mismatch_reason VARCHAR(255), reconciled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), correlation_id UUID NOT NULL, CONSTRAINT chk_mismatch_reason_when_detected CHECK (NOT mismatch_detected OR mismatch_reason IS NOT NULL) ); CREATE INDEX IF NOT EXISTS idx_reconciliation_logs_trade_id ON portfolio_management.reconciliation_logs(trade_id); CREATE INDEX IF NOT EXISTS idx_reconciliation_logs_holding_id ON portfolio_management.reconciliation_logs(holding_id); CREATE INDEX IF NOT EXISTS idx_reconciliation_logs_mismatch ON portfolio_management.reconciliation_logs(mismatch_detected); CREATE INDEX IF NOT EXISTS idx_reconciliation_logs_correlation_id ON portfolio_management.reconciliation_logs(correlation_id); CREATE INDEX IF NOT EXISTS idx_reconciliation_logs_reconciled_at ON portfolio_management.reconciliation_logs(reconciled_at DESC); CREATE TABLE IF NOT EXISTS portfolio_management.lots ( id UUID PRIMARY KEY, holding_id UUID NOT NULL REFERENCES portfolio_management.holdings(id), purchase_date DATE NOT NULL, quantity INT NOT NULL, unit_cost DECIMAL(15,2) NOT NULL, total_cost DECIMAL(18,2) NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'OPEN', fifo_order INT NOT NULL, published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), correlation_id UUID NOT NULL, CONSTRAINT chk_lot_quantity_positive CHECK (quantity > 0), CONSTRAINT chk_lot_status CHECK (status IN ('OPEN', 'PARTIAL_SOLD', 'CLOSED')) ); CREATE INDEX IF NOT EXISTS idx_lots_holding_id ON portfolio_management.lots(holding_id); CREATE INDEX IF NOT EXISTS idx_lots_status ON portfolio_management.lots(status); CREATE INDEX IF NOT EXISTS idx_lots_fifo_order ON portfolio_management.lots(holding_id, fifo_order); CREATE INDEX IF NOT EXISTS idx_lots_correlation_id ON portfolio_management.lots(correlation_id); -- Grant permissions (adjust to match your security model) GRANT SELECT, INSERT ON portfolio_management.holdings TO kartsell; GRANT SELECT, INSERT ON portfolio_management.reconciliation_logs TO kartsell; GRANT SELECT, INSERT ON portfolio_management.lots TO kartsell;