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:
172
apps/api/test/anatel-pdf.spec.ts
Normal file
172
apps/api/test/anatel-pdf.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user