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:
@@ -1,4 +1,7 @@
|
||||
import { IsBoolean, IsNumber, IsOptional, IsString, Min, MinLength } from "class-validator";
|
||||
import { IsBoolean, IsIn, IsNumber, IsOptional, IsString, Min, MinLength, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { SERVICE_TYPES, type ServiceType } from "@eden/domain-shared";
|
||||
import { TariffRatesDto } from "./tariff-rate.dto.js";
|
||||
|
||||
export class CreateProductDto {
|
||||
@IsString()
|
||||
@@ -33,4 +36,15 @@ export class CreateProductDto {
|
||||
@IsOptional() @IsNumber() @Min(0) price36?: number;
|
||||
@IsOptional() @IsNumber() @Min(0) price48?: number;
|
||||
@IsOptional() @IsNumber() @Min(0) implUnitPrice?: number;
|
||||
|
||||
// Recorte regulatório RGC (Resolução Anatel 765/2023, ver
|
||||
// packages/domain-shared/src/telecom/service-types.ts) — usado pelo
|
||||
// anexo de conformidade ANATEL do PDF de oferta (apps/api/src/quotes/
|
||||
// anatel-offer-template.ts). Snapshot no item da oferta no momento da
|
||||
// criação, não referência viva ao produto.
|
||||
@IsOptional() @IsIn(SERVICE_TYPES) serviceType?: ServiceType;
|
||||
@IsOptional() @IsBoolean() metered?: boolean;
|
||||
@IsOptional() @IsNumber() @Min(0) minutesAllowance?: number;
|
||||
@IsOptional() @ValidateNested() @Type(() => TariffRatesDto) tariffRates?: TariffRatesDto;
|
||||
@IsOptional() @IsBoolean() hasLdi?: boolean;
|
||||
}
|
||||
|
||||
17
apps/api/src/products/dto/tariff-rate.dto.ts
Normal file
17
apps/api/src/products/dto/tariff-rate.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IsNumber, IsOptional, Min, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
export class TariffRateDto {
|
||||
@IsNumber() @Min(0) normal!: number;
|
||||
@IsNumber() @Min(0) reduced!: number;
|
||||
}
|
||||
|
||||
/** One optional key per TARIFF_TYPES entry (LC/LDN/VC1/VC2/VC3/LDI) — validated by TariffRateDto below, not decorated individually since the set is derived from the shared catalog. */
|
||||
export class TariffRatesDto {
|
||||
@IsOptional() @ValidateNested() @Type(() => TariffRateDto) LC?: TariffRateDto;
|
||||
@IsOptional() @ValidateNested() @Type(() => TariffRateDto) LDN?: TariffRateDto;
|
||||
@IsOptional() @ValidateNested() @Type(() => TariffRateDto) VC1?: TariffRateDto;
|
||||
@IsOptional() @ValidateNested() @Type(() => TariffRateDto) VC2?: TariffRateDto;
|
||||
@IsOptional() @ValidateNested() @Type(() => TariffRateDto) VC3?: TariffRateDto;
|
||||
@IsOptional() @ValidateNested() @Type(() => TariffRateDto) LDI?: TariffRateDto;
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { auditRepo, productsRepo, type Product } from "@eden/database";
|
||||
import { emptyTariffRates, type TariffRates } from "@eden/domain-shared";
|
||||
import type { CreateProductDto } from "./dto/create-product.dto.js";
|
||||
import type { UpdateProductDto } from "./dto/update-product.dto.js";
|
||||
|
||||
/** DTO fields are individually optional (partial override); persisted tariff_rates always carries all 6 keys — mirrors the legacy's normalizeTariffRates(), never a partial object at rest. */
|
||||
function normalizeTariffRates(partial: Partial<TariffRates> | undefined): TariffRates | undefined {
|
||||
if (!partial) return undefined;
|
||||
return { ...emptyTariffRates(), ...partial };
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ProductsService {
|
||||
list(includeInactive: boolean): Promise<Product[]> {
|
||||
@@ -36,6 +43,11 @@ export class ProductsService {
|
||||
price36: dto.price36,
|
||||
price48: dto.price48,
|
||||
implUnitPrice: dto.implUnitPrice,
|
||||
serviceType: dto.serviceType,
|
||||
metered: dto.metered,
|
||||
minutesAllowance: dto.minutesAllowance,
|
||||
tariffRates: normalizeTariffRates(dto.tariffRates),
|
||||
hasLdi: dto.hasLdi,
|
||||
createdBy: actorId,
|
||||
});
|
||||
|
||||
@@ -73,6 +85,11 @@ export class ProductsService {
|
||||
price36: dto.price36,
|
||||
price48: dto.price48,
|
||||
implUnitPrice: dto.implUnitPrice,
|
||||
serviceType: dto.serviceType,
|
||||
metered: dto.metered,
|
||||
minutesAllowance: dto.minutesAllowance,
|
||||
tariffRates: normalizeTariffRates(dto.tariffRates),
|
||||
hasLdi: dto.hasLdi,
|
||||
});
|
||||
if (!product) throw new NotFoundException("Produto não encontrado");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user