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:
2026-09-04 10:33:33 -03:00
parent 29430d0d96
commit ccac936372
26 changed files with 1321 additions and 20 deletions

View File

@@ -0,0 +1,61 @@
/**
* Adds the legacy telecom-billing specifics that 1788438907074_products.cjs
* deliberately deferred ("service_type/metered/tariff_rates/ixc_product_code
* — those are legacy telecom-billing specifics that belong to a later
* phase, not invented here"). That later phase is now: the ANATEL/RGC
* compliance annex (Resolução Anatel nº 765/2023) needs to know which
* products are STFC (art. 21 §3º), whether they're metered per-minute,
* and their LC/LDN/VC1/VC2/VC3/LDI tariff rates to print the required
* disclosure. Column shapes and the 4-value service_type list are ported
* with exact fidelity from the legacy's baseline-schema.js products table.
*
* `quotes.sales_rep_name` rides along here (small, single-column, same
* ANATEL-annex consumer — printed in the document's signature block) for
* the same reason can_approve_discount rode along in 1788445280742_quotes.
*
* service_type/metered/minutes_allowance/tariff_rates/has_ldi are snapshot
* onto quotes.items (a JSONB blob, no schema change needed there) at
* quote-creation time — see apps/api/src/quotes/quotes.service.ts
* buildItems() — so a later product edit never retroactively changes an
* already-issued quote's regulatory disclosure (ported from the legacy's
* same snapshot behavior, NewQuote.jsx).
*/
exports.shorthands = undefined;
exports.up = (pgm) => {
pgm.sql(`
ALTER TABLE products
ADD COLUMN service_type TEXT NOT NULL DEFAULT 'STFC'
CHECK (service_type IN ('STFC', 'Serviço PABX', 'SVA (locação)', 'Serviço software')),
ADD COLUMN metered BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN minutes_allowance NUMERIC(12,2),
ADD COLUMN tariff_rates JSONB NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN has_ldi BOOLEAN NOT NULL DEFAULT false;
COMMENT ON COLUMN products.service_type IS
'Recorte regulatório RGC (Resolução Anatel 765/2023) — só estes 4 valores existem no legado, ported verbatim. Gate para a seção de conformidade ANATEL no PDF de oferta (só aparece se algum item da oferta for STFC).';
COMMENT ON COLUMN products.metered IS
'Cobrança por minuto (chamadas STFC) — gate para a tabela de tarifação (LC/LDN/VC1/VC2/VC3[/LDI]) no PDF de oferta.';
COMMENT ON COLUMN products.tariff_rates IS
'Tarifas por tipo de chamada, formato {TIPO: {normal, reduced}} — chaves possíveis LC/LDN/VC1/VC2/VC3/LDI (ver packages/domain-shared para o catálogo de tipos). LDI só é exibido na tabela quando has_ldi=true, mas a chave pode existir no JSON de qualquer forma.';
COMMENT ON COLUMN products.has_ldi IS
'Habilita a coluna/linha de legenda LDI (longa distância internacional) na tabela de tarifação.';
ALTER TABLE quotes ADD COLUMN sales_rep_name TEXT;
COMMENT ON COLUMN quotes.sales_rep_name IS
'Nome do responsável comercial impresso no bloco de assinatura do PDF de oferta — texto livre, opcional (cai para "Responsável Comercial" genérico quando ausente).';
`);
};
exports.down = (pgm) => {
pgm.sql(`
ALTER TABLE quotes DROP COLUMN IF EXISTS sales_rep_name;
ALTER TABLE products
DROP COLUMN IF EXISTS service_type,
DROP COLUMN IF EXISTS metered,
DROP COLUMN IF EXISTS minutes_allowance,
DROP COLUMN IF EXISTS tariff_rates,
DROP COLUMN IF EXISTS has_ldi;
`);
};