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
104 lines
4.0 KiB
TypeScript
104 lines
4.0 KiB
TypeScript
import { Body, Controller, Get, Header, Param, Patch, Post, Query, Res, StreamableFile, UseGuards } from "@nestjs/common";
|
|
import type { Response } from "express";
|
|
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
|
import { QuotesService } from "./quotes.service.js";
|
|
import { CreateQuoteDto } from "./dto/create-quote.dto.js";
|
|
import { UpdateQuoteDto } from "./dto/update-quote.dto.js";
|
|
import { DecideApprovalDto } from "./dto/decide-approval.dto.js";
|
|
import { CloseDealDto } from "./dto/close-deal.dto.js";
|
|
import { JwtAuthGuard } from "../auth/jwt-auth.guard.js";
|
|
import { PermissionGuard } from "../auth/permission.guard.js";
|
|
import { SuperAdminGuard } from "../auth/super-admin.guard.js";
|
|
import { RequirePermission } from "../auth/decorators/require-permission.decorator.js";
|
|
import { CurrentUser } from "../auth/decorators/current-user.decorator.js";
|
|
import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js";
|
|
|
|
/**
|
|
* Ofertas comerciais (eden.md módulo 2). Scope resolution (own/reseller/all)
|
|
* happens in the service, driven by the actor's granted permission scope —
|
|
* see QuotesService.list/get. Every write recomputes pricing server-side
|
|
* (Master Prompt §6.4) — the client never supplies a trusted total.
|
|
*/
|
|
@ApiTags("quotes")
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard, PermissionGuard)
|
|
@Controller("v1/quotes")
|
|
export class QuotesController {
|
|
constructor(private readonly quotes: QuotesService) {}
|
|
|
|
@Get()
|
|
@RequirePermission("quotes", "view")
|
|
list(@CurrentUser() user: EdenAuthenticatedUser, @Query("dealStatus") dealStatus?: string) {
|
|
return this.quotes.list(user, dealStatus);
|
|
}
|
|
|
|
@Get(":id")
|
|
@RequirePermission("quotes", "view")
|
|
get(@Param("id") id: string, @CurrentUser() user: EdenAuthenticatedUser) {
|
|
return this.quotes.get(id, user);
|
|
}
|
|
|
|
@Post()
|
|
@RequirePermission("quotes", "edit")
|
|
create(@Body() dto: CreateQuoteDto, @CurrentUser() user: EdenAuthenticatedUser) {
|
|
return this.quotes.create(dto, user);
|
|
}
|
|
|
|
/** Anexo de conformidade ANATEL/RGC (Resolução 765/2023) — 400 se a oferta não tiver item STFC. Renderizado ao vivo, nunca cacheado (ver doc comment em QuotesService.generateAnatelPdf). */
|
|
@Get(":id/anatel-pdf")
|
|
@RequirePermission("quotes", "view")
|
|
@Header("Content-Type", "application/pdf")
|
|
async downloadAnatelPdf(
|
|
@Param("id") id: string,
|
|
@CurrentUser() user: EdenAuthenticatedUser,
|
|
@Res({ passthrough: true }) res: Response,
|
|
) {
|
|
const pdf = await this.quotes.generateAnatelPdf(id, user);
|
|
res.set({ "Content-Disposition": `inline; filename="anatel-${id}.pdf"` });
|
|
return new StreamableFile(pdf);
|
|
}
|
|
|
|
@Patch(":id")
|
|
@RequirePermission("quotes", "edit")
|
|
update(
|
|
@Param("id") id: string,
|
|
@Body() dto: UpdateQuoteDto,
|
|
@CurrentUser() user: EdenAuthenticatedUser,
|
|
) {
|
|
return this.quotes.update(id, dto, user);
|
|
}
|
|
|
|
/** Approve/reject a pending discount or reduced-fidelity exception — eden.md §3.3. Gated inside the service by canApproveDiscount(), not by a generic permission (mirrors the legacy's per-user flag, not a role-based permission). */
|
|
@Patch(":id/approval")
|
|
@RequirePermission("quotes", "approve")
|
|
decideApproval(
|
|
@Param("id") id: string,
|
|
@Body() dto: DecideApprovalDto,
|
|
@CurrentUser() user: EdenAuthenticatedUser,
|
|
) {
|
|
return this.quotes.decideApproval(id, dto, user);
|
|
}
|
|
|
|
/** Closes the deal, locking the quote — eden.md §5.1/§6. */
|
|
@Post(":id/close")
|
|
@RequirePermission("quotes", "edit")
|
|
closeDeal(
|
|
@Param("id") id: string,
|
|
@Body() dto: CloseDealDto,
|
|
@CurrentUser() user: EdenAuthenticatedUser,
|
|
) {
|
|
return this.quotes.closeDeal(id, dto, user);
|
|
}
|
|
|
|
/** super_admin-only shortcut, works even on a locked quote — eden.md §5.2. Route-level guard for defense in depth, on top of the service-layer check. */
|
|
@Patch(":id/deal-status")
|
|
@UseGuards(SuperAdminGuard)
|
|
dealStatus(
|
|
@Param("id") id: string,
|
|
@Body("dealStatus") dealStatus: "orcamento" | "fechado" | "perdido",
|
|
@CurrentUser() user: EdenAuthenticatedUser,
|
|
) {
|
|
return this.quotes.changeDealStatus(id, dealStatus, user);
|
|
}
|
|
}
|