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:
67
backend/src/modules/departments/dto/create-department.dto.ts
Normal file
67
backend/src/modules/departments/dto/create-department.dto.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
IsString,
|
||||
Matches,
|
||||
MinLength,
|
||||
IsOptional,
|
||||
IsUrl,
|
||||
IsEmail,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateDepartmentDto {
|
||||
@ApiProperty({
|
||||
description: 'Department code (uppercase letters, numbers, and underscores only)',
|
||||
example: 'DEPT_001',
|
||||
pattern: '^[A-Z0-9_]+$',
|
||||
})
|
||||
@IsString()
|
||||
@Matches(/^[A-Z0-9_]+$/, {
|
||||
message: 'Department code must contain only uppercase letters, numbers, and underscores',
|
||||
})
|
||||
@MinLength(3, { message: 'Department code must be at least 3 characters long' })
|
||||
code: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Department name',
|
||||
example: 'Finance Department',
|
||||
})
|
||||
@IsString()
|
||||
@MinLength(3, { message: 'Department name must be at least 3 characters long' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Department description',
|
||||
example: 'Handles financial operations and compliance',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Contact email for the department',
|
||||
example: 'contact@department.gov.in',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
contactEmail?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Contact phone number',
|
||||
example: '+91-11-XXXXXXXX',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Webhook URL for credential issuance events',
|
||||
example: 'https://api.department.gov.in/webhooks/credentials',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
webhookUrl?: string;
|
||||
}
|
||||
121
backend/src/modules/departments/dto/department-response.dto.ts
Normal file
121
backend/src/modules/departments/dto/department-response.dto.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Department } from '../../../database/models/department.model';
|
||||
|
||||
export class DepartmentResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Unique identifier for the department',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Department code',
|
||||
example: 'DEPT_001',
|
||||
})
|
||||
code: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Department name',
|
||||
example: 'Finance Department',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Blockchain wallet address',
|
||||
example: '0x1234567890123456789012345678901234567890',
|
||||
required: false,
|
||||
})
|
||||
walletAddress?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Department description',
|
||||
example: 'Handles financial operations',
|
||||
required: false,
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Contact email',
|
||||
example: 'contact@department.gov.in',
|
||||
required: false,
|
||||
})
|
||||
contactEmail?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Contact phone number',
|
||||
example: '+91-11-XXXXXXXX',
|
||||
required: false,
|
||||
})
|
||||
contactPhone?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Webhook URL',
|
||||
example: 'https://api.department.gov.in/webhooks/credentials',
|
||||
required: false,
|
||||
})
|
||||
webhookUrl?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the department is active',
|
||||
example: true,
|
||||
})
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Timestamp when the department was created',
|
||||
example: '2024-01-15T10:30:00Z',
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Timestamp when the department was last updated',
|
||||
example: '2024-01-15T10:30:00Z',
|
||||
})
|
||||
updatedAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Total number of applicants in this department',
|
||||
example: 150,
|
||||
})
|
||||
totalApplicants: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Total number of issued credentials',
|
||||
example: 145,
|
||||
})
|
||||
issuedCredentials: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Timestamp of last webhook call',
|
||||
example: '2024-01-15T10:30:00Z',
|
||||
required: false,
|
||||
})
|
||||
lastWebhookAt?: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Hashed API key for the department',
|
||||
example: 'hashed_api_key_value',
|
||||
required: false,
|
||||
})
|
||||
apiKeyHash?: string;
|
||||
|
||||
static fromEntity(department: Department): DepartmentResponseDto {
|
||||
const dto = new DepartmentResponseDto();
|
||||
dto.id = department.id;
|
||||
dto.code = department.code;
|
||||
dto.name = department.name;
|
||||
dto.walletAddress = (department as any).walletAddress;
|
||||
dto.description = (department as any).description;
|
||||
dto.contactEmail = (department as any).contactEmail;
|
||||
dto.contactPhone = (department as any).contactPhone;
|
||||
dto.webhookUrl = department.webhookUrl;
|
||||
dto.isActive = department.isActive;
|
||||
dto.createdAt = department.createdAt;
|
||||
dto.updatedAt = department.updatedAt;
|
||||
dto.totalApplicants = (department as any).totalApplicants;
|
||||
dto.issuedCredentials = (department as any).issuedCredentials;
|
||||
dto.lastWebhookAt = (department as any).lastWebhookAt;
|
||||
dto.apiKeyHash = department.apiKeyHash;
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
65
backend/src/modules/departments/dto/department-stats.dto.ts
Normal file
65
backend/src/modules/departments/dto/department-stats.dto.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DepartmentStatsDto {
|
||||
@ApiProperty({
|
||||
description: 'Department code',
|
||||
example: 'DEPT_001',
|
||||
})
|
||||
departmentCode: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Department name',
|
||||
example: 'Finance Department',
|
||||
})
|
||||
departmentName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Total number of applicants',
|
||||
example: 150,
|
||||
})
|
||||
totalApplicants: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Total number of credentials issued',
|
||||
example: 145,
|
||||
})
|
||||
totalCredentialsIssued: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the department is active',
|
||||
example: true,
|
||||
})
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Department creation timestamp',
|
||||
example: '2024-01-15T10:30:00Z',
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Department last update timestamp',
|
||||
example: '2024-01-15T10:30:00Z',
|
||||
})
|
||||
updatedAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Timestamp of last webhook event',
|
||||
example: '2024-01-20T14:00:00Z',
|
||||
required: false,
|
||||
})
|
||||
lastWebhookAt?: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Percentage of credentials issued vs applicants',
|
||||
example: 96.67,
|
||||
})
|
||||
issueRate?: number;
|
||||
|
||||
constructor() {
|
||||
if (this.totalApplicants > 0) {
|
||||
this.issueRate =
|
||||
(this.totalCredentialsIssued / this.totalApplicants) * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
backend/src/modules/departments/dto/update-department.dto.ts
Normal file
42
backend/src/modules/departments/dto/update-department.dto.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateDepartmentDto } from './create-department.dto';
|
||||
import {
|
||||
IsString,
|
||||
Matches,
|
||||
MinLength,
|
||||
IsOptional,
|
||||
IsUrl,
|
||||
IsEmail,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateDepartmentDto extends PartialType(CreateDepartmentDto) {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^[A-Z_]+$/, {
|
||||
message: 'Department code must contain only uppercase letters and underscores',
|
||||
})
|
||||
@MinLength(3)
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
contactEmail?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
webhookUrl?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user