405 lines
12 KiB
TypeScript
405 lines
12 KiB
TypeScript
|
|
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<ApprovalResponseDto> {
|
||
|
|
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<ApprovalResponseDto> {
|
||
|
|
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<ApprovalResponseDto> {
|
||
|
|
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<ApprovalResponseDto> {
|
||
|
|
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<ApprovalResponseDto> {
|
||
|
|
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<ApprovalResponseDto> {
|
||
|
|
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<ApprovalResponseDto[]> {
|
||
|
|
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<ApprovalResponseDto> {
|
||
|
|
this.logger.debug(`[${correlationId}] Revalidating approval: ${approvalId}`);
|
||
|
|
return this.approvalsService.revalidateApproval(approvalId, dto);
|
||
|
|
}
|
||
|
|
}
|