import { Controller, Post, Get, Put, Param, Body, UseGuards, HttpCode, HttpStatus, Query, Logger, ForbiddenException, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam, ApiQuery, } from '@nestjs/swagger'; import { ApprovalsService } from './approvals.service'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { ApproveRequestDto } from './dto/approve-request.dto'; import { RejectRequestDto } from './dto/reject-request.dto'; import { RequestChangesDto } from './dto/request-changes.dto'; import { RevalidateDto } from './dto/revalidate.dto'; import { ApprovalResponseDto } from './dto/approval-response.dto'; import { CorrelationId } from '../../common/decorators/correlation-id.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { JwtPayload } from '../../common/interfaces/request-context.interface'; import { Department } from '../../database/models/department.model'; import { Inject, BadRequestException } from '@nestjs/common'; import { UuidValidationPipe } from '../../common/pipes/uuid-validation.pipe'; @ApiTags('Approvals') @Controller('approvals') @ApiBearerAuth() @UseGuards(JwtAuthGuard) export class ApprovalsController { private readonly logger = new Logger(ApprovalsController.name); constructor( private readonly approvalsService: ApprovalsService, @Inject(Department) private readonly departmentModel: typeof Department, ) {} @Post(':requestId/approve') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Approve request (short form)', description: 'Approve a license request for a specific department', }) @ApiParam({ name: 'requestId', description: 'License request ID (UUID)', }) @ApiResponse({ status: 200, description: 'Request approved successfully', type: ApprovalResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid request or no pending approval found', }) @ApiResponse({ status: 404, description: 'Request not found', }) async approveShort( @Param('requestId', UuidValidationPipe) requestId: string, @Body() dto: ApproveRequestDto, @CurrentUser() user: JwtPayload, @CorrelationId() correlationId: string, ): Promise { this.logger.debug(`[${correlationId}] Approving request: ${requestId}`); if (!user.departmentCode) { throw new ForbiddenException('Department code not found in user context'); } // Look up department by code to get ID const department = await this.departmentModel.query().findOne({ code: user.departmentCode }); if (!department) { throw new BadRequestException(`Department not found: ${user.departmentCode}`); } return this.approvalsService.approve(requestId, department.id, dto, user.sub); } @Post('requests/:requestId/approve') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Approve request', description: 'Approve a license request for a specific department', }) @ApiParam({ name: 'requestId', description: 'License request ID (UUID)', }) @ApiResponse({ status: 200, description: 'Request approved successfully', type: ApprovalResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid request or no pending approval found', }) @ApiResponse({ status: 404, description: 'Request not found', }) async approve( @Param('requestId', UuidValidationPipe) requestId: string, @Body() dto: ApproveRequestDto, @CurrentUser() user: JwtPayload, @CorrelationId() correlationId: string, ): Promise { this.logger.debug(`[${correlationId}] Approving request: ${requestId}`); if (!user.departmentCode) { throw new ForbiddenException('Department code not found in user context'); } // Look up department by code to get ID const department = await this.departmentModel.query().findOne({ code: user.departmentCode }); if (!department) { throw new BadRequestException(`Department not found: ${user.departmentCode}`); } return this.approvalsService.approve(requestId, department.id, dto, user.sub); } @Post(':requestId/reject') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Reject request (short form)', description: 'Reject a license request for a specific department', }) @ApiParam({ name: 'requestId', description: 'License request ID (UUID)', }) @ApiResponse({ status: 200, description: 'Request rejected successfully', type: ApprovalResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid request or no pending approval found', }) @ApiResponse({ status: 404, description: 'Request not found', }) async rejectShort( @Param('requestId', UuidValidationPipe) requestId: string, @Body() dto: RejectRequestDto, @CurrentUser() user: JwtPayload, @CorrelationId() correlationId: string, ): Promise { this.logger.debug(`[${correlationId}] Rejecting request: ${requestId}`); if (!user.departmentCode) { throw new ForbiddenException('Department code not found in user context'); } // Look up department by code to get ID const department = await this.departmentModel.query().findOne({ code: user.departmentCode }); if (!department) { throw new BadRequestException(`Department not found: ${user.departmentCode}`); } return this.approvalsService.reject(requestId, department.id, dto, user.sub); } @Post('requests/:requestId/reject') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Reject request', description: 'Reject a license request for a specific department', }) @ApiParam({ name: 'requestId', description: 'License request ID (UUID)', }) @ApiResponse({ status: 200, description: 'Request rejected successfully', type: ApprovalResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid request or no pending approval found', }) @ApiResponse({ status: 404, description: 'Request not found', }) async reject( @Param('requestId', UuidValidationPipe) requestId: string, @Body() dto: RejectRequestDto, @CurrentUser() user: JwtPayload, @CorrelationId() correlationId: string, ): Promise { this.logger.debug(`[${correlationId}] Rejecting request: ${requestId}`); if (!user.departmentCode) { throw new ForbiddenException('Department code not found in user context'); } // Look up department by code to get ID const department = await this.departmentModel.query().findOne({ code: user.departmentCode }); if (!department) { throw new BadRequestException(`Department not found: ${user.departmentCode}`); } return this.approvalsService.reject(requestId, department.id, dto, user.sub); } @Post('requests/:requestId/request-changes') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Request changes on request', description: 'Request changes from applicant for a license request', }) @ApiParam({ name: 'requestId', description: 'License request ID (UUID)', }) @ApiResponse({ status: 200, description: 'Changes requested successfully', type: ApprovalResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid request or no pending approval found', }) @ApiResponse({ status: 404, description: 'Request not found', }) async requestChanges( @Param('requestId', UuidValidationPipe) requestId: string, @Body() dto: RequestChangesDto, @CurrentUser() user: JwtPayload, @CorrelationId() correlationId: string, ): Promise { this.logger.debug(`[${correlationId}] Requesting changes for request: ${requestId}`); if (!user.departmentCode) { throw new ForbiddenException('Department code not found in user context'); } // Look up department by code to get ID const department = await this.departmentModel.query().findOne({ code: user.departmentCode }); if (!department) { throw new BadRequestException(`Department not found: ${user.departmentCode}`); } return this.approvalsService.requestChanges(requestId, department.id, dto, user.sub); } @Get(':approvalId') @ApiOperation({ summary: 'Get approval by ID', description: 'Retrieve approval details by approval ID', }) @ApiParam({ name: 'approvalId', description: 'Approval ID (UUID)', }) @ApiResponse({ status: 200, description: 'Approval details', type: ApprovalResponseDto, }) @ApiResponse({ status: 404, description: 'Approval not found', }) async findById( @Param('approvalId', UuidValidationPipe) approvalId: string, @CorrelationId() correlationId: string, ): Promise { this.logger.debug(`[${correlationId}] Fetching approval: ${approvalId}`); return this.approvalsService.findById(approvalId); } @Get('requests/:requestId') @ApiOperation({ summary: 'Get approvals for request', description: 'Retrieve all approvals for a license request', }) @ApiParam({ name: 'requestId', description: 'License request ID (UUID)', }) @ApiQuery({ name: 'includeInvalidated', required: false, description: 'Include invalidated approvals (default: false)', example: 'false', }) @ApiResponse({ status: 200, description: 'List of approvals', type: [ApprovalResponseDto], }) @ApiResponse({ status: 404, description: 'Request not found', }) async findByRequestId( @Param('requestId', UuidValidationPipe) requestId: string, @Query('includeInvalidated') includeInvalidated?: string, @CorrelationId() correlationId?: string, ): Promise { this.logger.debug(`[${correlationId}] Fetching approvals for request: ${requestId}`); return this.approvalsService.findByRequestId( requestId, includeInvalidated === 'true', ); } @Get('department/:departmentCode') @ApiOperation({ summary: 'Get approvals by department', description: 'Retrieve approvals for a specific department with pagination', }) @ApiParam({ name: 'departmentCode', description: 'Department code', }) @ApiQuery({ name: 'page', required: false, description: 'Page number (default: 1)', type: Number, }) @ApiQuery({ name: 'limit', required: false, description: 'Items per page (default: 20)', type: Number, }) @ApiResponse({ status: 200, description: 'Paginated list of approvals', }) async findByDepartment( @Param('departmentCode') departmentCode: string, @Query() pagination: any, @CorrelationId() correlationId: string, ) { this.logger.debug(`[${correlationId}] Fetching approvals for department: ${departmentCode}`); // Look up department by code to get ID const department = await this.departmentModel.query().findOne({ code: departmentCode }); if (!department) { throw new BadRequestException(`Department not found: ${departmentCode}`); } return this.approvalsService.findByDepartment(department.id, pagination); } @Put(':approvalId/revalidate') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Revalidate approval', description: 'Revalidate an invalidated approval after document updates', }) @ApiParam({ name: 'approvalId', description: 'Approval ID (UUID)', }) @ApiResponse({ status: 200, description: 'Approval revalidated successfully', type: ApprovalResponseDto, }) @ApiResponse({ status: 400, description: 'Approval is not in invalidated state', }) @ApiResponse({ status: 404, description: 'Approval not found', }) async revalidate( @Param('approvalId', UuidValidationPipe) approvalId: string, @Body() dto: RevalidateDto, @CorrelationId() correlationId: string, ): Promise { this.logger.debug(`[${correlationId}] Revalidating approval: ${approvalId}`); return this.approvalsService.revalidateApproval(approvalId, dto); } }