/** * CRM — Leads & Opportunities (Master Prompt §6.2 "CRM e Comercial"). * * Greenfield for EDEN: the legacy OrçaFácil system (eden.md) has no CRM * module — it starts directly at Quotes. This migration is driven entirely * by the Master Prompt's requirements, not a port of legacy behavior. * * MVP scope: `leads` and `opportunities` with free-text `status`/`stage` * columns (documented via COMMENT ON COLUMN, no CHECK constraint yet — a * fully configurable pipeline is a future refinement, not this MVP). * * `leads.company_name` is a free-text prospect company name — it is * deliberately NOT a foreign key to the `companies` table, which models * issuing legal entities, not sales prospects. * * The `set_updated_at()` trigger function is created by the baseline * schema migration (1788430575858_baseline-schema.cjs) — reused here, not * redefined. */ exports.shorthands = undefined; exports.up = (pgm) => { pgm.sql(` -- ===================================================================== -- leads -- ===================================================================== CREATE TABLE leads ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID REFERENCES users(id), source TEXT, contact_name TEXT NOT NULL, contact_email TEXT, contact_phone TEXT, company_name TEXT, status TEXT NOT NULL DEFAULT 'new', owner_id UUID REFERENCES users(id), notes TEXT ); COMMENT ON COLUMN leads.source IS 'Free-text origin/campaign (e.g. "indicação", "site", "feira X") — no fixed taxonomy in this MVP.'; COMMENT ON COLUMN leads.company_name IS 'Free-text prospect company name. Deliberately NOT a FK to companies — that table models issuing legal entities, not sales prospects.'; COMMENT ON COLUMN leads.status IS 'Plain TEXT, no CHECK constraint yet. Intended values: new, contacted, qualified, converted, discarded. converted is set only via POST /v1/leads/:id/convert.'; COMMENT ON COLUMN leads.owner_id IS 'Responsible salesperson.'; CREATE INDEX idx_leads_status ON leads(status); CREATE INDEX idx_leads_owner ON leads(owner_id); CREATE TRIGGER trg_leads_updated_at BEFORE UPDATE ON leads FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ===================================================================== -- opportunities -- ===================================================================== CREATE TABLE opportunities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID REFERENCES users(id), lead_id UUID REFERENCES leads(id), title TEXT NOT NULL, stage TEXT NOT NULL DEFAULT 'prospecting', probability INTEGER CHECK (probability >= 0 AND probability <= 100), owner_id UUID REFERENCES users(id), loss_reason TEXT, notes TEXT ); COMMENT ON COLUMN opportunities.lead_id IS 'Nullable — an opportunity can be created directly, not only converted from a lead.'; COMMENT ON COLUMN opportunities.stage IS 'Plain TEXT, no CHECK constraint yet. Intended values: prospecting, qualification, proposal, negotiation, won, lost. A fully configurable pipeline is a future refinement, not this MVP.'; COMMENT ON COLUMN opportunities.loss_reason IS 'Nullable — filled only when stage = ''lost''. Not enforced by a DB constraint in this MVP.'; COMMENT ON COLUMN opportunities.owner_id IS 'Responsible salesperson.'; CREATE INDEX idx_opportunities_stage ON opportunities(stage); CREATE INDEX idx_opportunities_owner ON opportunities(owner_id); CREATE TRIGGER trg_opportunities_updated_at BEFORE UPDATE ON opportunities FOR EACH ROW EXECUTE FUNCTION set_updated_at(); `); }; exports.down = (pgm) => { pgm.sql(` DROP TABLE IF EXISTS opportunities; DROP TABLE IF EXISTS leads; `); };