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>
190 lines
6.5 KiB
TypeScript
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");
|
|
});
|
|
});
|