Integra módulo de CRM (leads/opportunities); corrige 2 bugs reais do agente

- Wire LeadsModule/OpportunitiesModule em app.module.ts; adiciona os
  exports correspondentes ao barrel @eden/database.

Bugs reais encontrados e corrigidos antes do commit (não pegos pelo
próprio agente porque ele não tinha o módulo de auth disponível no
worktree dele para testar de verdade — só stubado e revertido):

1. Os controllers de Leads/Opportunities liam `req.user?.id`, mas nosso
   JwtAuthGuard real popula `request.edenUser`, nunca `request.user` —
   todo evento de auditoria de lead/oportunidade estaria gravando
   actorId: null sempre, perdendo a atribuição de quem fez a ação.
   Trocado para o padrão real (@CurrentUser() + EdenAuthenticatedUser).
2. As chamadas appendAuditEvent() nos dois services omitiam o campo
   obrigatório `actorType` — corrigido para "user" em todas as 6
   ocorrências.

Também corrigidos: import de guards/decorators via um barrel
"../auth" inexistente (trocado pelos caminhos reais), e os dois módulos
não importavam AuthModule (necessário para o Nest resolver as
dependências do JwtAuthGuard via DI).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 11:02:40 -03:00
16 changed files with 1036 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
/**
* 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;
`);
};