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>
This commit is contained in:
2026-09-03 12:25:49 -03:00
parent e8014b4135
commit 18838a9031
36 changed files with 3199 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
import type { QuoteItem } from "@eden/database";
/**
* Quote pricing/fidelity/approval engine — ported verbatim from eden.md §8
* (formulas) and §3–§4 (fidelity/discount business rules). These formulas
* are exact, not approximate — do not "simplify" without an ADR. The
* backend is always the source of truth (Master Prompt §6.4: "nunca
* confiar no frontend para cálculo financeiro"); the frontend may mirror
* this logic for UX, but every quote write recomputes from scratch here.
*/
export interface PricingInput {
items: QuoteItem[];
contractPeriod: number;
fidelityPeriod: number | null;
implGeral: number;
proposedMonthlyTotal: number | null;
}
export interface PricingResult {
monthlyTotal: number;
implFromProducts: number;
totalImpl: number;
hasSpecialPrice: boolean;
isDiscount: boolean;
isMarkup: boolean;
diffMonthly: number;
effectiveMonthly: number;
hasReducedFidelity: boolean;
needsApproval: boolean;
contractTotal: number;
/** What to persist to quotes.fidelity_period — null unless it's genuinely a reduced-fidelity exception (eden.md §8 "só persiste se for de fato menor que a faixa"). */
persistedFidelityPeriod: number | null;
/** What to persist to quotes.proposed_monthly_total — null unless a special price is actually active. */
persistedProposedMonthlyTotal: number | null;
}
export function computeQuotePricing(input: PricingInput): PricingResult {
const monthlyTotal = round2(input.items.reduce((sum, item) => sum + item.total, 0));
const implFromProducts = round2(input.items.reduce((sum, item) => sum + item.implTotal, 0));
const totalImpl = round2(implFromProducts + (input.implGeral || 0));
const proposedMonthly = input.proposedMonthlyTotal;
const hasSpecialPrice =
proposedMonthly != null && proposedMonthly > 0 && proposedMonthly !== monthlyTotal;
const isDiscount = hasSpecialPrice && proposedMonthly! < monthlyTotal;
const isMarkup = hasSpecialPrice && proposedMonthly! > monthlyTotal;
const diffMonthly = hasSpecialPrice ? round2(monthlyTotal - proposedMonthly!) : 0;
const effectiveMonthly = hasSpecialPrice ? proposedMonthly! : monthlyTotal;
const hasReducedFidelity =
input.fidelityPeriod != null && input.fidelityPeriod < input.contractPeriod;
const needsApproval = isDiscount || hasReducedFidelity;
const contractTotal = round2(effectiveMonthly * (input.contractPeriod || 1) + totalImpl);
return {
monthlyTotal,
implFromProducts,
totalImpl,
hasSpecialPrice,
isDiscount,
isMarkup,
diffMonthly,
effectiveMonthly,
hasReducedFidelity,
needsApproval,
contractTotal,
persistedFidelityPeriod: hasReducedFidelity ? input.fidelityPeriod : null,
persistedProposedMonthlyTotal: hasSpecialPrice ? proposedMonthly : null,
};
}
/**
* Resolves what `approval_status` should be persisted, given the previous
* status and whether the actor is submitting an approval decision.
* Mirrors eden.md §8: `needsApproval ? (approvalStatus === 'none' ?
* 'pending' : approvalStatus) : 'none'` — but generalized to accept an
* explicit decision (approve/reject) from a privileged actor, which the
* legacy handled as a separate code path (§3.3) that this folds in here
* for a single source of truth.
*/
export function resolveApprovalStatus(
needsApproval: boolean,
previousStatus: "none" | "pending" | "approved" | "rejected",
decision?: "approved" | "rejected",
): "none" | "pending" | "approved" | "rejected" {
if (!needsApproval) return "none";
if (decision) return decision;
return previousStatus === "none" ? "pending" : previousStatus;
}
/** eden.md §3.3: super_admin always; admin only with the explicit per-user flag — never inferred from role alone. */
export function canApproveDiscount(user: { roleKey: string; canApproveDiscount?: boolean }): boolean {
if (user.roleKey === "super_admin") return true;
return user.roleKey === "admin" && user.canApproveDiscount === true;
}
/**
* The fidelity that actually has legal effect (vigência/vencimento/multa)
* — eden.md §3.4/§10.4, repeated everywhere in the legacy as the single
* most important rule to preserve: a pending/rejected exception NEVER
* affects anything legal, only an approved one does.
*/
export function resolveEffectiveFidelity(
contractPeriod: number,
fidelityPeriod: number | null,
approvalStatus: "none" | "pending" | "approved" | "rejected",
): number {
return fidelityPeriod != null && approvalStatus === "approved" ? fidelityPeriod : contractPeriod || 0;
}
/** eden.md §7 (paymentConditions.js) — progressive by implementation value. */
export const PAYMENT_CONDITIONS = ["À vista", "1+1", "1+2"] as const;
export function getAvailablePaymentConditions(implTotal: number): readonly string[] {
const value = implTotal || 0;
if (value <= 1500) return ["À vista"];
if (value <= 3000) return ["À vista", "1+1"];
return PAYMENT_CONDITIONS;
}
export interface SavingsResult {
totalDescontoMensal: number;
totalEconomiaMensal: number;
economiaTotalContrato: number;
beneficioTotalFidelidade: number;
}
/**
* eden.md §3.5/§4.4 — two DIFFERENT numbers that must never be confused:
* `beneficioTotalFidelidade` (fidelity discount only, used as the legal
* cap for the RGC art. 37 early-termination penalty) vs.
* `totalEconomiaMensal`/`economiaTotalContrato` (fidelity + special
* discount combined, shown as commercial "savings" info to the client).
*/
export function computeSavings(
items: QuoteItem[],
contractPeriod: number,
effectiveFidelityPeriod: number,
monthlyTotal: number,
proposedMonthlyTotal: number | null,
): SavingsResult {
const totalDescontoMensal = round2(
items.reduce((sum, item) => sum + ((item.price0 ?? 0) - item.unitPrice) * item.quantity, 0),
);
const hasSpecialPrice =
proposedMonthlyTotal != null && proposedMonthlyTotal > 0 && proposedMonthlyTotal !== monthlyTotal;
const specialDiscount = hasSpecialPrice ? round2(monthlyTotal - proposedMonthlyTotal!) : 0;
const totalEconomiaMensal = round2(totalDescontoMensal + specialDiscount);
const economiaTotalContrato =
totalEconomiaMensal > 0 && contractPeriod > 0 ? round2(totalEconomiaMensal * contractPeriod) : 0;
const beneficioTotalFidelidade = round2(totalDescontoMensal * effectiveFidelityPeriod);
return { totalDescontoMensal, totalEconomiaMensal, economiaTotalContrato, beneficioTotalFidelidade };
}
function round2(value: number): number {
return Math.round((value + Number.EPSILON) * 100) / 100;
}