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,172 @@
import { INestApplication } from "@nestjs/common";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, type AuthedTokens } from "./setup.js";
/**
* `renderPdf` is mocked (real call path through quotes.service.ts still
* runs) — what's under test is the ANATEL/RGC annex's computed content
* (anatel-offer-template.ts), not Chromium itself (covered by the
* documents-pdf.spec.ts suite + the manual smoke test in progress.md).
* Business-critical regulatory content: this asserts the actual HTML
* handed to the renderer contains the right computed fields, not just
* that the endpoint returns 200.
*/
const renderPdfMock = vi.fn(async (_input: { html: string }) => Buffer.from("%PDF-1.4 fake anatel pdf"));
vi.mock("@eden/integrations", () => ({
renderPdf: (input: { html: string }) => renderPdfMock(input),
}));
describe("quotes — anexo de conformidade ANATEL/RGC", () => {
let app: INestApplication;
let admin: AuthedTokens;
let resellerId: string;
let stfcProductId: string;
let pabxProductId: string;
beforeAll(async () => {
app = await createTestApp();
admin = await loginSuperAdmin(app);
const reseller = await agent(app)
.post("/api/v1/resellers")
.set(...bearer(admin.accessToken))
.send({ name: "qa-test- Revenda ANATEL" })
.expect(201);
resellerId = reseller.body.id;
const stfcProduct = await agent(app)
.post("/api/v1/products")
.set(...bearer(admin.accessToken))
.send({
name: "qa-test- Linha STFC",
productType: "service",
price0: 200,
price12: 150,
serviceType: "STFC",
metered: true,
minutesAllowance: 100,
hasLdi: true,
tariffRates: {
LC: { normal: 0.1, reduced: 0.1 },
LDN: { normal: 0.2, reduced: 0.2 },
VC1: { normal: 0.15, reduced: 0.15 },
VC2: { normal: 0.25, reduced: 0.25 },
VC3: { normal: 0.3, reduced: 0.3 },
LDI: { normal: 1.5, reduced: 1.5 },
},
})
.expect(201);
stfcProductId = stfcProduct.body.id;
expect(stfcProduct.body.service_type).toBe("STFC");
expect(stfcProduct.body.tariff_rates.LC).toEqual({ normal: 0.1, reduced: 0.1 });
const pabxProduct = await agent(app)
.post("/api/v1/products")
.set(...bearer(admin.accessToken))
.send({ name: "qa-test- Ramal PABX", productType: "service", price0: 50, serviceType: "Serviço PABX" })
.expect(201);
pabxProductId = pabxProduct.body.id;
});
afterAll(async () => {
await agent(app)
.patch(`/api/v1/products/${stfcProductId}`)
.set(...bearer(admin.accessToken))
.send({ active: false })
.expect(200);
await agent(app)
.patch(`/api/v1/products/${pabxProductId}`)
.set(...bearer(admin.accessToken))
.send({ active: false })
.expect(200);
await agent(app)
.patch(`/api/v1/resellers/${resellerId}`)
.set(...bearer(admin.accessToken))
.send({ active: false })
.expect(200);
await closeTestApp(app);
});
it("snapshots service_type/metered/tariffRates onto the quote item at creation", async () => {
const quote = await agent(app)
.post("/api/v1/quotes")
.set(...bearer(admin.accessToken))
.send({
resellerId,
clientName: "qa-test Cliente ANATEL",
clientPhone: "11999998888",
contractPeriod: 12,
items: [{ productId: stfcProductId, quantity: 2 }],
salesRepName: "qa-test Vendedor",
})
.expect(201);
const item = quote.body.items[0];
expect(item.serviceType).toBe("STFC");
expect(item.metered).toBe(true);
expect(item.hasLdi).toBe(true);
expect(item.tariffRates.LC).toEqual({ normal: 0.1, reduced: 0.1 });
expect(item.minutesAllowance).toBe(100);
});
it("renders the RGC annex with the right computed fields for an STFC quote", async () => {
renderPdfMock.mockClear();
const quote = await agent(app)
.post("/api/v1/quotes")
.set(...bearer(admin.accessToken))
.send({
resellerId,
clientName: "qa-test Cliente ANATEL 2",
clientPhone: "11999998888",
contractPeriod: 12,
items: [{ productId: stfcProductId, quantity: 1 }],
salesRepName: "qa-test Vendedor",
})
.expect(201);
const download = await agent(app)
.get(`/api/v1/quotes/${quote.body.id}/anatel-pdf`)
.set(...bearer(admin.accessToken))
.expect(200);
expect(download.headers["content-type"]).toBe("application/pdf");
expect(renderPdfMock).toHaveBeenCalledTimes(1);
const html = renderPdfMock.mock.calls[0]![0].html;
// Legal-basis section only renders because the quote has an STFC item.
expect(html).toContain("Conformidade RGC — Resolução Anatel nº 765/2023");
expect(html).toContain("Prazo de Comercialização");
expect(html).toContain(quote.body.quote_number);
expect(html).toContain("qa-test Cliente ANATEL 2");
expect(html).toContain("qa-test Vendedor");
// Tariff section, since the item is metered.
expect(html).toContain("TABELA DE TARIFAÇÃO");
expect(html).toContain("LDI");
expect(html).toContain("A tecnologia utilizada para entrega das chamadas do serviço STFC ofertado é VoIP.");
// Fixed regulatory fields.
expect(html).toContain("Pós-paga");
expect(html).toContain("Nacional");
expect(html).toContain("Intervalo mínimo de 12 meses");
});
it("rejects (400) a quote with no STFC item", async () => {
const quote = await agent(app)
.post("/api/v1/quotes")
.set(...bearer(admin.accessToken))
.send({
resellerId,
clientName: "qa-test Cliente Sem STFC",
clientPhone: "11999998888",
contractPeriod: 0,
items: [{ productId: pabxProductId, quantity: 1 }],
})
.expect(201);
const res = await agent(app)
.get(`/api/v1/quotes/${quote.body.id}/anatel-pdf`)
.set(...bearer(admin.accessToken))
.expect(400);
expect(res.body.message).toContain("STFC");
});
});