Files
eden/packages/database/migrations/1788445280742_quotes.cjs
Matheus (Handix) 18838a9031 feat(api): Quotes, Client Registration e esqueleto de Contratos
Fecha o núcleo comercial da Fase 2/3: motor de precificação de Ofertas
(feito diretamente, dado o risco de erro de arredondamento/regra de
negócio), Cadastro de Cliente e esqueleto de Contratos (via 2 subagentes
em paralelo, salvos manualmente após ambos baterem no limite de gasto
mensal da conta — trabalho não commitado nos worktrees foi revisado
arquivo a arquivo e copiado para main, com as peças faltantes
completadas diretamente).

- Quotes: pricing.ts porta as fórmulas do legado (eden.md §7/§8) —
  needsApproval, faixas de condição de pagamento, efeito legal da
  fidelidade condicionado a approval_status==='approved'. Fluxo
  criar→decidir aprovação→fechar negócio (vinculado a um cadastro de
  cliente, com guarda contra fechamento duplo).
- Client Registration: cadastro direto (eden.md §3), MVP autenticado-only
  (sem token público de autoatendimento, upload ou lookup CNPJ/CEP nesta
  fase — documentado em comentário).
- Contracts: máquina de estados do ADR-0006 imposta via SELECT...FOR
  UPDATE + tabela de transições, com signed_at/activated_at e histórico
  de status; 4 das 11 tabelas do ADR-0006 (as demais adiadas).

2 bugs próprios corrigidos antes do commit: UpdateQuoteInput usava
COALESCE para campos financeiros (undefined do JS apagaria valor em
update parcial) — campos tornados obrigatórios; User.reseller_id/
can_approve_discount faltavam em types.ts apesar das colunas já
existirem no banco desde migrations anteriores.

Verificado: pnpm turbo run typecheck build (28/28), pnpm test (15/15),
e todo o fluxo testado ao vivo via curl contra a stack Docker real,
incluindo cenários de erro (transição de status inválida, FK de produto
inexistente, fechamento duplo, 401 sem token) e trilha de auditoria
conferida no Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 12:25:49 -03:00

83 lines
4.8 KiB
JavaScript

/**
* Commercial bounded context: quotes (ofertas) — the pricing/fidelity/
* approval core of the legacy system, eden.md module 2. Ported with exact
* fidelity to the formulas documented there (see apps/api/src/quotes/
* pricing.ts for the calculation engine itself).
*
* `client_registration_id` is added as a plain nullable UUID WITHOUT a
* foreign key — the Customer 360 module (client_registrations table) is
* being built in parallel by another agent and doesn't exist yet in this
* migration's context. The FK is added in a follow-up migration once both
* land (this mirrors the legacy's own history: client_registration_id was
* itself added to quotes in a later migration, 1785513008756).
*
* `users.can_approve_discount` is added here too (small, single-column
* change) since it's exclusively needed by the discount-approval rule
* this migration's quotes table exists to support — eden.md §1.1:
* `canApproveDiscount(user) = super_admin || (admin && can_approve_discount)`.
*/
exports.shorthands = undefined;
exports.up = (pgm) => {
pgm.sql(`
ALTER TABLE users ADD COLUMN can_approve_discount BOOLEAN NOT NULL DEFAULT false;
COMMENT ON COLUMN users.can_approve_discount IS
'Business flag (not a role) — an admin can approve a quote''s discount/reduced-fidelity exception only if this is true; super_admin always can, regardless of this flag. eden.md §1.1, §3.3.';
CREATE SEQUENCE quotes_quote_number_seq START 1;
CREATE TABLE quotes (
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),
quote_number TEXT NOT NULL UNIQUE DEFAULT ('Q-' || to_char(now(), 'YYYY') || '-' || lpad(nextval('quotes_quote_number_seq')::text, 6, '0')),
reseller_id UUID NOT NULL REFERENCES resellers(id),
client_name TEXT NOT NULL,
client_company TEXT,
client_email TEXT,
client_phone TEXT NOT NULL,
contract_period INTEGER NOT NULL DEFAULT 0 CHECK (contract_period IN (0,12,24,36,48)),
fidelity_period INTEGER CHECK (fidelity_period IS NULL OR (fidelity_period >= 0 AND fidelity_period <= contract_period)),
items JSONB NOT NULL DEFAULT '[]'::jsonb,
implementation_fee NUMERIC(12,2) NOT NULL DEFAULT 0,
impl_geral NUMERIC(12,2) NOT NULL DEFAULT 0,
monthly_total NUMERIC(12,2) NOT NULL DEFAULT 0,
contract_total NUMERIC(12,2) NOT NULL DEFAULT 0,
proposed_monthly_total NUMERIC(12,2),
approval_status TEXT NOT NULL DEFAULT 'none' CHECK (approval_status IN ('none','pending','approved','rejected')),
approval_notes TEXT,
approved_by_id UUID REFERENCES users(id),
notes TEXT,
deal_status TEXT NOT NULL DEFAULT 'orcamento' CHECK (deal_status IN ('orcamento','fechado','perdido')),
impl_payment_condition TEXT NOT NULL DEFAULT 'À vista' CHECK (impl_payment_condition IN ('À vista','1+1','1+2')),
client_registration_id UUID
);
COMMENT ON TABLE quotes IS
'Ofertas comerciais — eden.md módulo 2. contract_period é a faixa de preço; fidelity_period é a permanência efetiva quando MENOR que a faixa, só produz efeito legal (vigência/multa) depois de approval_status=approved (eden.md §3.4). client_registration_id referencia client_registrations (Customer 360) sem FK por enquanto — ver comentário no topo desta migration.';
COMMENT ON COLUMN quotes.client_registration_id IS
'Setar este campo trava a oferta para edição (exceto super_admin) — eden.md §5.1. FK a ser adicionada em migration futura quando client_registrations existir.';
COMMENT ON COLUMN quotes.impl_payment_condition IS
'À vista / 1+1 / 1+2 — progressivo por faixa de valor de implantação, eden.md §7 (paymentConditions.js). Faixas validadas na aplicação, não aqui.';
CREATE TRIGGER trg_quotes_updated_at BEFORE UPDATE ON quotes
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE INDEX idx_quotes_reseller ON quotes(reseller_id);
CREATE INDEX idx_quotes_deal_status ON quotes(deal_status);
CREATE INDEX idx_quotes_created_by ON quotes(created_by);
CREATE INDEX idx_quotes_client_registration ON quotes(client_registration_id);
ALTER SEQUENCE quotes_quote_number_seq OWNED BY NONE;
`);
};
exports.down = (pgm) => {
pgm.sql(`
DROP TABLE IF EXISTS quotes;
DROP SEQUENCE IF EXISTS quotes_quote_number_seq;
ALTER TABLE users DROP COLUMN IF EXISTS can_approve_discount;
`);
};