dealroom/internal/db/migrate.go

349 lines
17 KiB
Go

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
}