341 lines
16 KiB
Go
341 lines
16 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,
|
|
}
|
|
|
|
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 'pipeline' CHECK (stage IN ('pipeline','loi','initial_review','due_diligence','final_negotiation','closed','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 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', 'due_diligence', 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_review', 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', 'pipeline', 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
|
|
}
|