/ db / migrations / 037-add-missing-outreach-statuses.sql
037-add-missing-outreach-statuses.sql
 1  -- Migration 037: Add Missing Outreach Statuses
 2  -- Created: 2026-02-10
 3  -- Purpose: Add 'rework' and 'gdpr_blocked' statuses to outreaches table CHECK constraint
 4  --
 5  -- Background:
 6  -- - proposal-generator-v2.js uses 'rework' status but it was never added to schema
 7  -- - Migration 028 added 'gdpr_blocked' but was never run
 8  -- - Need to consolidate all valid statuses in one migration
 9  
10  -- SQLite doesn't support ALTER TABLE ... ALTER COLUMN to modify CHECK constraints
11  -- We need to recreate the table with the new constraint
12  
13  -- Step 1: Temporarily disable foreign key constraints during migration
14  PRAGMA foreign_keys = OFF;
15  
16  -- Step 2: Create new table with updated CHECK constraint
17  CREATE TABLE outreaches_new (
18      id INTEGER PRIMARY KEY AUTOINCREMENT,
19      site_id INTEGER NOT NULL,
20      variant_number INTEGER,  -- No CHECK constraint - allows any variant number
21      contact_method TEXT NOT NULL CHECK(contact_method IN (
22          'sms', 'email', 'form', 'x', 'linkedin'
23      )),
24      contact_uri TEXT NOT NULL,
25      our_account TEXT,
26      proposal_text TEXT NOT NULL,
27      status TEXT DEFAULT 'pending' CHECK(status IN (
28          'pending', 'approved', 'rework', 'sent', 'delivered', 'failed',
29          'bounced', 'opened', 'clicked', 'replied', 'scheduled',
30          'gdpr_blocked', 'gov_blocked', 'no_message_button'
31      )),
32      error_message TEXT,
33      rework_instructions TEXT,
34      sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
35      delivered_at TIMESTAMP,
36      resulted_in_sale BOOLEAN DEFAULT 0,
37      sale_amount DECIMAL(10, 2),
38      tracking_clicked_at DATETIME,
39      opened_at DATETIME,
40      email_id TEXT,
41      unsubscribed BOOLEAN DEFAULT 0,
42      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
43      subject_line TEXT,
44      exported_at DATETIME DEFAULT NULL,
45      FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
46      UNIQUE(site_id, contact_method, contact_uri)
47  );
48  
49  -- Step 3: Copy data from old table
50  INSERT INTO outreaches_new (
51      id, site_id, variant_number, contact_method, contact_uri, our_account,
52      proposal_text, status, error_message, rework_instructions, sent_at,
53      delivered_at, resulted_in_sale, sale_amount, tracking_clicked_at,
54      opened_at, email_id, unsubscribed, created_at, subject_line, exported_at
55  )
56  SELECT
57      id, site_id, variant_number, contact_method, contact_uri, our_account,
58      proposal_text, status, error_message, rework_instructions, sent_at,
59      delivered_at, resulted_in_sale, sale_amount, tracking_clicked_at,
60      opened_at, email_id, unsubscribed, created_at, subject_line, exported_at
61  FROM outreaches;
62  
63  -- Step 4: Drop old table
64  DROP TABLE outreaches;
65  
66  -- Step 5: Rename new table
67  ALTER TABLE outreaches_new RENAME TO outreaches;
68  
69  -- Step 6: Re-enable foreign key constraints
70  PRAGMA foreign_keys = ON;
71  
72  -- Step 7: Recreate indexes
73  CREATE INDEX IF NOT EXISTS idx_outreaches_site_id ON outreaches(site_id);
74  CREATE INDEX IF NOT EXISTS idx_outreaches_status ON outreaches(status);
75  CREATE INDEX IF NOT EXISTS idx_outreaches_contact_method ON outreaches(contact_method);
76  
77  -- Step 8: Create indexes for specific statuses
78  CREATE INDEX IF NOT EXISTS idx_outreaches_gdpr_blocked ON outreaches(status) WHERE status = 'gdpr_blocked';
79  CREATE INDEX IF NOT EXISTS idx_outreaches_rework ON outreaches(status) WHERE status = 'rework';
80  CREATE INDEX IF NOT EXISTS idx_outreaches_pending ON outreaches(status) WHERE status = 'pending';
81  CREATE INDEX IF NOT EXISTS idx_outreaches_exported_at ON outreaches(exported_at) WHERE exported_at IS NULL;
82  
83  -- Documentation:
84  -- Valid statuses:
85  -- - pending: Awaiting operator approval
86  -- - approved: Approved but not yet sent
87  -- - rework: Marked for regeneration with feedback (used by proposal-generator-v2.js)
88  -- - sent: Successfully sent to recipient
89  -- - delivered: Confirmed delivery (email webhooks)
90  -- - failed: Delivery failed
91  -- - bounced: Email bounced
92  -- - opened: Email opened (tracking)
93  -- - clicked: Link clicked (tracking)
94  -- - replied: Recipient replied
95  -- - scheduled: Scheduled for future delivery
96  -- - gdpr_blocked: GDPR compliance blocked sending (unverified company email in GDPR country)
97  -- - no_message_button: X/LinkedIn profile has no DM button