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>
83 lines
4.8 KiB
JavaScript
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;
|
|
`);
|
|
};
|