feat(quotes): anexo de conformidade ANATEL/RGC no PDF de oferta
Porta o anexo regulatório do legado (Resolução Anatel 765/2023, RGC, recorte STFC) com fidelidade total ao conteúdo legal, incluindo a seção de tarifação — em vez do html2canvas+jsPDF client-side do legado, usa o Chromium server-side já construído para Documents. Schema: products ganha service_type/metered/tariff_rates/has_ldi (migration 1788525270647) — campos que a migration original de products tinha deliberadamente deferido. Snapshot no item da oferta na criação (nunca referência viva ao produto). Novo pacote compartilhado packages/domain-shared/src/telecom para o catálogo de tipos e legendas de tarifação. Endpoint GET /v1/quotes/:id/anatel-pdf (400 sem item STFC), sempre renderizado ao vivo (nunca cacheado, já que o conteúdo deriva de updated_at). Frontend: campos de produto/oferta necessários + "Baixar PDF ANATEL" na tela de detalhe da oferta. Inclui também o botão de download de PDF em Documentos (endpoint já existia, faltava o link na UI). Verificado ao vivo através do container real: PDF gerado com Chromium de verdade, texto extraído confere byte a byte com os valores computados (multa, datas, tarifas, resumo financeiro). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { query } from "../pool.js";
|
||||
import type { ServiceType, TariffRates } from "@eden/domain-shared";
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
@@ -25,6 +26,11 @@ export interface Product {
|
||||
price_36: string | null;
|
||||
price_48: string | null;
|
||||
impl_unit_price: string;
|
||||
service_type: ServiceType;
|
||||
metered: boolean;
|
||||
minutes_allowance: string | null;
|
||||
tariff_rates: TariffRates;
|
||||
has_ldi: boolean;
|
||||
}
|
||||
|
||||
export async function listProducts(includeInactive = false): Promise<Product[]> {
|
||||
@@ -61,6 +67,11 @@ export interface CreateProductInput {
|
||||
price36?: number | null;
|
||||
price48?: number | null;
|
||||
implUnitPrice?: number;
|
||||
serviceType?: ServiceType;
|
||||
metered?: boolean;
|
||||
minutesAllowance?: number | null;
|
||||
tariffRates?: TariffRates;
|
||||
hasLdi?: boolean;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
|
||||
@@ -69,11 +80,13 @@ export async function createProduct(input: CreateProductInput): Promise<Product>
|
||||
`INSERT INTO products (
|
||||
name, description, product_type, product_group, product_subgroup, brand, unit,
|
||||
active, discontinued, tracks_stock, tracks_serial, tracks_asset_tag, tracks_mac,
|
||||
price_0, price_12, price_24, price_36, price_48, impl_unit_price, created_by
|
||||
price_0, price_12, price_24, price_36, price_48, impl_unit_price,
|
||||
service_type, metered, minutes_allowance, tariff_rates, has_ldi, created_by
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,COALESCE($7,'unidade'),
|
||||
COALESCE($8,true),COALESCE($9,false),COALESCE($10,false),COALESCE($11,false),COALESCE($12,false),COALESCE($13,false),
|
||||
$14,$15,$16,$17,$18,COALESCE($19,0),$20
|
||||
$14,$15,$16,$17,$18,COALESCE($19,0),
|
||||
COALESCE($20,'STFC'),COALESCE($21,false),$22,COALESCE($23,'{}'::jsonb),COALESCE($24,false),$25
|
||||
)
|
||||
RETURNING *`,
|
||||
[
|
||||
@@ -96,6 +109,11 @@ export async function createProduct(input: CreateProductInput): Promise<Product>
|
||||
input.price36 ?? null,
|
||||
input.price48 ?? null,
|
||||
input.implUnitPrice ?? null,
|
||||
input.serviceType ?? null,
|
||||
input.metered ?? null,
|
||||
input.minutesAllowance ?? null,
|
||||
input.tariffRates != null ? JSON.stringify(input.tariffRates) : null,
|
||||
input.hasLdi ?? null,
|
||||
input.createdBy ?? null,
|
||||
],
|
||||
);
|
||||
@@ -127,7 +145,12 @@ export async function updateProduct(id: string, input: UpdateProductInput): Prom
|
||||
price_24 = COALESCE($17, price_24),
|
||||
price_36 = COALESCE($18, price_36),
|
||||
price_48 = COALESCE($19, price_48),
|
||||
impl_unit_price = COALESCE($20, impl_unit_price)
|
||||
impl_unit_price = COALESCE($20, impl_unit_price),
|
||||
service_type = COALESCE($21, service_type),
|
||||
metered = COALESCE($22, metered),
|
||||
minutes_allowance = COALESCE($23, minutes_allowance),
|
||||
tariff_rates = COALESCE($24, tariff_rates),
|
||||
has_ldi = COALESCE($25, has_ldi)
|
||||
WHERE id = $1
|
||||
RETURNING *`,
|
||||
[
|
||||
@@ -151,6 +174,11 @@ export async function updateProduct(id: string, input: UpdateProductInput): Prom
|
||||
input.price36 ?? null,
|
||||
input.price48 ?? null,
|
||||
input.implUnitPrice ?? null,
|
||||
input.serviceType ?? null,
|
||||
input.metered ?? null,
|
||||
input.minutesAllowance ?? null,
|
||||
input.tariffRates != null ? JSON.stringify(input.tariffRates) : null,
|
||||
input.hasLdi ?? null,
|
||||
],
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { query } from "../pool.js";
|
||||
import type { ServiceType, TariffRates } from "@eden/domain-shared";
|
||||
|
||||
export interface QuoteItem {
|
||||
productId: string;
|
||||
@@ -13,6 +14,18 @@ export interface QuoteItem {
|
||||
price24: number | null;
|
||||
price36: number | null;
|
||||
price48: number | null;
|
||||
/**
|
||||
* Snapshot do produto no momento em que o item foi adicionado à oferta
|
||||
* (não uma referência viva) — eden.md/legado: editar o produto depois
|
||||
* nunca muda retroativamente a disclosure regulatória de uma oferta já
|
||||
* emitida. Usado só pelo anexo de conformidade ANATEL
|
||||
* (apps/api/src/quotes/anatel-offer-template.ts).
|
||||
*/
|
||||
serviceType: ServiceType;
|
||||
metered: boolean;
|
||||
minutesAllowance: number | null;
|
||||
tariffRates: TariffRates;
|
||||
hasLdi: boolean;
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
@@ -41,6 +54,7 @@ export interface Quote {
|
||||
deal_status: "orcamento" | "fechado" | "perdido";
|
||||
impl_payment_condition: "À vista" | "1+1" | "1+2";
|
||||
client_registration_id: string | null;
|
||||
sales_rep_name: string | null;
|
||||
}
|
||||
|
||||
export interface ListQuotesFilter {
|
||||
@@ -97,6 +111,7 @@ export interface CreateQuoteInput {
|
||||
approvalStatus: "none" | "pending" | "approved" | "rejected";
|
||||
implPaymentCondition: string;
|
||||
notes?: string | null;
|
||||
salesRepName?: string | null;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
|
||||
@@ -106,8 +121,8 @@ export async function createQuote(input: CreateQuoteInput): Promise<Quote> {
|
||||
reseller_id, client_name, client_company, client_email, client_phone,
|
||||
contract_period, fidelity_period, items, implementation_fee, impl_geral,
|
||||
monthly_total, contract_total, proposed_monthly_total, approval_status,
|
||||
impl_payment_condition, notes, created_by
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
|
||||
impl_payment_condition, notes, sales_rep_name, created_by
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
|
||||
RETURNING *`,
|
||||
[
|
||||
input.resellerId,
|
||||
@@ -126,6 +141,7 @@ export async function createQuote(input: CreateQuoteInput): Promise<Quote> {
|
||||
input.approvalStatus,
|
||||
input.implPaymentCondition,
|
||||
input.notes ?? null,
|
||||
input.salesRepName ?? null,
|
||||
input.createdBy ?? null,
|
||||
],
|
||||
);
|
||||
@@ -167,6 +183,7 @@ export interface UpdateQuoteInput {
|
||||
approvalNotes?: string | null;
|
||||
implPaymentCondition?: string;
|
||||
notes?: string | null;
|
||||
salesRepName?: string | null;
|
||||
}
|
||||
|
||||
export async function updateQuote(id: string, input: UpdateQuoteInput): Promise<Quote | null> {
|
||||
@@ -188,7 +205,8 @@ export async function updateQuote(id: string, input: UpdateQuoteInput): Promise<
|
||||
approved_by_id = $15,
|
||||
approval_notes = COALESCE($16, approval_notes),
|
||||
impl_payment_condition = COALESCE($17, impl_payment_condition),
|
||||
notes = COALESCE($18, notes)
|
||||
notes = COALESCE($18, notes),
|
||||
sales_rep_name = COALESCE($19, sales_rep_name)
|
||||
WHERE id = $1
|
||||
RETURNING *`,
|
||||
[
|
||||
@@ -210,6 +228,7 @@ export async function updateQuote(id: string, input: UpdateQuoteInput): Promise<
|
||||
input.approvalNotes ?? null,
|
||||
input.implPaymentCondition ?? null,
|
||||
input.notes ?? null,
|
||||
input.salesRepName ?? null,
|
||||
],
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
|
||||
Reference in New Issue
Block a user