feat: Goa GEL Blockchain e-Licensing Platform - Full Stack Implementation
Complete implementation of the Goa Government e-Licensing platform with: Backend: - NestJS API with JWT authentication - PostgreSQL database with Knex ORM - Redis caching and session management - MinIO document storage - Hyperledger Besu blockchain integration - Multi-department workflow system - Comprehensive API tests (266/282 passing) Frontend: - Angular 21 with standalone components - Angular Material + TailwindCSS UI - Visual workflow builder - Document upload with progress tracking - Blockchain explorer integration - Role-based dashboards (Admin, Department, Citizen) - E2E tests with Playwright (37 tests) Infrastructure: - Docker Compose orchestration - Blockscout blockchain explorer - Development and production configurations
This commit is contained in:
404
backend/src/modules/approvals/approvals.controller.ts
Normal file
404
backend/src/modules/approvals/approvals.controller.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user