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); } }