Greenfield for EDEN — eden.md has no CRM module, so this is driven by the
Master Prompt's requirements, not a legacy port.
- Migration 1788439012582_crm.cjs: leads + opportunities tables, free-text
status/stage (documented via COMMENT ON COLUMN, no CHECK yet — a
configurable pipeline is a future refinement), indexes on status/owner_id
and stage/owner_id, set_updated_at triggers reusing the baseline
schema's trigger function.
- packages/database/src/repositories/{leads,opportunities}.ts: list/get/
create/update per companies.ts conventions (COALESCE-based partial
update), plus convertLeadToOpportunity(leadId, input) wrapped in
withTransaction — inserts the opportunity (lead_id set) and marks the
lead 'converted' atomically.
- apps/api/src/{leads,opportunities}/: NestJS modules mirroring the
Companies module structure (controller/service/module/dto). Routes
v1/leads and v1/opportunities. Because leads/opportunities carry
prospect PII (unlike the open-read Companies/Products catalog), reads
are also gated behind @RequirePermission('<resource>', 'view'), not
just JwtAuthGuard. Writes require '<resource>', 'edit'.
POST /v1/leads/:id/convert gated by 'leads'/'edit'. Audit events:
lead.created/updated/converted, opportunity.created/updated.
- Added class-validator/class-transformer to apps/api/package.json
(needed for the DTOs; not previously a direct dependency).
Known cross-agent dependency gap (by design, not a bug in this code):
this worktree branched before any sibling agent's work landed, so
apps/api/src/auth/ (JwtAuthGuard, PermissionGuard, RequirePermission) and
the Companies module referenced as this task's template do not exist yet
here, and packages/database/src/index.ts / apps/api/src/app.module.ts are
explicitly off-limits for me to edit (merge-conflict avoidance across the
4 parallel agents). `pnpm --filter @eden/database typecheck/build` pass
unconditionally. `pnpm --filter @eden/api typecheck/build` currently fail
with exactly two error classes: "Cannot find module '../auth'" and
"@eden/database has no exported member <leads/opportunities repo fns,
auditRepo>" — both expected to resolve once the auth module and the
index.ts/app.module.ts wiring land at merge time. Verified locally (not
committed) by stubbing both gaps: with the stub in place, both typecheck
and build pass with zero errors, confirming this module's own code is
correct against the documented interfaces.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
94 lines
4.2 KiB
JavaScript
94 lines
4.2 KiB
JavaScript
/**
|
|
* 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;
|
|
`);
|
|
};
|