package db import ( "database/sql" "fmt" "log" ) func Migrate(db *sql.DB) error { migrations := []string{ createOrganizations, createProfiles, createDeals, createFolders, createFiles, createDiligenceRequests, createContacts, createDealActivity, createSessions, createIndexes, createInvites, createBuyerGroups, createFolderAccess, createFileComments, createContactDeals, } for i, m := range migrations { if _, err := db.Exec(m); err != nil { return fmt.Errorf("migration %d failed: %w", i+1, err) } } // Run additive ALTER TABLE migrations (ignore errors for already-existing columns) for _, stmt := range additiveMigrationStmts { db.Exec(stmt) } // Seed demo data if empty var count int db.QueryRow("SELECT COUNT(*) FROM organizations").Scan(&count) if count == 0 { log.Println("Seeding demo data...") if err := seed(db); err != nil { return fmt.Errorf("seed failed: %w", err) } } return nil } const createOrganizations = ` CREATE TABLE IF NOT EXISTS organizations ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );` const createProfiles = ` CREATE TABLE IF NOT EXISTS profiles ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, full_name TEXT NOT NULL, avatar_url TEXT DEFAULT '', organization_id TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ('owner','admin','member','viewer')), password_hash TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME, FOREIGN KEY (organization_id) REFERENCES organizations(id) );` const createDeals = ` CREATE TABLE IF NOT EXISTS deals ( id TEXT PRIMARY KEY, organization_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT DEFAULT '', target_company TEXT DEFAULT '', stage TEXT NOT NULL DEFAULT 'prospect' CHECK (stage IN ('prospect','internal','initial_marketing','ioi','loi','closed','pipeline','initial_review','due_diligence','final_negotiation','dead')), deal_size REAL DEFAULT 0, currency TEXT DEFAULT 'USD', ioi_date TEXT DEFAULT '', loi_date TEXT DEFAULT '', exclusivity_end_date TEXT DEFAULT '', expected_close_date TEXT DEFAULT '', close_probability INTEGER DEFAULT 0, is_archived BOOLEAN DEFAULT 0, created_by TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (organization_id) REFERENCES organizations(id) );` const createFolders = ` CREATE TABLE IF NOT EXISTS folders ( id TEXT PRIMARY KEY, deal_id TEXT NOT NULL, parent_id TEXT DEFAULT '', name TEXT NOT NULL, description TEXT DEFAULT '', created_by TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (deal_id) REFERENCES deals(id) );` const createFiles = ` CREATE TABLE IF NOT EXISTS files ( id TEXT PRIMARY KEY, deal_id TEXT NOT NULL, folder_id TEXT DEFAULT '', name TEXT NOT NULL, file_size INTEGER DEFAULT 0, mime_type TEXT DEFAULT '', status TEXT DEFAULT 'uploaded' CHECK (status IN ('uploaded','processing','reviewed','flagged','archived')), uploaded_by TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (deal_id) REFERENCES deals(id) );` const createDiligenceRequests = ` CREATE TABLE IF NOT EXISTS diligence_requests ( id TEXT PRIMARY KEY, deal_id TEXT NOT NULL, item_number TEXT DEFAULT '', section TEXT NOT NULL, description TEXT NOT NULL, priority TEXT DEFAULT 'medium' CHECK (priority IN ('high','medium','low')), atlas_status TEXT DEFAULT 'missing' CHECK (atlas_status IN ('fulfilled','partial','missing','not_applicable')), atlas_note TEXT DEFAULT '', confidence INTEGER DEFAULT 0, buyer_comment TEXT DEFAULT '', seller_comment TEXT DEFAULT '', buyer_group TEXT DEFAULT '', linked_file_ids TEXT DEFAULT '', created_by TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (deal_id) REFERENCES deals(id) );` const createContacts = ` CREATE TABLE IF NOT EXISTS contacts ( id TEXT PRIMARY KEY, organization_id TEXT NOT NULL, full_name TEXT NOT NULL, email TEXT DEFAULT '', phone TEXT DEFAULT '', company TEXT DEFAULT '', title TEXT DEFAULT '', contact_type TEXT DEFAULT 'buyer', tags TEXT DEFAULT '', notes TEXT DEFAULT '', last_activity_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (organization_id) REFERENCES organizations(id) );` const createDealActivity = ` CREATE TABLE IF NOT EXISTS deal_activity ( id TEXT PRIMARY KEY, organization_id TEXT DEFAULT '', deal_id TEXT DEFAULT '', user_id TEXT DEFAULT '', activity_type TEXT NOT NULL, resource_type TEXT DEFAULT '', resource_name TEXT DEFAULT '', resource_id TEXT DEFAULT '', details TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP );` const createSessions = ` CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, user_id TEXT NOT NULL, expires_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES profiles(id) );` const createIndexes = ` CREATE INDEX IF NOT EXISTS idx_profiles_email ON profiles(email); CREATE INDEX IF NOT EXISTS idx_profiles_org ON profiles(organization_id); CREATE INDEX IF NOT EXISTS idx_deals_org ON deals(organization_id); CREATE INDEX IF NOT EXISTS idx_deals_stage ON deals(stage); CREATE INDEX IF NOT EXISTS idx_folders_deal ON folders(deal_id); CREATE INDEX IF NOT EXISTS idx_files_deal ON files(deal_id); CREATE INDEX IF NOT EXISTS idx_files_folder ON files(folder_id); CREATE INDEX IF NOT EXISTS idx_requests_deal ON diligence_requests(deal_id); CREATE INDEX IF NOT EXISTS idx_contacts_org ON contacts(organization_id); CREATE INDEX IF NOT EXISTS idx_activity_deal ON deal_activity(deal_id); CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); ` const createInvites = ` CREATE TABLE IF NOT EXISTS invites ( token TEXT PRIMARY KEY, org_id TEXT NOT NULL, email TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'member', invited_by TEXT NOT NULL, expires_at DATETIME NOT NULL, used_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );` const createBuyerGroups = ` CREATE TABLE IF NOT EXISTS buyer_groups ( id TEXT PRIMARY KEY, deal_id TEXT NOT NULL, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );` const createFolderAccess = ` CREATE TABLE IF NOT EXISTS folder_access ( folder_id TEXT NOT NULL, buyer_group TEXT NOT NULL, PRIMARY KEY (folder_id, buyer_group) );` const createContactDeals = ` CREATE TABLE IF NOT EXISTS contact_deals ( contact_id TEXT NOT NULL, deal_id TEXT NOT NULL, PRIMARY KEY (contact_id, deal_id) );` const createFileComments = ` CREATE TABLE IF NOT EXISTS file_comments ( id TEXT PRIMARY KEY, file_id TEXT NOT NULL, deal_id TEXT NOT NULL, user_id TEXT NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );` // Additive migrations - each statement is run individually, errors ignored (for already-existing columns) var additiveMigrationStmts = []string{ // Section 1: org_type `ALTER TABLE organizations ADD COLUMN org_type TEXT DEFAULT 'company'`, // Section 4: industry `ALTER TABLE deals ADD COLUMN industry TEXT DEFAULT ''`, // Section 5: permission controls `ALTER TABLE deals ADD COLUMN buyer_can_comment INTEGER DEFAULT 1`, `ALTER TABLE deals ADD COLUMN seller_can_comment INTEGER DEFAULT 1`, `ALTER TABLE profiles ADD COLUMN buyer_group TEXT DEFAULT ''`, // Section 6: folder sort order `ALTER TABLE folders ADD COLUMN sort_order INTEGER DEFAULT 0`, // Section 7: file storage path `ALTER TABLE files ADD COLUMN storage_path TEXT DEFAULT ''`, // Section 9: buyer-specific requests `ALTER TABLE diligence_requests ADD COLUMN is_buyer_specific INTEGER DEFAULT 0`, `ALTER TABLE diligence_requests ADD COLUMN visible_to_buyer_group TEXT DEFAULT ''`, // Section 13: analytics per-buyer `ALTER TABLE deal_activity ADD COLUMN buyer_group TEXT DEFAULT ''`, `ALTER TABLE deal_activity ADD COLUMN time_spent_seconds INTEGER DEFAULT 0`, } func seed(db *sql.DB) error { stmts := []string{ // Organization `INSERT INTO organizations (id, name, slug) VALUES ('org-1', 'Apex Capital Partners', 'apex-capital')`, // Seed admin profile (password must be set via createadmin tool) `INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES ('admin-misha', 'misha@jongsma.me', 'Misha Jongsma', 'org-1', 'owner', '')`, // Deals `INSERT INTO deals (id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by) VALUES ('deal-1', 'org-1', 'Project Aurora', 'AI-powered enterprise SaaS platform acquisition', 'TechNova Solutions', 'ioi', 45000000, 'USD', '2025-12-15', '2026-01-10', '2026-03-15', '2026-04-30', 72, 'admin-misha'), ('deal-2', 'org-1', 'Project Beacon', 'Healthcare technology platform investment', 'MedFlow Health', 'initial_marketing', 28000000, 'USD', '2026-01-05', '', '', '2026-06-15', 45, 'admin-misha'), ('deal-3', 'org-1', 'Project Cascade', 'Fintech payment processing acquisition', 'PayStream Inc', 'loi', 62000000, 'USD', '2025-11-20', '2026-02-01', '2026-04-01', '2026-05-15', 58, 'admin-misha'), ('deal-4', 'org-1', 'Project Delta', 'Industrial IoT platform strategic investment', 'SensorGrid Corp', 'prospect', 15000000, 'USD', '', '', '', '2026-08-01', 30, 'admin-misha')`, // Folders for deal-1 (Project Aurora) `INSERT INTO folders (id, deal_id, parent_id, name, description) VALUES ('folder-1', 'deal-1', '', 'Financial Documents', 'All financial statements and models'), ('folder-2', 'deal-1', '', 'Legal Documents', 'Contracts, NDAs, and legal agreements'), ('folder-3', 'deal-1', '', 'Technical Due Diligence', 'Technical documentation and audits'), ('folder-4', 'deal-1', 'folder-1', 'Q4 2025 Reports', 'Quarterly financial reports'), ('folder-5', 'deal-2', '', 'Clinical Data', 'Healthcare compliance and clinical data'), ('folder-6', 'deal-2', '', 'Financial Projections', 'Revenue models and projections'), ('folder-7', 'deal-3', '', 'Regulatory Filings', 'Payment processing regulatory documents'), ('folder-8', 'deal-3', '', 'Technology Stack', 'Architecture and infrastructure docs')`, // Files `INSERT INTO files (id, deal_id, folder_id, name, file_size, mime_type, status, uploaded_by) VALUES ('file-1', 'deal-1', 'folder-1', 'Annual_Report_2025.pdf', 2450000, 'application/pdf', 'reviewed', 'admin-misha'), ('file-2', 'deal-1', 'folder-1', 'Revenue_Model_v3.xlsx', 890000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'reviewed', 'admin-misha'), ('file-3', 'deal-1', 'folder-4', 'Q4_Income_Statement.pdf', 1200000, 'application/pdf', 'reviewed', 'admin-misha'), ('file-4', 'deal-1', 'folder-2', 'NDA_Executed.pdf', 450000, 'application/pdf', 'reviewed', 'admin-misha'), ('file-5', 'deal-1', 'folder-2', 'IP_Assignment_Agreement.pdf', 780000, 'application/pdf', 'flagged', 'admin-misha'), ('file-6', 'deal-1', 'folder-3', 'Architecture_Overview.pdf', 3200000, 'application/pdf', 'reviewed', 'admin-misha'), ('file-7', 'deal-1', 'folder-3', 'Security_Audit_2025.pdf', 1800000, 'application/pdf', 'processing', 'admin-misha'), ('file-8', 'deal-2', 'folder-5', 'Clinical_Trial_Results.pdf', 5600000, 'application/pdf', 'uploaded', 'admin-misha'), ('file-9', 'deal-2', 'folder-6', 'Five_Year_Projection.xlsx', 670000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'reviewed', 'admin-misha'), ('file-10', 'deal-3', 'folder-7', 'PCI_Compliance_Cert.pdf', 340000, 'application/pdf', 'reviewed', 'admin-misha'), ('file-11', 'deal-3', 'folder-8', 'System_Architecture.pdf', 2100000, 'application/pdf', 'reviewed', 'admin-misha'), ('file-12', 'deal-3', 'folder-8', 'API_Documentation.pdf', 1500000, 'application/pdf', 'uploaded', 'admin-misha')`, // Diligence requests for deal-1 `INSERT INTO diligence_requests (id, deal_id, item_number, section, description, priority, atlas_status, atlas_note, confidence, buyer_comment, seller_comment, buyer_group) VALUES ('req-1', 'deal-1', '1.1', 'Financial', 'Audited financial statements for last 3 fiscal years', 'high', 'fulfilled', 'Found in Annual_Report_2025.pdf', 95, '', 'Uploaded to Financial Documents folder', 'Meridian Capital'), ('req-2', 'deal-1', '1.2', 'Financial', 'Monthly revenue breakdown by product line', 'high', 'partial', 'Partial data in Revenue_Model_v3.xlsx', 60, 'Need more granular breakdown', 'Working on detailed version', 'Meridian Capital'), ('req-3', 'deal-1', '1.3', 'Financial', 'Customer concentration analysis (top 20)', 'medium', 'missing', '', 0, 'Critical for our valuation model', '', 'Meridian Capital'), ('req-4', 'deal-1', '2.1', 'Legal', 'All material contracts and amendments', 'high', 'fulfilled', 'Located in Legal Documents', 88, '', 'All contracts uploaded', 'Meridian Capital'), ('req-5', 'deal-1', '2.2', 'Legal', 'IP portfolio and patent filings', 'high', 'partial', 'IP Assignment found but patents pending', 45, 'Need complete patent list', 'Patent list being compiled', 'Meridian Capital'), ('req-6', 'deal-1', '3.1', 'Technical', 'System architecture and infrastructure documentation', 'medium', 'fulfilled', 'Architecture_Overview.pdf covers this', 92, '', '', 'Summit Health Equity'), ('req-7', 'deal-1', '3.2', 'Technical', 'Security audit and penetration test results', 'high', 'partial', 'Security audit uploaded, pen test pending', 50, 'When was the last pen test?', 'Scheduled for next month', 'Summit Health Equity'), ('req-8', 'deal-1', '3.3', 'Technical', 'Data privacy and GDPR compliance documentation', 'medium', 'missing', '', 0, '', 'In preparation', 'Summit Health Equity'), ('req-9', 'deal-1', '4.1', 'HR', 'Organization chart and key personnel bios', 'low', 'fulfilled', 'Found in company overview docs', 85, '', '', 'Meridian Capital'), ('req-10', 'deal-1', '4.2', 'HR', 'Employee benefit plans and compensation structure', 'medium', 'not_applicable', 'Deferred to Phase 2', 0, '', 'Will provide in Phase 2', 'Summit Health Equity')`, // Contacts `INSERT INTO contacts (id, organization_id, full_name, email, phone, company, title, contact_type, tags) VALUES ('contact-1', 'org-1', 'Marcus Webb', 'm.webb@alpinecap.com', '+1 415-555-0142', 'Alpine Capital', 'Managing Director', 'buyer', 'active,lead-buyer'), ('contact-2', 'org-1', 'James Liu', 'j.liu@sequoia.com', '+1 650-555-0198', 'Sequoia Partners', 'Vice President', 'buyer', 'nda-signed'), ('contact-3', 'org-1', 'Sarah Park', 's.park@kkr.com', '+1 212-555-0267', 'KKR Growth', 'Principal', 'buyer', 'active'), ('contact-4', 'org-1', 'David Chen', 'd.chen@warburg.com', '+1 212-555-0334', 'Warburg Pincus', 'Director', 'buyer', ''), ('contact-5', 'org-1', 'Rachel Adams', 'r.adams@bain.com', '+1 617-555-0411', 'Bain Capital', 'Associate', 'buyer', 'new'), ('contact-6', 'org-1', 'Michael Torres', 'm.torres@acmecap.com', '+1 415-555-0523', 'Acme Capital', 'CFO', 'internal', 'admin'), ('contact-7', 'org-1', 'Sarah Chen', 's.chen@acmecap.com', '+1 415-555-0678', 'Acme Capital', 'VP Finance', 'internal', 'admin'), ('contact-8', 'org-1', 'Emily Watson', 'e.watson@skadden.com', '+1 212-555-0789', 'Skadden Arps', 'Partner', 'advisor', 'legal-counsel')`, // Activity `INSERT INTO deal_activity (id, organization_id, deal_id, user_id, activity_type, resource_type, resource_name, created_at) VALUES ('act-1', 'org-1', 'deal-1', 'admin-misha', 'upload', 'file', 'Annual_Report_2025.pdf', '2026-02-14 16:35:00'), ('act-2', 'org-1', 'deal-1', 'admin-misha', 'view', 'file', 'Revenue_Model_v3.xlsx', '2026-02-14 16:30:00'), ('act-3', 'org-1', 'deal-1', 'admin-misha', 'edit', 'deal', 'Project Aurora', '2026-02-14 15:20:00'), ('act-4', 'org-1', 'deal-2', 'admin-misha', 'upload', 'file', 'Clinical_Trial_Results.pdf', '2026-02-14 14:10:00'), ('act-5', 'org-1', 'deal-1', 'admin-misha', 'download', 'file', 'NDA_Executed.pdf', '2026-02-14 13:00:00'), ('act-6', 'org-1', 'deal-3', 'admin-misha', 'upload', 'file', 'PCI_Compliance_Cert.pdf', '2026-02-13 10:00:00'), ('act-7', 'org-1', 'deal-1', 'admin-misha', 'comment', 'request', 'Customer concentration analysis', '2026-02-13 09:00:00'), ('act-8', 'org-1', 'deal-1', 'admin-misha', 'view', 'folder', 'Financial Documents', '2026-02-12 16:00:00')`, } for i, stmt := range stmts { if _, err := db.Exec(stmt); err != nil { return fmt.Errorf("seed statement %d failed: %w", i+1, err) } } return nil }