erp-construccion/docs/02-definicion-modulos/MAI-007-seguridad-industrial/especificaciones/ET-SEG-001-backend.md

3202 lines
79 KiB
Markdown

# ET-SEG-001: Especificaciones Técnicas Backend - Seguridad Industrial
## 1. Información General
**Módulo:** MAI-007 - Seguridad Industrial
**Componente:** Backend API
**Stack Tecnológico:** NestJS 10+, TypeORM
**Responsable:** Equipo Backend
**Fecha:** 2025-12-06
**Versión:** 1.0.0
## 2. Arquitectura de Módulos
### 2.1. Estructura de Módulos NestJS
```typescript
src/
├── modules/
├── epp/ # Equipos de Protección Personal
├── epp.module.ts
├── entities/
├── dto/
├── services/
└── controllers/
├── safety-inspections/ # Inspecciones de Seguridad
├── safety-inspections.module.ts
├── entities/
├── dto/
├── services/
└── controllers/
├── incidents/ # Gestión de Incidentes
├── incidents.module.ts
├── entities/
├── dto/
├── services/
├── controllers/
└── workflows/
└── safety-trainings/ # Capacitaciones de Seguridad
├── safety-trainings.module.ts
├── entities/
├── dto/
├── services/
└── controllers/
├── shared/
├── notifications/
└── reporting/
└── config/
```
### 2.2. EPPModule - Equipos de Protección Personal
```typescript
// epp.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
EPPItem,
EPPAssignment,
EPPInventory,
EPPDelivery,
EPPCategory
} from './entities';
import { EPPService } from './services/epp.service';
import { EPPAssignmentService } from './services/epp-assignment.service';
import { EPPInventoryService } from './services/epp-inventory.service';
import { EPPController } from './controllers/epp.controller';
@Module({
imports: [
TypeOrmModule.forFeature([
EPPItem,
EPPAssignment,
EPPInventory,
EPPDelivery,
EPPCategory
])
],
controllers: [EPPController],
providers: [
EPPService,
EPPAssignmentService,
EPPInventoryService
],
exports: [EPPService, EPPAssignmentService]
})
export class EPPModule {}
```
### 2.3. SafetyInspectionsModule - Inspecciones de Seguridad
```typescript
// safety-inspections.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
SafetyInspection,
InspectionTemplate,
InspectionItem,
InspectionResponse,
InspectionEvidence
} from './entities';
import { SafetyInspectionsService } from './services/safety-inspections.service';
import { InspectionTemplatesService } from './services/inspection-templates.service';
import { SafetyInspectionsController } from './controllers/safety-inspections.controller';
@Module({
imports: [
TypeOrmModule.forFeature([
SafetyInspection,
InspectionTemplate,
InspectionItem,
InspectionResponse,
InspectionEvidence
])
],
controllers: [SafetyInspectionsController],
providers: [
SafetyInspectionsService,
InspectionTemplatesService
],
exports: [SafetyInspectionsService]
})
export class SafetyInspectionsModule {}
```
### 2.4. IncidentsModule - Gestión de Incidentes
```typescript
// incidents.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
Incident,
IncidentInvestigation,
IncidentWitness,
IncidentEvidence,
CorrectiveAction,
RootCauseAnalysis
} from './entities';
import { IncidentsService } from './services/incidents.service';
import { IncidentInvestigationService } from './services/incident-investigation.service';
import { CorrectiveActionsService } from './services/corrective-actions.service';
import { IncidentWorkflowService } from './workflows/incident-workflow.service';
import { IncidentsController } from './controllers/incidents.controller';
@Module({
imports: [
TypeOrmModule.forFeature([
Incident,
IncidentInvestigation,
IncidentWitness,
IncidentEvidence,
CorrectiveAction,
RootCauseAnalysis
])
],
controllers: [IncidentsController],
providers: [
IncidentsService,
IncidentInvestigationService,
CorrectiveActionsService,
IncidentWorkflowService
],
exports: [IncidentsService, IncidentWorkflowService]
})
export class IncidentsModule {}
```
### 2.5. SafetyTrainingsModule - Capacitaciones
```typescript
// safety-trainings.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
SafetyTraining,
TrainingCourse,
TrainingAttendance,
TrainingCertification,
TrainingRequirement
} from './entities';
import { SafetyTrainingsService } from './services/safety-trainings.service';
import { TrainingCertificationsService } from './services/training-certifications.service';
import { SafetyTrainingsController } from './controllers/safety-trainings.controller';
@Module({
imports: [
TypeOrmModule.forFeature([
SafetyTraining,
TrainingCourse,
TrainingAttendance,
TrainingCertification,
TrainingRequirement
])
],
controllers: [SafetyTrainingsController],
providers: [
SafetyTrainingsService,
TrainingCertificationsService
],
exports: [SafetyTrainingsService]
})
export class SafetyTrainingsModule {}
```
## 3. Entities y Modelos de Datos
### 3.1. EPP - Equipos de Protección Personal
```typescript
// entities/epp-item.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('epp_items')
export class EPPItem {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
code: string;
@Column()
name: string;
@Column('text', { nullable: true })
description: string;
@ManyToOne(() => EPPCategory)
category: EPPCategory;
@Column('uuid')
categoryId: string;
@Column()
brand: string;
@Column({ nullable: true })
model: string;
@Column('jsonb', { nullable: true })
specifications: {
size?: string;
color?: string;
material?: string;
certifications?: string[];
};
@Column('decimal', { precision: 10, scale: 2 })
unitCost: number;
@Column('int')
usefulLifeDays: number; // Vida útil en días
@Column('boolean', { default: true })
requiresCertification: boolean;
@Column('jsonb', { nullable: true })
nomCompliance: {
norm: string;
requirement: string;
validUntil?: Date;
}[];
@Column('boolean', { default: true })
isActive: true;
@OneToMany(() => EPPInventory, inventory => inventory.eppItem)
inventory: EPPInventory[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/epp-category.entity.ts
@Entity('epp_categories')
export class EPPCategory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column('text', { nullable: true })
description: string;
@Column({ nullable: true })
icon: string;
@Column('simple-array', { nullable: true })
requiredByRoles: string[]; // Roles que requieren este EPP
@Column('boolean', { default: true })
isMandatory: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/epp-assignment.entity.ts
@Entity('epp_assignments')
export class EPPAssignment {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
workerId: string;
@ManyToOne(() => EPPItem)
eppItem: EPPItem;
@Column('uuid')
eppItemId: string;
@Column('uuid')
projectId: string;
@Column('date')
assignmentDate: Date;
@Column({ nullable: true })
serialNumber: string; // Para EPP individualizado
@Column()
quantity: number;
@Column('date')
expectedReturnDate: Date;
@Column('date', { nullable: true })
actualReturnDate: Date;
@Column({
type: 'enum',
enum: ['assigned', 'in_use', 'returned', 'damaged', 'lost'],
default: 'assigned'
})
status: string;
@Column('text', { nullable: true })
condition: string;
@Column('jsonb', { nullable: true })
maintenanceHistory: {
date: Date;
type: string;
notes: string;
}[];
@Column('uuid')
assignedBy: string;
@Column('uuid', { nullable: true })
receivedBy: string;
@Column('text', { nullable: true })
notes: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/epp-inventory.entity.ts
@Entity('epp_inventory')
export class EPPInventory {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => EPPItem, item => item.inventory)
eppItem: EPPItem;
@Column('uuid')
eppItemId: string;
@Column('uuid')
warehouseId: string;
@Column('uuid', { nullable: true })
projectId: string; // null = almacén central
@Column('int')
quantity: number;
@Column('int')
minimumStock: number;
@Column('int')
maximumStock: number;
@Column('int', { default: 0 })
reserved: number; // Cantidad reservada pero no asignada
@Column('date', { nullable: true })
lastRestockDate: Date;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/epp-delivery.entity.ts
@Entity('epp_deliveries')
export class EPPDelivery {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
deliveryNumber: string;
@Column('uuid')
workerId: string;
@Column('uuid')
projectId: string;
@Column('jsonb')
items: {
eppItemId: string;
quantity: number;
serialNumbers?: string[];
}[];
@Column('date')
deliveryDate: Date;
@Column('uuid')
deliveredBy: string;
@Column({ nullable: true })
signatureUrl: string; // Firma digital del trabajador
@Column('text', { nullable: true })
notes: string;
@CreateDateColumn()
createdAt: Date;
}
```
### 3.2. Safety Inspections - Inspecciones
```typescript
// entities/inspection-template.entity.ts
@Entity('inspection_templates')
export class InspectionTemplate {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
code: string;
@Column()
name: string;
@Column('text', { nullable: true })
description: string;
@Column({
type: 'enum',
enum: ['epp', 'workplace', 'equipment', 'vehicle', 'scaffolding', 'electrical', 'general']
})
inspectionType: string;
@Column('jsonb')
sections: {
id: string;
name: string;
order: number;
items: {
id: string;
question: string;
type: 'yes_no' | 'scale' | 'text' | 'numeric' | 'checklist';
required: boolean;
options?: string[];
criticalItem?: boolean; // Si falla, requiere acción inmediata
}[];
}[];
@Column('int')
frequency: number; // Frecuencia en días
@Column('simple-array', { nullable: true })
applicableRoles: string[];
@Column('boolean', { default: true })
requiresEvidence: boolean;
@Column('boolean', { default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/safety-inspection.entity.ts
@Entity('safety_inspections')
export class SafetyInspection {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
inspectionNumber: string;
@ManyToOne(() => InspectionTemplate)
template: InspectionTemplate;
@Column('uuid')
templateId: string;
@Column('uuid')
projectId: string;
@Column('uuid', { nullable: true })
inspectedAreaId: string; // Área/zona inspeccionada
@Column('uuid')
inspectorId: string;
@Column('timestamp')
inspectionDate: Date;
@Column('timestamp', { nullable: true })
scheduledDate: Date;
@Column({
type: 'enum',
enum: ['scheduled', 'in_progress', 'completed', 'cancelled'],
default: 'scheduled'
})
status: string;
@OneToMany(() => InspectionResponse, response => response.inspection, { cascade: true })
responses: InspectionResponse[];
@Column('decimal', { precision: 5, scale: 2, nullable: true })
overallScore: number; // Puntuación general
@Column('int', { default: 0 })
criticalFindings: number;
@Column('int', { default: 0 })
majorFindings: number;
@Column('int', { default: 0 })
minorFindings: number;
@Column('jsonb', { nullable: true })
geolocation: {
latitude: number;
longitude: number;
accuracy: number;
};
@Column('text', { nullable: true })
generalObservations: string;
@Column('simple-array', { nullable: true })
attachments: string[]; // URLs de fotos/documentos
@Column({ nullable: true })
signature: string; // Firma digital del inspector
@Column('timestamp', { nullable: true })
submittedAt: Date;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/inspection-response.entity.ts
@Entity('inspection_responses')
export class InspectionResponse {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => SafetyInspection, inspection => inspection.responses)
inspection: SafetyInspection;
@Column('uuid')
inspectionId: string;
@Column()
sectionId: string;
@Column()
itemId: string;
@Column('text')
response: string; // Respuesta según el tipo de pregunta
@Column('boolean', { default: false })
isNonCompliant: boolean;
@Column({
type: 'enum',
enum: ['critical', 'major', 'minor', 'observation'],
nullable: true
})
severity: string;
@Column('text', { nullable: true })
notes: string;
@Column('simple-array', { nullable: true })
evidenceUrls: string[];
@Column('boolean', { default: false })
requiresCorrectiveAction: boolean;
@CreateDateColumn()
createdAt: Date;
}
```
### 3.3. Incidents - Gestión de Incidentes
```typescript
// entities/incident.entity.ts
@Entity('incidents')
export class Incident {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
incidentNumber: string;
@Column('uuid')
projectId: string;
@Column({
type: 'enum',
enum: ['accident', 'near_miss', 'unsafe_condition', 'property_damage', 'environmental']
})
type: string;
@Column({
type: 'enum',
enum: ['fatal', 'serious', 'moderate', 'minor', 'first_aid']
})
severity: string;
@Column('timestamp')
incidentDate: Date;
@Column('time')
incidentTime: string;
@Column()
location: string;
@Column('uuid', { nullable: true })
areaId: string;
@Column('text')
description: string;
@Column('text', { nullable: true })
immediateActions: string;
@Column('jsonb', { nullable: true })
affectedPersons: {
workerId: string;
name: string;
role: string;
injuryType?: string;
injuryLocation?: string;
hospitalizedRequired?: boolean;
}[];
@Column('uuid')
reportedBy: string;
@Column('timestamp')
reportedAt: Date;
@Column({
type: 'enum',
enum: ['reported', 'investigating', 'analysis', 'corrective_actions', 'closed'],
default: 'reported'
})
status: string;
@Column('int', { default: 0 })
workDaysLost: number;
@Column('boolean', { default: false })
requiresAuthority: boolean; // Requiere notificación a autoridades
@Column('boolean', { default: false })
notifiedToSTPS: boolean; // Notificado a STPS
@Column('timestamp', { nullable: true })
stpsNotificationDate: Date;
@Column('simple-array', { nullable: true })
evidenceUrls: string[];
@Column('jsonb', { nullable: true })
geolocation: {
latitude: number;
longitude: number;
};
@OneToMany(() => IncidentInvestigation, investigation => investigation.incident)
investigations: IncidentInvestigation[];
@OneToMany(() => IncidentWitness, witness => witness.incident)
witnesses: IncidentWitness[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/incident-investigation.entity.ts
@Entity('incident_investigations')
export class IncidentInvestigation {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Incident, incident => incident.investigations)
incident: Incident;
@Column('uuid')
incidentId: string;
@Column('uuid')
investigatorId: string;
@Column('date')
investigationDate: Date;
@Column({
type: 'enum',
enum: ['initiated', 'in_progress', 'completed'],
default: 'initiated'
})
status: string;
@Column('text', { nullable: true })
findings: string;
@Column('text', { nullable: true })
contributingFactors: string;
@Column('jsonb', { nullable: true })
rootCauseAnalysis: {
method: '5why' | 'fishbone' | 'fta'; // Five Whys, Fishbone, Fault Tree Analysis
analysis: any;
rootCauses: string[];
};
@Column('text', { nullable: true })
recommendations: string;
@Column('simple-array', { nullable: true })
investigationDocuments: string[];
@OneToMany(() => CorrectiveAction, action => action.investigation)
correctiveActions: CorrectiveAction[];
@Column('timestamp', { nullable: true })
completedAt: Date;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/incident-witness.entity.ts
@Entity('incident_witnesses')
export class IncidentWitness {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Incident, incident => incident.witnesses)
incident: Incident;
@Column('uuid')
incidentId: string;
@Column('uuid', { nullable: true })
workerId: string; // null si es testigo externo
@Column()
name: string;
@Column()
role: string;
@Column({ nullable: true })
company: string; // Para subcontratistas
@Column('text')
statement: string;
@Column('timestamp')
statementDate: Date;
@Column({ nullable: true })
signatureUrl: string;
@Column()
contactPhone: string;
@CreateDateColumn()
createdAt: Date;
}
// entities/corrective-action.entity.ts
@Entity('corrective_actions')
export class CorrectiveAction {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => IncidentInvestigation, investigation => investigation.correctiveActions)
investigation: IncidentInvestigation;
@Column('uuid')
investigationId: string;
@Column()
actionNumber: string;
@Column('text')
description: string;
@Column({
type: 'enum',
enum: ['immediate', 'corrective', 'preventive']
})
actionType: string;
@Column({
type: 'enum',
enum: ['critical', 'high', 'medium', 'low']
})
priority: string;
@Column('uuid')
responsibleId: string;
@Column('date')
dueDate: Date;
@Column({
type: 'enum',
enum: ['pending', 'in_progress', 'completed', 'verified', 'overdue'],
default: 'pending'
})
status: string;
@Column('text', { nullable: true })
implementationNotes: string;
@Column('date', { nullable: true })
completionDate: Date;
@Column('uuid', { nullable: true })
verifiedBy: string;
@Column('date', { nullable: true })
verificationDate: Date;
@Column('text', { nullable: true })
verificationNotes: string;
@Column('simple-array', { nullable: true })
evidenceUrls: string[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
```
### 3.4. Safety Trainings - Capacitaciones
```typescript
// entities/training-course.entity.ts
@Entity('training_courses')
export class TrainingCourse {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
code: string;
@Column()
name: string;
@Column('text')
description: string;
@Column({
type: 'enum',
enum: ['safety_induction', 'epp_usage', 'emergency_response', 'first_aid', 'heights', 'confined_spaces', 'hazmat', 'electrical', 'machinery', 'nom_compliance']
})
category: string;
@Column('int')
durationHours: number;
@Column('boolean', { default: false })
isMandatory: boolean;
@Column('simple-array', { nullable: true })
requiredByRoles: string[];
@Column('int', { nullable: true })
validityMonths: number; // Meses de validez del certificado
@Column('int', { nullable: true })
passingScore: number; // Calificación mínima aprobatoria
@Column('jsonb', { nullable: true })
curriculum: {
topics: string[];
objectives: string[];
evaluation: string;
};
@Column('jsonb', { nullable: true })
nomCompliance: {
norm: string;
requirement: string;
}[];
@Column({ nullable: true })
certificateTemplateId: string;
@Column('boolean', { default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/safety-training.entity.ts
@Entity('safety_trainings')
export class SafetyTraining {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
trainingNumber: string;
@ManyToOne(() => TrainingCourse)
course: TrainingCourse;
@Column('uuid')
courseId: string;
@Column('uuid', { nullable: true })
projectId: string; // null = corporativo
@Column('date')
trainingDate: Date;
@Column('time')
startTime: string;
@Column('time')
endTime: string;
@Column()
location: string;
@Column('uuid')
instructorId: string;
@Column({ nullable: true })
externalInstructor: string; // Para instructores externos
@Column('int')
capacity: number;
@Column({
type: 'enum',
enum: ['scheduled', 'in_progress', 'completed', 'cancelled'],
default: 'scheduled'
})
status: string;
@OneToMany(() => TrainingAttendance, attendance => attendance.training)
attendances: TrainingAttendance[];
@Column('text', { nullable: true })
materials: string; // Material didáctico utilizado
@Column('simple-array', { nullable: true })
attachments: string[];
@Column('text', { nullable: true })
notes: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/training-attendance.entity.ts
@Entity('training_attendances')
export class TrainingAttendance {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => SafetyTraining, training => training.attendances)
training: SafetyTraining;
@Column('uuid')
trainingId: string;
@Column('uuid')
workerId: string;
@Column('boolean', { default: false })
attended: boolean;
@Column('int', { nullable: true })
score: number; // Calificación de evaluación
@Column('boolean', { default: false })
passed: boolean;
@Column({ nullable: true })
signatureUrl: string;
@Column('timestamp', { nullable: true })
signedAt: Date;
@Column('text', { nullable: true })
notes: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// entities/training-certification.entity.ts
@Entity('training_certifications')
export class TrainingCertification {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
certificateNumber: string;
@Column('uuid')
workerId: string;
@ManyToOne(() => TrainingCourse)
course: TrainingCourse;
@Column('uuid')
courseId: string;
@ManyToOne(() => SafetyTraining)
training: SafetyTraining;
@Column('uuid')
trainingId: string;
@Column('date')
issueDate: Date;
@Column('date')
expirationDate: Date;
@Column({
type: 'enum',
enum: ['active', 'expired', 'revoked'],
default: 'active'
})
status: string;
@Column({ nullable: true })
certificateUrl: string; // PDF del certificado
@Column('uuid')
issuedBy: string;
@Column('text', { nullable: true })
notes: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
```
## 4. DTOs (Data Transfer Objects)
### 4.1. EPP DTOs
```typescript
// dto/create-epp-assignment.dto.ts
import { IsUUID, IsDate, IsNumber, IsString, IsOptional, IsEnum, Min } from 'class-validator';
import { Type } from 'class-transformer';
export class CreateEPPAssignmentDto {
@IsUUID()
workerId: string;
@IsUUID()
eppItemId: string;
@IsUUID()
projectId: string;
@Type(() => Date)
@IsDate()
assignmentDate: Date;
@IsNumber()
@Min(1)
quantity: number;
@IsOptional()
@IsString()
serialNumber?: string;
@Type(() => Date)
@IsDate()
expectedReturnDate: Date;
@IsOptional()
@IsString()
notes?: string;
}
// dto/epp-delivery.dto.ts
export class CreateEPPDeliveryDto {
@IsUUID()
workerId: string;
@IsUUID()
projectId: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => EPPDeliveryItemDto)
items: EPPDeliveryItemDto[];
@Type(() => Date)
@IsDate()
deliveryDate: Date;
@IsOptional()
@IsString()
signatureUrl?: string;
@IsOptional()
@IsString()
notes?: string;
}
class EPPDeliveryItemDto {
@IsUUID()
eppItemId: string;
@IsNumber()
@Min(1)
quantity: number;
@IsOptional()
@IsArray()
@IsString({ each: true })
serialNumbers?: string[];
}
```
### 4.2. Inspections DTOs
```typescript
// dto/create-inspection.dto.ts
export class CreateSafetyInspectionDto {
@IsUUID()
templateId: string;
@IsUUID()
projectId: string;
@IsOptional()
@IsUUID()
inspectedAreaId?: string;
@Type(() => Date)
@IsDate()
inspectionDate: Date;
@IsOptional()
@Type(() => Date)
@IsDate()
scheduledDate?: Date;
@IsOptional()
@ValidateNested()
@Type(() => GeolocationDto)
geolocation?: GeolocationDto;
}
// dto/submit-inspection.dto.ts
export class SubmitInspectionDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => InspectionResponseDto)
responses: InspectionResponseDto[];
@IsOptional()
@IsString()
generalObservations?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
attachments?: string[];
@IsOptional()
@IsString()
signature?: string;
}
class InspectionResponseDto {
@IsString()
sectionId: string;
@IsString()
itemId: string;
@IsString()
response: string;
@IsOptional()
@IsBoolean()
isNonCompliant?: boolean;
@IsOptional()
@IsEnum(['critical', 'major', 'minor', 'observation'])
severity?: string;
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
evidenceUrls?: string[];
}
```
### 4.3. Incidents DTOs
```typescript
// dto/create-incident.dto.ts
export class CreateIncidentDto {
@IsUUID()
projectId: string;
@IsEnum(['accident', 'near_miss', 'unsafe_condition', 'property_damage', 'environmental'])
type: string;
@IsEnum(['fatal', 'serious', 'moderate', 'minor', 'first_aid'])
severity: string;
@Type(() => Date)
@IsDate()
incidentDate: Date;
@IsString()
incidentTime: string;
@IsString()
location: string;
@IsOptional()
@IsUUID()
areaId?: string;
@IsString()
description: string;
@IsOptional()
@IsString()
immediateActions?: string;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AffectedPersonDto)
affectedPersons?: AffectedPersonDto[];
@IsOptional()
@IsArray()
@IsString({ each: true })
evidenceUrls?: string[];
@IsOptional()
@ValidateNested()
@Type(() => GeolocationDto)
geolocation?: GeolocationDto;
}
class AffectedPersonDto {
@IsUUID()
workerId: string;
@IsString()
name: string;
@IsString()
role: string;
@IsOptional()
@IsString()
injuryType?: string;
@IsOptional()
@IsString()
injuryLocation?: string;
@IsOptional()
@IsBoolean()
hospitalizedRequired?: boolean;
}
// dto/create-investigation.dto.ts
export class CreateInvestigationDto {
@IsUUID()
incidentId: string;
@IsUUID()
investigatorId: string;
@Type(() => Date)
@IsDate()
investigationDate: Date;
@IsOptional()
@IsString()
findings?: string;
@IsOptional()
@IsString()
contributingFactors?: string;
}
// dto/complete-investigation.dto.ts
export class CompleteInvestigationDto {
@IsString()
findings: string;
@IsString()
contributingFactors: string;
@ValidateNested()
@Type(() => RootCauseAnalysisDto)
rootCauseAnalysis: RootCauseAnalysisDto;
@IsString()
recommendations: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateCorrectiveActionDto)
correctiveActions: CreateCorrectiveActionDto[];
}
class RootCauseAnalysisDto {
@IsEnum(['5why', 'fishbone', 'fta'])
method: string;
@IsObject()
analysis: any;
@IsArray()
@IsString({ each: true })
rootCauses: string[];
}
// dto/create-corrective-action.dto.ts
export class CreateCorrectiveActionDto {
@IsString()
description: string;
@IsEnum(['immediate', 'corrective', 'preventive'])
actionType: string;
@IsEnum(['critical', 'high', 'medium', 'low'])
priority: string;
@IsUUID()
responsibleId: string;
@Type(() => Date)
@IsDate()
dueDate: Date;
}
```
### 4.4. Trainings DTOs
```typescript
// dto/create-training.dto.ts
export class CreateSafetyTrainingDto {
@IsUUID()
courseId: string;
@IsOptional()
@IsUUID()
projectId?: string;
@Type(() => Date)
@IsDate()
trainingDate: Date;
@IsString()
startTime: string;
@IsString()
endTime: string;
@IsString()
location: string;
@IsUUID()
instructorId: string;
@IsOptional()
@IsString()
externalInstructor?: string;
@IsNumber()
@Min(1)
capacity: number;
@IsOptional()
@IsString()
materials?: string;
@IsOptional()
@IsString()
notes?: string;
}
// dto/register-attendance.dto.ts
export class RegisterAttendanceDto {
@IsUUID()
trainingId: string;
@IsArray()
@IsUUID({}, { each: true })
workerIds: string[];
}
// dto/complete-attendance.dto.ts
export class CompleteAttendanceDto {
@IsBoolean()
attended: boolean;
@IsOptional()
@IsNumber()
@Min(0)
@Max(100)
score?: number;
@IsOptional()
@IsString()
signatureUrl?: string;
@IsOptional()
@IsString()
notes?: string;
}
```
## 5. Services - Lógica de Negocio
### 5.1. EPP Service
```typescript
// services/epp.service.ts
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EPPItem, EPPAssignment, EPPInventory } from '../entities';
@Injectable()
export class EPPService {
constructor(
@InjectRepository(EPPItem)
private eppItemRepository: Repository<EPPItem>,
@InjectRepository(EPPInventory)
private inventoryRepository: Repository<EPPInventory>
) {}
async findAll(filters?: any): Promise<EPPItem[]> {
const query = this.eppItemRepository.createQueryBuilder('epp')
.leftJoinAndSelect('epp.category', 'category')
.where('epp.isActive = :isActive', { isActive: true });
if (filters?.categoryId) {
query.andWhere('epp.categoryId = :categoryId', { categoryId: filters.categoryId });
}
if (filters?.search) {
query.andWhere('(epp.name ILIKE :search OR epp.code ILIKE :search)', {
search: `%${filters.search}%`
});
}
return query.getMany();
}
async findOne(id: string): Promise<EPPItem> {
const item = await this.eppItemRepository.findOne({
where: { id },
relations: ['category']
});
if (!item) {
throw new NotFoundException(`EPP item ${id} not found`);
}
return item;
}
async checkAvailability(eppItemId: string, projectId: string, quantity: number): Promise<boolean> {
const inventory = await this.inventoryRepository.findOne({
where: { eppItemId, projectId }
});
if (!inventory) {
return false;
}
const available = inventory.quantity - inventory.reserved;
return available >= quantity;
}
async getInventoryByProject(projectId: string): Promise<EPPInventory[]> {
return this.inventoryRepository.find({
where: { projectId },
relations: ['eppItem', 'eppItem.category']
});
}
async getLowStockItems(projectId?: string): Promise<EPPInventory[]> {
const query = this.inventoryRepository.createQueryBuilder('inv')
.leftJoinAndSelect('inv.eppItem', 'epp')
.where('inv.quantity <= inv.minimumStock');
if (projectId) {
query.andWhere('inv.projectId = :projectId', { projectId });
}
return query.getMany();
}
}
// services/epp-assignment.service.ts
@Injectable()
export class EPPAssignmentService {
constructor(
@InjectRepository(EPPAssignment)
private assignmentRepository: Repository<EPPAssignment>,
@InjectRepository(EPPInventory)
private inventoryRepository: Repository<EPPInventory>,
private eppService: EPPService,
private notificationsService: NotificationsService
) {}
async assignEPP(dto: CreateEPPAssignmentDto, userId: string): Promise<EPPAssignment> {
// Verificar disponibilidad
const isAvailable = await this.eppService.checkAvailability(
dto.eppItemId,
dto.projectId,
dto.quantity
);
if (!isAvailable) {
throw new BadRequestException('Insufficient EPP inventory');
}
// Crear asignación
const assignment = this.assignmentRepository.create({
...dto,
assignedBy: userId,
status: 'assigned'
});
// Reducir inventario
await this.inventoryRepository.decrement(
{ eppItemId: dto.eppItemId, projectId: dto.projectId },
'quantity',
dto.quantity
);
const saved = await this.assignmentRepository.save(assignment);
// Notificar al trabajador
await this.notificationsService.send({
userId: dto.workerId,
type: 'epp_assigned',
title: 'EPP Asignado',
message: `Se te ha asignado equipo de protección personal`,
data: { assignmentId: saved.id }
});
return saved;
}
async returnEPP(assignmentId: string, condition: string, userId: string): Promise<EPPAssignment> {
const assignment = await this.assignmentRepository.findOne({
where: { id: assignmentId }
});
if (!assignment) {
throw new NotFoundException('Assignment not found');
}
assignment.actualReturnDate = new Date();
assignment.condition = condition;
assignment.receivedBy = userId;
assignment.status = condition === 'good' ? 'returned' : 'damaged';
// Si está en buenas condiciones, regresar al inventario
if (condition === 'good') {
await this.inventoryRepository.increment(
{ eppItemId: assignment.eppItemId, projectId: assignment.projectId },
'quantity',
assignment.quantity
);
}
return this.assignmentRepository.save(assignment);
}
async getWorkerEPP(workerId: string, projectId: string): Promise<EPPAssignment[]> {
return this.assignmentRepository.find({
where: {
workerId,
projectId,
status: 'assigned'
},
relations: ['eppItem', 'eppItem.category']
});
}
async getExpiringEPP(days: number = 30): Promise<EPPAssignment[]> {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + days);
return this.assignmentRepository.createQueryBuilder('assignment')
.leftJoinAndSelect('assignment.eppItem', 'epp')
.where('assignment.status = :status', { status: 'assigned' })
.andWhere('assignment.expectedReturnDate <= :futureDate', { futureDate })
.getMany();
}
}
```
### 5.2. Incidents Workflow Service
```typescript
// workflows/incident-workflow.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Incident, IncidentInvestigation, CorrectiveAction } from '../entities';
@Injectable()
export class IncidentWorkflowService {
constructor(
@InjectRepository(Incident)
private incidentRepository: Repository<Incident>,
@InjectRepository(IncidentInvestigation)
private investigationRepository: Repository<IncidentInvestigation>,
@InjectRepository(CorrectiveAction)
private actionRepository: Repository<CorrectiveAction>,
private notificationsService: NotificationsService,
private alertsService: AlertsService
) {}
/**
* Workflow Step 1: Reporte inicial del incidente
*/
async reportIncident(dto: CreateIncidentDto, reportedBy: string): Promise<Incident> {
const incidentNumber = await this.generateIncidentNumber();
const incident = this.incidentRepository.create({
...dto,
incidentNumber,
reportedBy,
reportedAt: new Date(),
status: 'reported'
});
const saved = await this.incidentRepository.save(incident);
// Determinar si requiere notificación a autoridades (STPS)
if (this.requiresAuthorityNotification(saved)) {
saved.requiresAuthority = true;
await this.incidentRepository.save(saved);
// Alerta inmediata
await this.alertsService.sendCriticalAlert({
type: 'incident_requires_authority',
incidentId: saved.id,
severity: saved.severity
});
}
// Notificar a responsables de seguridad
await this.notifySecurityTeam(saved);
// Iniciar investigación automáticamente para incidentes graves
if (['fatal', 'serious'].includes(saved.severity)) {
await this.initiateInvestigation(saved.id);
}
return saved;
}
/**
* Workflow Step 2: Iniciar investigación
*/
async initiateInvestigation(incidentId: string, investigatorId?: string): Promise<IncidentInvestigation> {
const incident = await this.incidentRepository.findOne({
where: { id: incidentId }
});
if (!incident) {
throw new NotFoundException('Incident not found');
}
// Asignar investigador automáticamente si no se especifica
if (!investigatorId) {
investigatorId = await this.assignInvestigator(incident);
}
const investigation = this.investigationRepository.create({
incidentId,
investigatorId,
investigationDate: new Date(),
status: 'initiated'
});
const saved = await this.investigationRepository.save(investigation);
// Actualizar estado del incidente
incident.status = 'investigating';
await this.incidentRepository.save(incident);
// Notificar al investigador
await this.notificationsService.send({
userId: investigatorId,
type: 'investigation_assigned',
title: 'Investigación de Incidente Asignada',
message: `Se te ha asignado la investigación del incidente ${incident.incidentNumber}`,
data: { incidentId, investigationId: saved.id },
priority: 'high'
});
return saved;
}
/**
* Workflow Step 3: Completar investigación con análisis de causa raíz
*/
async completeInvestigation(
investigationId: string,
dto: CompleteInvestigationDto
): Promise<IncidentInvestigation> {
const investigation = await this.investigationRepository.findOne({
where: { id: investigationId },
relations: ['incident']
});
if (!investigation) {
throw new NotFoundException('Investigation not found');
}
// Actualizar investigación
Object.assign(investigation, {
findings: dto.findings,
contributingFactors: dto.contributingFactors,
rootCauseAnalysis: dto.rootCauseAnalysis,
recommendations: dto.recommendations,
status: 'completed',
completedAt: new Date()
});
await this.investigationRepository.save(investigation);
// Crear acciones correctivas
for (const actionDto of dto.correctiveActions) {
await this.createCorrectiveAction(investigationId, actionDto);
}
// Actualizar estado del incidente
investigation.incident.status = 'corrective_actions';
await this.incidentRepository.save(investigation.incident);
return investigation;
}
/**
* Workflow Step 4: Crear y asignar acciones correctivas
*/
async createCorrectiveAction(
investigationId: string,
dto: CreateCorrectiveActionDto
): Promise<CorrectiveAction> {
const actionNumber = await this.generateActionNumber();
const action = this.actionRepository.create({
...dto,
investigationId,
actionNumber,
status: 'pending'
});
const saved = await this.actionRepository.save(action);
// Notificar al responsable
await this.notificationsService.send({
userId: dto.responsibleId,
type: 'corrective_action_assigned',
title: 'Acción Correctiva Asignada',
message: `Se te ha asignado una acción correctiva: ${dto.description}`,
data: {
actionId: saved.id,
dueDate: dto.dueDate,
priority: dto.priority
},
priority: dto.priority === 'critical' ? 'high' : 'normal'
});
return saved;
}
/**
* Workflow Step 5: Implementar acción correctiva
*/
async implementAction(
actionId: string,
implementationNotes: string,
evidenceUrls: string[]
): Promise<CorrectiveAction> {
const action = await this.actionRepository.findOne({
where: { id: actionId }
});
if (!action) {
throw new NotFoundException('Corrective action not found');
}
action.status = 'completed';
action.completionDate = new Date();
action.implementationNotes = implementationNotes;
action.evidenceUrls = evidenceUrls;
return this.actionRepository.save(action);
}
/**
* Workflow Step 6: Verificar acción correctiva
*/
async verifyAction(
actionId: string,
verifiedBy: string,
verificationNotes: string,
approved: boolean
): Promise<CorrectiveAction> {
const action = await this.actionRepository.findOne({
where: { id: actionId },
relations: ['investigation', 'investigation.incident']
});
if (!action) {
throw new NotFoundException('Corrective action not found');
}
action.verifiedBy = verifiedBy;
action.verificationDate = new Date();
action.verificationNotes = verificationNotes;
action.status = approved ? 'verified' : 'in_progress';
await this.actionRepository.save(action);
// Si todas las acciones están verificadas, cerrar el incidente
const allActions = await this.actionRepository.find({
where: { investigationId: action.investigationId }
});
const allVerified = allActions.every(a => a.status === 'verified');
if (allVerified) {
action.investigation.incident.status = 'closed';
await this.incidentRepository.save(action.investigation.incident);
}
return action;
}
/**
* Verifica si el incidente requiere notificación a autoridades según NOM-STPS
*/
private requiresAuthorityNotification(incident: Incident): boolean {
// NOM-019-STPS-2011: Notificar incidentes fatales o con días perdidos
if (incident.severity === 'fatal') {
return true;
}
if (incident.severity === 'serious' && incident.workDaysLost > 0) {
return true;
}
return false;
}
/**
* Asigna investigador basado en severidad y disponibilidad
*/
private async assignInvestigator(incident: Incident): Promise<string> {
// Lógica para asignar investigador
// Por ahora retorna un ID dummy - implementar según estructura organizacional
return 'safety-manager-id';
}
/**
* Notifica al equipo de seguridad sobre nuevo incidente
*/
private async notifySecurityTeam(incident: Incident): Promise<void> {
await this.alertsService.sendToRole({
role: 'safety_manager',
type: 'new_incident',
title: `Nuevo Incidente: ${incident.type}`,
message: `Severidad: ${incident.severity} - ${incident.location}`,
data: { incidentId: incident.id },
priority: incident.severity === 'fatal' ? 'critical' : 'high'
});
}
private async generateIncidentNumber(): Promise<string> {
const year = new Date().getFullYear();
const count = await this.incidentRepository.count({
where: {
incidentNumber: Like(`INC-${year}%`)
}
});
return `INC-${year}-${String(count + 1).padStart(4, '0')}`;
}
private async generateActionNumber(): Promise<string> {
const year = new Date().getFullYear();
const count = await this.actionRepository.count({
where: {
actionNumber: Like(`AC-${year}%`)
}
});
return `AC-${year}-${String(count + 1).padStart(4, '0')}`;
}
}
```
### 5.3. Safety Inspections Service
```typescript
// services/safety-inspections.service.ts
@Injectable()
export class SafetyInspectionsService {
constructor(
@InjectRepository(SafetyInspection)
private inspectionRepository: Repository<SafetyInspection>,
@InjectRepository(InspectionResponse)
private responseRepository: Repository<InspectionResponse>,
private notificationsService: NotificationsService
) {}
async createInspection(dto: CreateSafetyInspectionDto, inspectorId: string): Promise<SafetyInspection> {
const inspectionNumber = await this.generateInspectionNumber();
const inspection = this.inspectionRepository.create({
...dto,
inspectionNumber,
inspectorId,
status: 'in_progress'
});
return this.inspectionRepository.save(inspection);
}
async submitInspection(inspectionId: string, dto: SubmitInspectionDto): Promise<SafetyInspection> {
const inspection = await this.inspectionRepository.findOne({
where: { id: inspectionId },
relations: ['template']
});
if (!inspection) {
throw new NotFoundException('Inspection not found');
}
// Guardar respuestas
const responses = dto.responses.map(r =>
this.responseRepository.create({
...r,
inspectionId
})
);
await this.responseRepository.save(responses);
// Calcular métricas
const { score, findings } = this.calculateInspectionScore(responses, inspection.template);
// Actualizar inspección
inspection.status = 'completed';
inspection.submittedAt = new Date();
inspection.overallScore = score;
inspection.criticalFindings = findings.critical;
inspection.majorFindings = findings.major;
inspection.minorFindings = findings.minor;
inspection.generalObservations = dto.generalObservations;
inspection.attachments = dto.attachments;
inspection.signature = dto.signature;
const saved = await this.inspectionRepository.save(inspection);
// Generar alertas para hallazgos críticos
if (findings.critical > 0) {
await this.notificationsService.send({
userId: 'safety-manager-id',
type: 'critical_inspection_findings',
title: 'Hallazgos Críticos en Inspección',
message: `${findings.critical} hallazgos críticos en ${inspection.inspectionNumber}`,
data: { inspectionId: saved.id },
priority: 'high'
});
}
return saved;
}
private calculateInspectionScore(
responses: InspectionResponse[],
template: InspectionTemplate
): { score: number; findings: { critical: number; major: number; minor: number } } {
let totalPoints = 0;
let earnedPoints = 0;
const findings = { critical: 0, major: 0, minor: 0 };
for (const response of responses) {
totalPoints += 1;
if (!response.isNonCompliant) {
earnedPoints += 1;
} else {
if (response.severity === 'critical') findings.critical++;
else if (response.severity === 'major') findings.major++;
else if (response.severity === 'minor') findings.minor++;
}
}
const score = totalPoints > 0 ? (earnedPoints / totalPoints) * 100 : 0;
return { score, findings };
}
private async generateInspectionNumber(): Promise<string> {
const year = new Date().getFullYear();
const month = String(new Date().getMonth() + 1).padStart(2, '0');
const count = await this.inspectionRepository.count({
where: {
inspectionNumber: Like(`INS-${year}${month}%`)
}
});
return `INS-${year}${month}-${String(count + 1).padStart(4, '0')}`;
}
}
```
## 6. Controllers - Endpoints API
### 6.1. EPP Controller
```typescript
// controllers/epp.controller.ts
import { Controller, Get, Post, Put, Body, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/guards/jwt-auth.guard';
import { RolesGuard } from '@/guards/roles.guard';
import { Roles } from '@/decorators/roles.decorator';
import { CurrentUser } from '@/decorators/current-user.decorator';
@ApiTags('EPP - Equipos de Protección Personal')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('safety/epp')
export class EPPController {
constructor(
private readonly eppService: EPPService,
private readonly assignmentService: EPPAssignmentService,
private readonly inventoryService: EPPInventoryService
) {}
@Get()
@ApiOperation({ summary: 'Listar equipos EPP' })
@Roles('safety_manager', 'warehouse_manager', 'supervisor')
async findAll(@Query() filters: any) {
return this.eppService.findAll(filters);
}
@Get('inventory/:projectId')
@ApiOperation({ summary: 'Obtener inventario EPP por proyecto' })
@Roles('safety_manager', 'warehouse_manager', 'supervisor')
async getInventory(@Param('projectId') projectId: string) {
return this.eppService.getInventoryByProject(projectId);
}
@Get('inventory/low-stock')
@ApiOperation({ summary: 'Items con stock bajo' })
@Roles('safety_manager', 'warehouse_manager')
async getLowStock(@Query('projectId') projectId?: string) {
return this.eppService.getLowStockItems(projectId);
}
@Post('assignments')
@ApiOperation({ summary: 'Asignar EPP a trabajador' })
@Roles('safety_manager', 'warehouse_manager', 'supervisor')
async assignEPP(
@Body() dto: CreateEPPAssignmentDto,
@CurrentUser() user: any
) {
return this.assignmentService.assignEPP(dto, user.id);
}
@Put('assignments/:id/return')
@ApiOperation({ summary: 'Devolver EPP asignado' })
@Roles('safety_manager', 'warehouse_manager')
async returnEPP(
@Param('id') id: string,
@Body() body: { condition: string },
@CurrentUser() user: any
) {
return this.assignmentService.returnEPP(id, body.condition, user.id);
}
@Get('workers/:workerId/assignments')
@ApiOperation({ summary: 'EPP asignado a trabajador - Para apps móviles' })
@Roles('worker', 'supervisor', 'safety_manager')
async getWorkerEPP(
@Param('workerId') workerId: string,
@Query('projectId') projectId: string
) {
return this.assignmentService.getWorkerEPP(workerId, projectId);
}
@Get('assignments/expiring')
@ApiOperation({ summary: 'EPP próximo a vencer' })
@Roles('safety_manager', 'supervisor')
async getExpiringEPP(@Query('days') days: number = 30) {
return this.assignmentService.getExpiringEPP(days);
}
@Post('deliveries')
@ApiOperation({ summary: 'Registrar entrega de EPP - Para MOB-003' })
@Roles('warehouse_manager', 'safety_manager')
async createDelivery(
@Body() dto: CreateEPPDeliveryDto,
@CurrentUser() user: any
) {
return this.assignmentService.createDelivery(dto, user.id);
}
}
```
### 6.2. Safety Inspections Controller
```typescript
// controllers/safety-inspections.controller.ts
@ApiTags('Inspecciones de Seguridad')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('safety/inspections')
export class SafetyInspectionsController {
constructor(
private readonly inspectionsService: SafetyInspectionsService,
private readonly templatesService: InspectionTemplatesService
) {}
@Get('templates')
@ApiOperation({ summary: 'Listar plantillas de inspección' })
@Roles('safety_manager', 'supervisor', 'inspector')
async getTemplates(@Query('type') type?: string) {
return this.templatesService.findAll({ type });
}
@Get('templates/:id')
@ApiOperation({ summary: 'Obtener plantilla - Para apps móviles MOB-003' })
@Roles('safety_manager', 'supervisor', 'inspector')
async getTemplate(@Param('id') id: string) {
return this.templatesService.findOne(id);
}
@Post()
@ApiOperation({ summary: 'Crear inspección - Desde app móvil MOB-003' })
@Roles('safety_manager', 'supervisor', 'inspector')
async create(
@Body() dto: CreateSafetyInspectionDto,
@CurrentUser() user: any
) {
return this.inspectionsService.createInspection(dto, user.id);
}
@Put(':id/submit')
@ApiOperation({ summary: 'Enviar inspección completada - Desde app móvil' })
@Roles('safety_manager', 'supervisor', 'inspector')
async submit(
@Param('id') id: string,
@Body() dto: SubmitInspectionDto
) {
return this.inspectionsService.submitInspection(id, dto);
}
@Get('project/:projectId')
@ApiOperation({ summary: 'Inspecciones por proyecto' })
@Roles('safety_manager', 'supervisor', 'inspector')
async getByProject(
@Param('projectId') projectId: string,
@Query() filters: any
) {
return this.inspectionsService.findByProject(projectId, filters);
}
@Get(':id')
@ApiOperation({ summary: 'Detalle de inspección' })
@Roles('safety_manager', 'supervisor', 'inspector')
async findOne(@Param('id') id: string) {
return this.inspectionsService.findOne(id);
}
@Get('scheduled/pending')
@ApiOperation({ summary: 'Inspecciones pendientes - Para MOB-003' })
@Roles('safety_manager', 'supervisor', 'inspector')
async getPending(@Query('projectId') projectId: string) {
return this.inspectionsService.findPending(projectId);
}
}
```
### 6.3. Incidents Controller
```typescript
// controllers/incidents.controller.ts
@ApiTags('Incidentes y Accidentes')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('safety/incidents')
export class IncidentsController {
constructor(
private readonly incidentsService: IncidentsService,
private readonly workflowService: IncidentWorkflowService,
private readonly investigationService: IncidentInvestigationService
) {}
@Post()
@ApiOperation({ summary: 'Reportar incidente - Desde app móvil MOB-004' })
@Roles('worker', 'supervisor', 'safety_manager')
async report(
@Body() dto: CreateIncidentDto,
@CurrentUser() user: any
) {
return this.workflowService.reportIncident(dto, user.id);
}
@Get()
@ApiOperation({ summary: 'Listar incidentes' })
@Roles('safety_manager', 'supervisor')
async findAll(@Query() filters: any) {
return this.incidentsService.findAll(filters);
}
@Get(':id')
@ApiOperation({ summary: 'Detalle de incidente' })
@Roles('safety_manager', 'supervisor', 'worker')
async findOne(@Param('id') id: string) {
return this.incidentsService.findOne(id);
}
@Post(':id/investigation')
@ApiOperation({ summary: 'Iniciar investigación' })
@Roles('safety_manager')
async initiateInvestigation(
@Param('id') id: string,
@Body() body: { investigatorId?: string }
) {
return this.workflowService.initiateInvestigation(id, body.investigatorId);
}
@Put('investigations/:id/complete')
@ApiOperation({ summary: 'Completar investigación con análisis' })
@Roles('safety_manager', 'investigator')
async completeInvestigation(
@Param('id') id: string,
@Body() dto: CompleteInvestigationDto
) {
return this.workflowService.completeInvestigation(id, dto);
}
@Post('investigations/:id/corrective-actions')
@ApiOperation({ summary: 'Agregar acción correctiva' })
@Roles('safety_manager')
async addCorrectiveAction(
@Param('id') investigationId: string,
@Body() dto: CreateCorrectiveActionDto
) {
return this.workflowService.createCorrectiveAction(investigationId, dto);
}
@Put('corrective-actions/:id/implement')
@ApiOperation({ summary: 'Implementar acción correctiva' })
@Roles('supervisor', 'safety_manager')
async implementAction(
@Param('id') id: string,
@Body() body: { implementationNotes: string; evidenceUrls: string[] }
) {
return this.workflowService.implementAction(
id,
body.implementationNotes,
body.evidenceUrls
);
}
@Put('corrective-actions/:id/verify')
@ApiOperation({ summary: 'Verificar acción correctiva' })
@Roles('safety_manager')
async verifyAction(
@Param('id') id: string,
@Body() body: { verificationNotes: string; approved: boolean },
@CurrentUser() user: any
) {
return this.workflowService.verifyAction(
id,
user.id,
body.verificationNotes,
body.approved
);
}
@Get('project/:projectId/statistics')
@ApiOperation({ summary: 'Estadísticas de incidentes por proyecto' })
@Roles('safety_manager', 'supervisor')
async getStatistics(@Param('projectId') projectId: string) {
return this.incidentsService.getStatistics(projectId);
}
@Post(':id/witnesses')
@ApiOperation({ summary: 'Agregar testigo a incidente' })
@Roles('safety_manager', 'investigator')
async addWitness(
@Param('id') id: string,
@Body() dto: CreateWitnessDto
) {
return this.incidentsService.addWitness(id, dto);
}
}
```
### 6.4. Safety Trainings Controller
```typescript
// controllers/safety-trainings.controller.ts
@ApiTags('Capacitaciones de Seguridad')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('safety/trainings')
export class SafetyTrainingsController {
constructor(
private readonly trainingsService: SafetyTrainingsService,
private readonly certificationsService: TrainingCertificationsService
) {}
@Get('courses')
@ApiOperation({ summary: 'Listar cursos de capacitación' })
async getCourses(@Query() filters: any) {
return this.trainingsService.getCourses(filters);
}
@Post()
@ApiOperation({ summary: 'Programar capacitación' })
@Roles('safety_manager', 'hr_manager')
async schedule(@Body() dto: CreateSafetyTrainingDto) {
return this.trainingsService.scheduleTraining(dto);
}
@Get()
@ApiOperation({ summary: 'Listar capacitaciones' })
@Roles('safety_manager', 'supervisor')
async findAll(@Query() filters: any) {
return this.trainingsService.findAll(filters);
}
@Get(':id')
@ApiOperation({ summary: 'Detalle de capacitación' })
async findOne(@Param('id') id: string) {
return this.trainingsService.findOne(id);
}
@Post(':id/attendances')
@ApiOperation({ summary: 'Registrar asistentes' })
@Roles('safety_manager', 'instructor')
async registerAttendees(
@Param('id') id: string,
@Body() dto: RegisterAttendanceDto
) {
return this.trainingsService.registerAttendances(id, dto);
}
@Put('attendances/:id')
@ApiOperation({ summary: 'Completar asistencia y calificación' })
@Roles('safety_manager', 'instructor')
async completeAttendance(
@Param('id') id: string,
@Body() dto: CompleteAttendanceDto
) {
return this.trainingsService.completeAttendance(id, dto);
}
@Post(':id/complete')
@ApiOperation({ summary: 'Finalizar capacitación y generar certificados' })
@Roles('safety_manager')
async completeTraining(@Param('id') id: string) {
return this.trainingsService.completeTraining(id);
}
@Get('workers/:workerId/certifications')
@ApiOperation({ summary: 'Certificaciones de trabajador - Para MOB-003/MOB-004' })
async getWorkerCertifications(@Param('workerId') workerId: string) {
return this.certificationsService.getWorkerCertifications(workerId);
}
@Get('workers/:workerId/expired')
@ApiOperation({ summary: 'Certificaciones vencidas o próximas a vencer' })
@Roles('safety_manager', 'hr_manager')
async getExpiredCertifications(
@Param('workerId') workerId: string,
@Query('days') days: number = 30
) {
return this.certificationsService.getExpiring(workerId, days);
}
@Get('project/:projectId/compliance')
@ApiOperation({ summary: 'Cumplimiento de capacitaciones por proyecto' })
@Roles('safety_manager', 'supervisor')
async getComplianceReport(@Param('projectId') projectId: string) {
return this.trainingsService.getComplianceReport(projectId);
}
}
```
## 7. Sistema de Alertas y Notificaciones
### 7.1. Alerts Service
```typescript
// shared/alerts/alerts.service.ts
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class AlertsService {
constructor(
private eventEmitter: EventEmitter2,
private notificationsService: NotificationsService,
private emailService: EmailService,
private smsService: SMSService
) {}
/**
* Envía alerta crítica (incidentes fatales, condiciones peligrosas)
*/
async sendCriticalAlert(data: {
type: string;
incidentId?: string;
inspectionId?: string;
severity: string;
description?: string;
}): Promise<void> {
// Notificación push
await this.notificationsService.sendToRole({
role: 'safety_manager',
type: data.type,
title: 'ALERTA CRÍTICA DE SEGURIDAD',
message: this.buildAlertMessage(data),
data,
priority: 'critical'
});
// Email
await this.emailService.sendToRole({
role: 'safety_manager',
subject: 'ALERTA CRÍTICA DE SEGURIDAD',
template: 'critical-safety-alert',
data
});
// SMS para incidentes fatales
if (data.severity === 'fatal') {
await this.smsService.sendToRole({
role: 'safety_manager',
message: this.buildSMSAlert(data)
});
}
// Emitir evento para integraciones
this.eventEmitter.emit('safety.critical-alert', data);
}
/**
* Alertas de EPP
*/
async alertLowStock(inventory: EPPInventory): Promise<void> {
await this.notificationsService.sendToRole({
role: 'warehouse_manager',
type: 'epp_low_stock',
title: 'Stock Bajo de EPP',
message: `${inventory.eppItem.name}: ${inventory.quantity} unidades`,
data: { inventoryId: inventory.id },
priority: 'normal'
});
}
async alertExpiringEPP(assignments: EPPAssignment[]): Promise<void> {
for (const assignment of assignments) {
await this.notificationsService.send({
userId: assignment.workerId,
type: 'epp_expiring',
title: 'EPP Próximo a Vencer',
message: `Tu ${assignment.eppItem.name} debe devolverse el ${assignment.expectedReturnDate}`,
data: { assignmentId: assignment.id },
priority: 'normal'
});
}
}
/**
* Alertas de inspecciones
*/
async alertOverdueInspection(inspection: SafetyInspection): Promise<void> {
await this.notificationsService.send({
userId: inspection.inspectorId,
type: 'inspection_overdue',
title: 'Inspección Pendiente',
message: `Inspección ${inspection.inspectionNumber} está vencida`,
data: { inspectionId: inspection.id },
priority: 'high'
});
}
/**
* Alertas de acciones correctivas
*/
async alertOverdueAction(action: CorrectiveAction): Promise<void> {
await this.notificationsService.send({
userId: action.responsibleId,
type: 'action_overdue',
title: 'Acción Correctiva Vencida',
message: `${action.description} - Vencimiento: ${action.dueDate}`,
data: { actionId: action.id },
priority: 'high'
});
// Notificar también al supervisor
await this.notificationsService.sendToRole({
role: 'safety_manager',
type: 'action_overdue_escalation',
title: 'Acción Correctiva Vencida',
message: `Acción ${action.actionNumber} está vencida`,
data: { actionId: action.id },
priority: 'high'
});
}
/**
* Alertas de capacitaciones
*/
async alertExpiringCertifications(certifications: TrainingCertification[]): Promise<void> {
for (const cert of certifications) {
await this.notificationsService.send({
userId: cert.workerId,
type: 'certification_expiring',
title: 'Certificación Próxima a Vencer',
message: `Tu certificación ${cert.course.name} vence el ${cert.expirationDate}`,
data: { certificationId: cert.id },
priority: 'normal'
});
}
}
private buildAlertMessage(data: any): string {
const messages = {
'incident_requires_authority': 'Incidente que requiere notificación a STPS',
'critical_inspection_findings': 'Hallazgos críticos en inspección',
'unsafe_condition': 'Condición insegura detectada'
};
return messages[data.type] || 'Alerta de seguridad';
}
private buildSMSAlert(data: any): string {
return `ALERTA CRÍTICA: ${data.type} - Severidad: ${data.severity}. Revisar sistema inmediatamente.`;
}
}
```
### 7.2. Scheduled Jobs para Alertas
```typescript
// shared/alerts/alert-jobs.service.ts
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class AlertJobsService {
constructor(
private alertsService: AlertsService,
private eppService: EPPService,
private certificationsService: TrainingCertificationsService,
private actionsService: CorrectiveActionsService
) {}
/**
* Diario: Verificar EPP bajo en stock
*/
@Cron(CronExpression.EVERY_DAY_AT_8AM)
async checkLowStock(): Promise<void> {
const lowStock = await this.eppService.getLowStockItems();
for (const item of lowStock) {
await this.alertsService.alertLowStock(item);
}
}
/**
* Diario: Verificar EPP próximo a vencer
*/
@Cron(CronExpression.EVERY_DAY_AT_9AM)
async checkExpiringEPP(): Promise<void> {
const expiring = await this.eppService.getExpiringEPP(7); // 7 días
if (expiring.length > 0) {
await this.alertsService.alertExpiringEPP(expiring);
}
}
/**
* Diario: Verificar certificaciones próximas a vencer
*/
@Cron(CronExpression.EVERY_DAY_AT_10AM)
async checkExpiringCertifications(): Promise<void> {
const expiring = await this.certificationsService.getExpiringCertifications(30);
if (expiring.length > 0) {
await this.alertsService.alertExpiringCertifications(expiring);
}
}
/**
* Diario: Verificar acciones correctivas vencidas
*/
@Cron(CronExpression.EVERY_DAY_AT_11AM)
async checkOverdueActions(): Promise<void> {
const overdue = await this.actionsService.getOverdueActions();
for (const action of overdue) {
await this.alertsService.alertOverdueAction(action);
}
}
}
```
## 8. Reportes para Cumplimiento NOM-STPS
### 8.1. Reports Service
```typescript
// shared/reporting/safety-reports.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm';
@Injectable()
export class SafetyReportsService {
constructor(
@InjectRepository(Incident)
private incidentRepository: Repository<Incident>,
@InjectRepository(SafetyInspection)
private inspectionRepository: Repository<SafetyInspection>,
@InjectRepository(TrainingCertification)
private certificationRepository: Repository<TrainingCertification>,
private pdfService: PDFService
) {}
/**
* NOM-019-STPS-2011: Reporte de accidentes y enfermedades de trabajo
*/
async generateNOM019Report(projectId: string, startDate: Date, endDate: Date): Promise<Buffer> {
const incidents = await this.incidentRepository.find({
where: {
projectId,
incidentDate: Between(startDate, endDate),
type: 'accident'
},
relations: ['investigations', 'investigations.correctiveActions']
});
const data = {
period: { startDate, endDate },
totalIncidents: incidents.length,
byType: this.groupIncidentsByType(incidents),
bySeverity: this.groupIncidentsBySeverity(incidents),
workDaysLost: incidents.reduce((sum, i) => sum + i.workDaysLost, 0),
incidentRate: this.calculateIncidentRate(incidents, projectId),
severityRate: this.calculateSeverityRate(incidents, projectId),
details: incidents.map(i => ({
incidentNumber: i.incidentNumber,
date: i.incidentDate,
type: i.type,
severity: i.severity,
location: i.location,
description: i.description,
workDaysLost: i.workDaysLost,
investigated: i.investigations.length > 0,
correctiveActions: i.investigations[0]?.correctiveActions.length || 0
}))
};
return this.pdfService.generate('nom-019-report', data);
}
/**
* NOM-030-STPS-2009: Servicios preventivos de seguridad y salud
*/
async generateNOM030Report(projectId: string, year: number): Promise<Buffer> {
const startDate = new Date(year, 0, 1);
const endDate = new Date(year, 11, 31);
const [inspections, trainings, incidents] = await Promise.all([
this.inspectionRepository.count({
where: { projectId, inspectionDate: Between(startDate, endDate) }
}),
this.certificationRepository
.createQueryBuilder('cert')
.where('cert.issueDate BETWEEN :startDate AND :endDate', { startDate, endDate })
.getCount(),
this.incidentRepository.count({
where: { projectId, incidentDate: Between(startDate, endDate) }
})
]);
const data = {
year,
preventiveActivities: {
inspections,
trainings,
incidents
},
complianceStatus: this.calculateComplianceStatus(projectId),
recommendations: await this.generateRecommendations(projectId)
};
return this.pdfService.generate('nom-030-report', data);
}
/**
* Reporte de capacitaciones impartidas
*/
async generateTrainingReport(projectId: string, startDate: Date, endDate: Date): Promise<Buffer> {
const trainings = await this.trainingRepository.find({
where: {
projectId,
trainingDate: Between(startDate, endDate),
status: 'completed'
},
relations: ['course', 'attendances']
});
const data = {
period: { startDate, endDate },
totalTrainings: trainings.length,
totalAttendees: trainings.reduce((sum, t) => sum + t.attendances.length, 0),
byCategory: this.groupTrainingsByCategory(trainings),
certificationIssued: trainings.reduce((sum, t) =>
sum + t.attendances.filter(a => a.passed).length, 0
),
details: trainings.map(t => ({
trainingNumber: t.trainingNumber,
course: t.course.name,
date: t.trainingDate,
attendees: t.attendances.length,
approved: t.attendances.filter(a => a.passed).length,
averageScore: this.calculateAverageScore(t.attendances)
}))
};
return this.pdfService.generate('training-report', data);
}
/**
* Reporte de inspecciones de seguridad
*/
async generateInspectionReport(projectId: string, startDate: Date, endDate: Date): Promise<Buffer> {
const inspections = await this.inspectionRepository.find({
where: {
projectId,
inspectionDate: Between(startDate, endDate),
status: 'completed'
},
relations: ['template', 'responses']
});
const data = {
period: { startDate, endDate },
totalInspections: inspections.length,
averageScore: this.calculateAverageScore(inspections.map(i => ({ score: i.overallScore }))),
findings: {
critical: inspections.reduce((sum, i) => sum + i.criticalFindings, 0),
major: inspections.reduce((sum, i) => sum + i.majorFindings, 0),
minor: inspections.reduce((sum, i) => sum + i.minorFindings, 0)
},
byType: this.groupInspectionsByType(inspections),
details: inspections.map(i => ({
inspectionNumber: i.inspectionNumber,
date: i.inspectionDate,
type: i.template.inspectionType,
score: i.overallScore,
findings: i.criticalFindings + i.majorFindings + i.minorFindings
}))
};
return this.pdfService.generate('inspection-report', data);
}
/**
* Dashboard ejecutivo de seguridad
*/
async getExecutiveDashboard(projectId: string, month: number, year: number): Promise<any> {
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
const [incidents, inspections, trainings] = await Promise.all([
this.incidentRepository.count({
where: { projectId, incidentDate: Between(startDate, endDate) }
}),
this.inspectionRepository.count({
where: { projectId, inspectionDate: Between(startDate, endDate), status: 'completed' }
}),
this.trainingRepository.count({
where: { projectId, trainingDate: Between(startDate, endDate), status: 'completed' }
})
]);
return {
period: { month, year },
kpis: {
incidentRate: await this.calculateIncidentRate(projectId, startDate, endDate),
inspectionScore: await this.getAverageInspectionScore(projectId, startDate, endDate),
trainingCompliance: await this.getTrainingCompliance(projectId),
zeroIncidentDays: await this.getZeroIncidentDays(projectId, startDate, endDate)
},
summary: {
incidents,
inspections,
trainings
},
trends: await this.getTrends(projectId, year)
};
}
// Métodos auxiliares
private groupIncidentsByType(incidents: Incident[]) {
return incidents.reduce((acc, i) => {
acc[i.type] = (acc[i.type] || 0) + 1;
return acc;
}, {});
}
private groupIncidentsBySeverity(incidents: Incident[]) {
return incidents.reduce((acc, i) => {
acc[i.severity] = (acc[i.severity] || 0) + 1;
return acc;
}, {});
}
private async calculateIncidentRate(projectId: string, startDate: Date, endDate: Date): Promise<number> {
// Tasa de incidentes = (Número de incidentes / Horas trabajadas) * 200,000
// 200,000 = 100 trabajadores trabajando 40 horas/semana durante 50 semanas
const incidents = await this.incidentRepository.count({
where: { projectId, incidentDate: Between(startDate, endDate) }
});
// Obtener horas trabajadas del proyecto (desde módulo de asistencia)
const hoursWorked = await this.getProjectHoursWorked(projectId, startDate, endDate);
return hoursWorked > 0 ? (incidents / hoursWorked) * 200000 : 0;
}
private async calculateSeverityRate(projectId: string, startDate: Date, endDate: Date): Promise<number> {
const incidents = await this.incidentRepository.find({
where: { projectId, incidentDate: Between(startDate, endDate) }
});
const totalDaysLost = incidents.reduce((sum, i) => sum + i.workDaysLost, 0);
const hoursWorked = await this.getProjectHoursWorked(projectId, startDate, endDate);
return hoursWorked > 0 ? (totalDaysLost / hoursWorked) * 200000 : 0;
}
private async getProjectHoursWorked(projectId: string, startDate: Date, endDate: Date): Promise<number> {
// Integración con módulo de asistencia/nómina
// Por ahora retornar valor estimado
return 10000; // Placeholder
}
private calculateAverageScore(items: any[]): number {
if (items.length === 0) return 0;
const sum = items.reduce((acc, item) => acc + (item.score || 0), 0);
return sum / items.length;
}
}
```
## 9. Configuración y Buenas Prácticas
### 9.1. Validación Global
```typescript
// main.ts
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true
}
})
);
```
### 9.2. Exception Filters
```typescript
// filters/safety-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
@Catch()
export class SafetyExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: 500;
// Log crítico para errores en módulo de seguridad
if (request.path.includes('/safety/')) {
console.error('SAFETY MODULE ERROR:', {
path: request.path,
method: request.method,
error: exception
});
}
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception instanceof HttpException ? exception.message : 'Internal server error'
});
}
}
```
### 9.3. Interceptor de Auditoría
```typescript
// interceptors/audit.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class AuditInterceptor implements NestInterceptor {
constructor(private auditService: AuditService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, user, body } = request;
// Auditar operaciones críticas de seguridad
const criticalOperations = [
'/safety/incidents',
'/safety/inspections',
'/safety/epp/assignments'
];
const isCritical = criticalOperations.some(op => url.includes(op));
return next.handle().pipe(
tap((data) => {
if (isCritical) {
this.auditService.log({
module: 'safety',
action: method,
endpoint: url,
userId: user?.id,
data: body,
timestamp: new Date()
});
}
})
);
}
}
```
## 10. Pruebas Unitarias
### 10.1. EPP Service Test
```typescript
// epp.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { EPPService } from './epp.service';
import { EPPItem, EPPInventory } from './entities';
describe('EPPService', () => {
let service: EPPService;
let eppItemRepository: any;
let inventoryRepository: any;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EPPService,
{
provide: getRepositoryToken(EPPItem),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn()
}
},
{
provide: getRepositoryToken(EPPInventory),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
decrement: jest.fn()
}
}
]
}).compile();
service = module.get<EPPService>(EPPService);
eppItemRepository = module.get(getRepositoryToken(EPPItem));
inventoryRepository = module.get(getRepositoryToken(EPPInventory));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('checkAvailability', () => {
it('should return true if sufficient inventory', async () => {
inventoryRepository.findOne.mockResolvedValue({
quantity: 100,
reserved: 20
});
const result = await service.checkAvailability('epp-id', 'project-id', 50);
expect(result).toBe(true);
});
it('should return false if insufficient inventory', async () => {
inventoryRepository.findOne.mockResolvedValue({
quantity: 100,
reserved: 90
});
const result = await service.checkAvailability('epp-id', 'project-id', 50);
expect(result).toBe(false);
});
});
});
```
## 11. Documentación API
### 11.1. Swagger Configuration
```typescript
// main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
const config = new DocumentBuilder()
.setTitle('API de Seguridad Industrial')
.setDescription('Módulo de gestión de seguridad para construcción')
.setVersion('1.0')
.addTag('EPP', 'Equipos de Protección Personal')
.addTag('Inspecciones', 'Inspecciones de Seguridad')
.addTag('Incidentes', 'Gestión de Incidentes y Accidentes')
.addTag('Capacitaciones', 'Capacitaciones de Seguridad')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs/safety', app, document);
```
## 12. Notas de Implementación
### 12.1. Prioridades de Desarrollo
1. **Fase 1 - Core (Sprint 1-2)**
- Entities y DTOs básicos
- EPP Module (asignación y devolución)
- Incidents Module (reporte básico)
- Endpoints para apps móviles MOB-003 y MOB-004
2. **Fase 2 - Workflows (Sprint 3-4)**
- Workflow completo de investigación de incidentes
- Safety Inspections Module
- Sistema de alertas básico
3. **Fase 3 - Avanzado (Sprint 5-6)**
- Safety Trainings Module
- Reportes NOM-STPS
- Dashboard ejecutivo
### 12.2. Integraciones Requeridas
- **Módulo de RH**: Obtener datos de trabajadores
- **Módulo de Proyectos**: Validar proyectos y áreas
- **Módulo de Asistencia**: Calcular horas trabajadas para métricas
- **Storage Service**: Almacenar fotos, firmas digitales, PDFs
- **Notification Service**: Push notifications para apps móviles
### 12.3. Consideraciones de Seguridad
- Encriptar firmas digitales
- Logs de auditoría para todas las operaciones
- Backup diario de incidentes y evidencias
- Restricción de eliminación de registros (soft delete)
---
**Documento:** ET-SEG-001-backend.md
**Versión:** 1.0.0
**Última Actualización:** 2025-12-06