erp-construccion/docs/05-backend-specs/modules/SPEC-compliance.md

15 KiB

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

@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

@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

@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

@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

@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

@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

@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

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

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

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

@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

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


Ultima actualizacion: 2025-12-05