Files
eden/apps/api/test/anatel-pdf.spec.ts
Matheus (Handix) f38f2407ff feat(products): catálogos de Tipo/Grupo/Subgrupo/Marca/Unidade com submenu e dropdowns
Menu Produtos ganha submenu (AppShell agora suporta itens aninhados) para
cadastrar Tipos/Grupos/Marcas/Unidades; Subgrupo é gerenciado a partir da
tela do Grupo (sempre pertence a um). O cadastro de item passa a usar
dropdowns desses catálogos em vez de texto livre — exatamente o que o
comentário original da migration de products já previa como trabalho
futuro.

Backend: 5 tabelas de catálogo simples (mesmo padrão dos catálogos
fiscais), products ganha as FKs correspondentes com backfill, e
endpoints CRUD (sem exclusão física, active=false é o "apagar").

Corrige também um buraco de integridade real encontrado na integração:
PATCH /v1/products/:id usava COALESCE para os campos de catálogo
opcionais, então não dava para distinguir "não mexer" de "limpar" —
trocar o Grupo de um produto sem tocar no Subgrupo deixava o vínculo
antigo órfão e inconsistente, sem nenhuma validação pegando isso. Agora
a atualização valida o estado efetivo resultante (bloqueia com 400 em
vez de corromper o vínculo) e productSubgroupId: null limpa de verdade.

Suíte: 95/95 verdes (92→95). pnpm turbo run typecheck build: 28/28.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 23:46:14 -03:00

190 lines
6.5 KiB
TypeScript

import { INestApplication } from "@nestjs/common";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, qaName, 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;
let productTypeId: string;
let unitId: 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 productType = await agent(app)
.post("/api/v1/products/types")
.set(...bearer(admin.accessToken))
.send({ name: qaName("Tipo ANATEL") })
.expect(201);
productTypeId = productType.body.id;
const unit = await agent(app)
.post("/api/v1/products/units")
.set(...bearer(admin.accessToken))
.send({ name: qaName("Unidade ANATEL") })
.expect(201);
unitId = unit.body.id;
const stfcProduct = await agent(app)
.post("/api/v1/products")
.set(...bearer(admin.accessToken))
.send({
name: "qa-test- Linha STFC",
productTypeId,
unitId,
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", productTypeId, unitId, 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");
});
});