567 lines
15 KiB
Markdown
567 lines
15 KiB
Markdown
# Backend Specification: Compliance Module (INFONAVIT)
|
|
|
|
**Version:** 1.0.0
|
|
**Fecha:** 2025-12-05
|
|
**Modulos:** MAI-011 (INFONAVIT Cumplimiento)
|
|
|
|
---
|
|
|
|
## Resumen
|
|
|
|
| Metrica | Valor |
|
|
|---------|-------|
|
|
| Controllers | 4 |
|
|
| Services | 5 |
|
|
| Entities | 10 |
|
|
| Endpoints | 30+ |
|
|
| Tests Requeridos | 40+ |
|
|
|
|
---
|
|
|
|
## Estructura del Modulo
|
|
|
|
```
|
|
modules/compliance/
|
|
+-- compliance.module.ts
|
|
+-- controllers/
|
|
| +-- program.controller.ts
|
|
| +-- compliance.controller.ts
|
|
| +-- audit.controller.ts
|
|
| +-- report.controller.ts
|
|
+-- services/
|
|
| +-- program.service.ts
|
|
| +-- compliance.service.ts
|
|
| +-- evidence.service.ts
|
|
| +-- audit.service.ts
|
|
| +-- report.service.ts
|
|
+-- entities/
|
|
| +-- infonavit-program.entity.ts
|
|
| +-- program-requirement.entity.ts
|
|
| +-- project-program.entity.ts
|
|
| +-- compliance-item.entity.ts
|
|
| +-- compliance-evidence.entity.ts
|
|
| +-- audit.entity.ts
|
|
| +-- audit-finding.entity.ts
|
|
| +-- corrective-action.entity.ts
|
|
| +-- compliance-alert.entity.ts
|
|
| +-- compliance-history.entity.ts
|
|
+-- dto/
|
|
+-- events/
|
|
+-- jobs/
|
|
+-- due-date-alerts.job.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Controllers
|
|
|
|
### 1. ProgramController
|
|
|
|
```typescript
|
|
@Controller('api/v1/compliance/programs')
|
|
@ApiTags('compliance-programs')
|
|
export class ProgramController {
|
|
|
|
// GET /api/v1/compliance/programs
|
|
@Get()
|
|
async findAll(@Query() query: ProgramQueryDto): Promise<PaginatedResponse<ProgramDto>>;
|
|
|
|
// GET /api/v1/compliance/programs/:id
|
|
@Get(':id')
|
|
async findOne(@Param('id') id: UUID): Promise<ProgramDetailDto>;
|
|
|
|
// POST /api/v1/compliance/programs
|
|
@Post()
|
|
async create(@Body() dto: CreateProgramDto): Promise<ProgramDto>;
|
|
|
|
// GET /api/v1/compliance/programs/:id/requirements
|
|
@Get(':id/requirements')
|
|
async getRequirements(@Param('id') id: UUID): Promise<RequirementTreeDto[]>;
|
|
|
|
// POST /api/v1/compliance/programs/:id/requirements
|
|
@Post(':id/requirements')
|
|
async addRequirement(@Param('id') id: UUID, @Body() dto: CreateRequirementDto): Promise<RequirementDto>;
|
|
}
|
|
```
|
|
|
|
### 2. ComplianceController
|
|
|
|
```typescript
|
|
@Controller('api/v1/compliance')
|
|
@ApiTags('compliance')
|
|
export class ComplianceController {
|
|
|
|
// POST /api/v1/compliance/register-project
|
|
@Post('register-project')
|
|
async registerProject(@Body() dto: RegisterProjectDto): Promise<ProjectProgramDto>;
|
|
|
|
// GET /api/v1/compliance/project/:projectId
|
|
@Get('project/:projectId')
|
|
async getProjectCompliance(@Param('projectId') projectId: UUID): Promise<ProjectComplianceDto>;
|
|
|
|
// GET /api/v1/compliance/project/:projectId/items
|
|
@Get('project/:projectId/items')
|
|
async getComplianceItems(
|
|
@Param('projectId') projectId: UUID,
|
|
@Query() query: ComplianceItemQueryDto
|
|
): Promise<ComplianceItemDto[]>;
|
|
|
|
// PUT /api/v1/compliance/items/:id/status
|
|
@Put('items/:id/status')
|
|
async updateItemStatus(
|
|
@Param('id') id: UUID,
|
|
@Body() dto: UpdateItemStatusDto
|
|
): Promise<ComplianceItemDto>;
|
|
|
|
// POST /api/v1/compliance/items/:id/evidence
|
|
@Post('items/:id/evidence')
|
|
@UseInterceptors(FileInterceptor('file'))
|
|
async uploadEvidence(
|
|
@Param('id') id: UUID,
|
|
@UploadedFile() file: Express.Multer.File,
|
|
@Body() dto: UploadEvidenceDto
|
|
): Promise<EvidenceDto>;
|
|
|
|
// GET /api/v1/compliance/items/:id/evidence
|
|
@Get('items/:id/evidence')
|
|
async getEvidence(@Param('id') id: UUID): Promise<EvidenceDto[]>;
|
|
|
|
// DELETE /api/v1/compliance/evidence/:id
|
|
@Delete('evidence/:id')
|
|
async removeEvidence(@Param('id') id: UUID): Promise<void>;
|
|
|
|
// GET /api/v1/compliance/project/:projectId/dashboard
|
|
@Get('project/:projectId/dashboard')
|
|
async getDashboard(@Param('projectId') projectId: UUID): Promise<ComplianceDashboardDto>;
|
|
}
|
|
```
|
|
|
|
### 3. AuditController
|
|
|
|
```typescript
|
|
@Controller('api/v1/compliance/audits')
|
|
@ApiTags('compliance-audits')
|
|
export class AuditController {
|
|
|
|
// GET /api/v1/compliance/audits
|
|
@Get()
|
|
async findAll(@Query() query: AuditQueryDto): Promise<PaginatedResponse<AuditDto>>;
|
|
|
|
// GET /api/v1/compliance/audits/:id
|
|
@Get(':id')
|
|
async findOne(@Param('id') id: UUID): Promise<AuditDetailDto>;
|
|
|
|
// POST /api/v1/compliance/audits
|
|
@Post()
|
|
async create(@Body() dto: CreateAuditDto): Promise<AuditDto>;
|
|
|
|
// PUT /api/v1/compliance/audits/:id
|
|
@Put(':id')
|
|
async update(@Param('id') id: UUID, @Body() dto: UpdateAuditDto): Promise<AuditDto>;
|
|
|
|
// POST /api/v1/compliance/audits/:id/start
|
|
@Post(':id/start')
|
|
async startAudit(@Param('id') id: UUID): Promise<AuditDto>;
|
|
|
|
// POST /api/v1/compliance/audits/:id/complete
|
|
@Post(':id/complete')
|
|
async completeAudit(@Param('id') id: UUID, @Body() dto: CompleteAuditDto): Promise<AuditDto>;
|
|
|
|
// GET /api/v1/compliance/audits/:id/findings
|
|
@Get(':id/findings')
|
|
async getFindings(@Param('id') id: UUID): Promise<FindingDto[]>;
|
|
|
|
// POST /api/v1/compliance/audits/:id/findings
|
|
@Post(':id/findings')
|
|
async addFinding(@Param('id') id: UUID, @Body() dto: CreateFindingDto): Promise<FindingDto>;
|
|
|
|
// PUT /api/v1/compliance/findings/:id
|
|
@Put('findings/:id')
|
|
async updateFinding(@Param('id') id: UUID, @Body() dto: UpdateFindingDto): Promise<FindingDto>;
|
|
|
|
// POST /api/v1/compliance/findings/:id/corrective-actions
|
|
@Post('findings/:id/corrective-actions')
|
|
async addCorrectiveAction(
|
|
@Param('id') id: UUID,
|
|
@Body() dto: CreateCorrectiveActionDto
|
|
): Promise<CorrectiveActionDto>;
|
|
|
|
// PUT /api/v1/compliance/corrective-actions/:id/complete
|
|
@Put('corrective-actions/:id/complete')
|
|
async completeAction(@Param('id') id: UUID): Promise<CorrectiveActionDto>;
|
|
|
|
// PUT /api/v1/compliance/corrective-actions/:id/verify
|
|
@Put('corrective-actions/:id/verify')
|
|
async verifyAction(@Param('id') id: UUID, @Body() dto: VerifyActionDto): Promise<CorrectiveActionDto>;
|
|
}
|
|
```
|
|
|
|
### 4. ReportController
|
|
|
|
```typescript
|
|
@Controller('api/v1/compliance/reports')
|
|
@ApiTags('compliance-reports')
|
|
export class ReportController {
|
|
|
|
// GET /api/v1/compliance/reports/status/:projectId
|
|
@Get('status/:projectId')
|
|
async getStatusReport(@Param('projectId') projectId: UUID): Promise<StatusReportDto>;
|
|
|
|
// GET /api/v1/compliance/reports/audit/:projectId
|
|
@Get('audit/:projectId')
|
|
async getAuditReport(@Param('projectId') projectId: UUID): Promise<AuditReportDto>;
|
|
|
|
// GET /api/v1/compliance/reports/executive/:projectId
|
|
@Get('executive/:projectId')
|
|
async getExecutiveReport(@Param('projectId') projectId: UUID): Promise<ExecutiveReportDto>;
|
|
|
|
// GET /api/v1/compliance/reports/evidence-package/:projectId
|
|
@Get('evidence-package/:projectId')
|
|
async downloadEvidencePackage(@Param('projectId') projectId: UUID): Promise<StreamableFile>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Services
|
|
|
|
### ComplianceService
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class ComplianceService {
|
|
constructor(
|
|
@InjectRepository(ProjectProgram)
|
|
private readonly projectProgramRepo: Repository<ProjectProgram>,
|
|
@InjectRepository(ComplianceItem)
|
|
private readonly itemRepo: Repository<ComplianceItem>,
|
|
private readonly eventEmitter: EventEmitter2
|
|
) {}
|
|
|
|
async registerProject(tenantId: UUID, userId: UUID, dto: RegisterProjectDto): Promise<ProjectProgramDto>;
|
|
async getProjectCompliance(tenantId: UUID, projectId: UUID): Promise<ProjectComplianceDto>;
|
|
async getComplianceItems(projectProgramId: UUID, query: ComplianceItemQueryDto): Promise<ComplianceItemDto[]>;
|
|
async updateItemStatus(tenantId: UUID, itemId: UUID, userId: UUID, dto: UpdateItemStatusDto): Promise<ComplianceItemDto>;
|
|
async calculateCompliancePercentage(projectProgramId: UUID): Promise<number>;
|
|
async getDashboard(tenantId: UUID, projectId: UUID): Promise<ComplianceDashboardDto>;
|
|
}
|
|
```
|
|
|
|
### EvidenceService
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class EvidenceService {
|
|
constructor(
|
|
@InjectRepository(ComplianceEvidence)
|
|
private readonly evidenceRepo: Repository<ComplianceEvidence>,
|
|
private readonly storageService: StorageService
|
|
) {}
|
|
|
|
async upload(itemId: UUID, userId: UUID, file: Express.Multer.File, dto: UploadEvidenceDto): Promise<EvidenceDto>;
|
|
async getByItem(itemId: UUID): Promise<EvidenceDto[]>;
|
|
async remove(tenantId: UUID, evidenceId: UUID): Promise<void>;
|
|
async replace(evidenceId: UUID, file: Express.Multer.File): Promise<EvidenceDto>;
|
|
}
|
|
```
|
|
|
|
### AuditService
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class AuditService {
|
|
constructor(
|
|
@InjectRepository(Audit)
|
|
private readonly auditRepo: Repository<Audit>,
|
|
@InjectRepository(AuditFinding)
|
|
private readonly findingRepo: Repository<AuditFinding>,
|
|
@InjectRepository(CorrectiveAction)
|
|
private readonly actionRepo: Repository<CorrectiveAction>,
|
|
private readonly eventEmitter: EventEmitter2
|
|
) {}
|
|
|
|
async findAll(tenantId: UUID, query: AuditQueryDto): Promise<PaginatedResponse<AuditDto>>;
|
|
async findOne(tenantId: UUID, id: UUID): Promise<AuditDetailDto>;
|
|
async create(tenantId: UUID, userId: UUID, dto: CreateAuditDto): Promise<AuditDto>;
|
|
async startAudit(tenantId: UUID, id: UUID): Promise<AuditDto>;
|
|
async completeAudit(tenantId: UUID, id: UUID, dto: CompleteAuditDto): Promise<AuditDto>;
|
|
async addFinding(auditId: UUID, userId: UUID, dto: CreateFindingDto): Promise<FindingDto>;
|
|
async updateFinding(findingId: UUID, dto: UpdateFindingDto): Promise<FindingDto>;
|
|
async addCorrectiveAction(findingId: UUID, userId: UUID, dto: CreateCorrectiveActionDto): Promise<CorrectiveActionDto>;
|
|
async completeAction(actionId: UUID, userId: UUID): Promise<CorrectiveActionDto>;
|
|
async verifyAction(actionId: UUID, userId: UUID, dto: VerifyActionDto): Promise<CorrectiveActionDto>;
|
|
async calculateFindingStats(auditId: UUID): Promise<{ critical: number; major: number; minor: number }>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DTOs
|
|
|
|
### Program DTOs
|
|
|
|
```typescript
|
|
export class CreateProgramDto {
|
|
@IsString() @MaxLength(20)
|
|
code: string;
|
|
|
|
@IsString() @MaxLength(200)
|
|
name: string;
|
|
|
|
@IsEnum(ProgramType)
|
|
programType: ProgramType;
|
|
|
|
@IsOptional() @IsDateString()
|
|
validFrom?: string;
|
|
|
|
@IsOptional() @IsDateString()
|
|
validTo?: string;
|
|
}
|
|
|
|
export class CreateRequirementDto {
|
|
@IsString() @MaxLength(30)
|
|
code: string;
|
|
|
|
@IsString() @MaxLength(300)
|
|
name: string;
|
|
|
|
@IsEnum(RequirementCategory)
|
|
category: RequirementCategory;
|
|
|
|
@IsOptional() @IsString()
|
|
description?: string;
|
|
|
|
@IsOptional() @IsArray() @IsString({ each: true })
|
|
evidenceRequired?: string[];
|
|
|
|
@IsOptional() @IsUUID()
|
|
parentId?: string;
|
|
|
|
@IsOptional() @IsBoolean()
|
|
isMandatory?: boolean;
|
|
}
|
|
```
|
|
|
|
### Compliance DTOs
|
|
|
|
```typescript
|
|
export class RegisterProjectDto {
|
|
@IsUUID()
|
|
projectId: string;
|
|
|
|
@IsUUID()
|
|
programId: string;
|
|
|
|
@IsOptional() @IsString()
|
|
registrationNumber?: string;
|
|
|
|
@IsOptional() @IsDateString()
|
|
registrationDate?: string;
|
|
}
|
|
|
|
export class UpdateItemStatusDto {
|
|
@IsEnum(RequirementStatus)
|
|
status: RequirementStatus;
|
|
|
|
@IsOptional() @IsString()
|
|
notes?: string;
|
|
|
|
@IsOptional() @IsBoolean()
|
|
isNotApplicable?: boolean;
|
|
|
|
@IsOptional() @IsString()
|
|
naReason?: string;
|
|
}
|
|
|
|
export class ComplianceDashboardDto {
|
|
projectId: string;
|
|
programs: {
|
|
programId: string;
|
|
programName: string;
|
|
compliancePercentage: number;
|
|
status: 'green' | 'yellow' | 'red';
|
|
}[];
|
|
summary: {
|
|
totalRequirements: number;
|
|
compliantCount: number;
|
|
pendingCount: number;
|
|
nonCompliantCount: number;
|
|
overallPercentage: number;
|
|
};
|
|
byCategory: {
|
|
category: RequirementCategory;
|
|
total: number;
|
|
compliant: number;
|
|
percentage: number;
|
|
}[];
|
|
upcomingDueDates: {
|
|
itemId: string;
|
|
requirementName: string;
|
|
dueDate: string;
|
|
daysRemaining: number;
|
|
}[];
|
|
recentAudits: AuditSummaryDto[];
|
|
}
|
|
```
|
|
|
|
### Audit DTOs
|
|
|
|
```typescript
|
|
export class CreateAuditDto {
|
|
@IsUUID()
|
|
projectId: string;
|
|
|
|
@IsEnum(AuditType)
|
|
auditType: AuditType;
|
|
|
|
@IsDateString()
|
|
scheduledDate: string;
|
|
|
|
@IsOptional() @IsString()
|
|
scheduledTime?: string;
|
|
|
|
@IsOptional() @IsString()
|
|
auditorName?: string;
|
|
|
|
@IsOptional() @IsString()
|
|
auditorCompany?: string;
|
|
}
|
|
|
|
export class CreateFindingDto {
|
|
@IsString() @MaxLength(30)
|
|
findingNumber: string;
|
|
|
|
@IsEnum(FindingSeverity)
|
|
severity: FindingSeverity;
|
|
|
|
@IsString() @MaxLength(300)
|
|
title: string;
|
|
|
|
@IsString()
|
|
description: string;
|
|
|
|
@IsOptional() @IsUUID()
|
|
complianceItemId?: string;
|
|
|
|
@IsOptional() @IsString()
|
|
responsibleName?: string;
|
|
|
|
@IsDateString()
|
|
dueDate: string;
|
|
}
|
|
|
|
export class CreateCorrectiveActionDto {
|
|
@IsString()
|
|
description: string;
|
|
|
|
@IsEnum(ActionType)
|
|
actionType: ActionType;
|
|
|
|
@IsOptional() @IsUUID()
|
|
assignedTo?: string;
|
|
|
|
@IsDateString()
|
|
dueDate: string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Scheduled Jobs
|
|
|
|
### DueDateAlertsJob
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class DueDateAlertsJob {
|
|
constructor(
|
|
private readonly complianceService: ComplianceService,
|
|
private readonly notificationService: NotificationService
|
|
) {}
|
|
|
|
@Cron('0 8 * * *') // Daily at 8 AM
|
|
async generateDueDateAlerts() {
|
|
// Find items due in next 7 days
|
|
const upcomingItems = await this.complianceService.getItemsDueSoon(7);
|
|
|
|
for (const item of upcomingItems) {
|
|
await this.notificationService.send({
|
|
type: 'requirement_due',
|
|
recipients: item.responsibleUsers,
|
|
data: {
|
|
requirementName: item.requirementName,
|
|
dueDate: item.dueDate,
|
|
daysRemaining: item.daysRemaining
|
|
}
|
|
});
|
|
}
|
|
|
|
// Find overdue findings
|
|
const overdueFindings = await this.complianceService.getOverdueFindings();
|
|
|
|
for (const finding of overdueFindings) {
|
|
await this.notificationService.send({
|
|
type: 'finding_overdue',
|
|
severity: finding.severity,
|
|
recipients: finding.responsibleUsers,
|
|
data: {
|
|
findingTitle: finding.title,
|
|
dueDate: finding.dueDate,
|
|
daysOverdue: finding.daysOverdue
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Events
|
|
|
|
```typescript
|
|
export class ProjectRegisteredInProgramEvent {
|
|
constructor(
|
|
public readonly projectProgramId: string,
|
|
public readonly projectId: string,
|
|
public readonly programId: string,
|
|
public readonly registrationNumber: string | null,
|
|
public readonly timestamp: Date = new Date()
|
|
) {}
|
|
}
|
|
|
|
export class ComplianceStatusChangedEvent {
|
|
constructor(
|
|
public readonly complianceItemId: string,
|
|
public readonly projectProgramId: string,
|
|
public readonly previousStatus: RequirementStatus,
|
|
public readonly newStatus: RequirementStatus,
|
|
public readonly changedBy: string,
|
|
public readonly timestamp: Date = new Date()
|
|
) {}
|
|
}
|
|
|
|
export class AuditCompletedEvent {
|
|
constructor(
|
|
public readonly auditId: string,
|
|
public readonly projectId: string,
|
|
public readonly overallCompliance: number,
|
|
public readonly findingsCount: number,
|
|
public readonly criticalCount: number,
|
|
public readonly timestamp: Date = new Date()
|
|
) {}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Referencias
|
|
|
|
- [DDL-SPEC-compliance.md](../../04-modelado/database-design/schemas/DDL-SPEC-compliance.md)
|
|
- [COMPLIANCE-CONTEXT.md](../../04-modelado/domain-models/COMPLIANCE-CONTEXT.md)
|
|
- [EPIC-MAI-011](../../08-epicas/EPIC-MAI-011-infonavit.md)
|
|
|
|
---
|
|
|
|
*Ultima actualizacion: 2025-12-05*
|