# Backend Specifications - ERP Construccion **Fecha:** 2025-12-05 **Version:** 1.0.0 **Stack:** Node.js 20+ / Express.js / TypeScript 5.3+ / TypeORM 0.3.17 --- ## Estructura del Directorio ``` 05-backend-specs/ +-- README.md (este archivo) +-- modules/ | +-- SPEC-construction.md [MAI-002, MAI-003, MAI-005] | +-- SPEC-compliance.md [MAI-011] | +-- SPEC-finance.md [MAE-014] | +-- SPEC-assets.md [MAE-015] | +-- SPEC-documents.md [MAE-016] +-- api/ | +-- API-construction.yaml [OpenAPI 3.0] | +-- API-compliance.yaml | +-- API-finance.yaml | +-- API-assets.yaml | +-- API-documents.yaml +-- services/ +-- SERVICE-patterns.md [Patrones de servicios] ``` --- ## Arquitectura Backend ### Estructura de Modulos ``` apps/backend/src/ +-- modules/ | +-- construction/ | | +-- construction.module.ts | | +-- controllers/ | | | +-- project.controller.ts | | | +-- budget.controller.ts | | | +-- progress.controller.ts | | +-- services/ | | | +-- project.service.ts | | | +-- budget.service.ts | | | +-- progress.service.ts | | +-- entities/ | | +-- dto/ | | +-- repositories/ | +-- compliance/ | +-- finance/ | +-- assets/ | +-- documents/ +-- shared/ | +-- guards/ | +-- interceptors/ | +-- decorators/ | +-- filters/ +-- config/ ``` --- ## Patrones de Desarrollo ### 1. Estructura de Controlador ```typescript @Controller('api/v1/projects') @UseGuards(AuthGuard, TenantGuard) @ApiTags('projects') export class ProjectController { constructor(private readonly projectService: ProjectService) {} @Get() @ApiOperation({ summary: 'List all projects' }) @ApiResponse({ status: 200, type: [ProjectDto] }) async findAll( @Query() query: ProjectQueryDto, @CurrentTenant() tenantId: UUID ): Promise> { return this.projectService.findAll(tenantId, query); } @Get(':id') @ApiOperation({ summary: 'Get project by ID' }) async findOne( @Param('id') id: UUID, @CurrentTenant() tenantId: UUID ): Promise { return this.projectService.findOne(tenantId, id); } @Post() @ApiOperation({ summary: 'Create new project' }) async create( @Body() dto: CreateProjectDto, @CurrentTenant() tenantId: UUID, @CurrentUser() userId: UUID ): Promise { return this.projectService.create(tenantId, userId, dto); } } ``` ### 2. Estructura de Servicio ```typescript @Injectable() export class ProjectService { constructor( @InjectRepository(Project) private readonly projectRepo: Repository, private readonly eventEmitter: EventEmitter2 ) {} async findAll(tenantId: UUID, query: ProjectQueryDto): Promise> { const qb = this.projectRepo.createQueryBuilder('p') .where('p.tenant_id = :tenantId', { tenantId }) .andWhere('p.deleted_at IS NULL'); if (query.status) { qb.andWhere('p.status = :status', { status: query.status }); } const [items, total] = await qb .skip(query.offset) .take(query.limit) .getManyAndCount(); return { items: items.map(toDto), total, ...query }; } async create(tenantId: UUID, userId: UUID, dto: CreateProjectDto): Promise { const project = this.projectRepo.create({ ...dto, tenantId, createdBy: userId }); await this.projectRepo.save(project); this.eventEmitter.emit('project.created', new ProjectCreatedEvent(project)); return toDto(project); } } ``` ### 3. Estructura de Entidad ```typescript @Entity('projects', { schema: 'construction' }) export class Project { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'tenant_id', type: 'uuid' }) tenantId: string; @Column({ length: 20 }) code: string; @Column({ length: 200 }) name: string; @Column({ type: 'text', nullable: true }) description: string; @Column({ type: 'enum', enum: ProjectStatus, default: ProjectStatus.PLANNING }) status: ProjectStatus; @Column({ name: 'progress_percentage', type: 'decimal', precision: 5, scale: 2, default: 0 }) progressPercentage: number; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @Column({ name: 'created_by', type: 'uuid', nullable: true }) createdBy: string; @UpdateDateColumn({ name: 'updated_at', nullable: true }) updatedAt: Date; @Column({ name: 'deleted_at', nullable: true }) deletedAt: Date; // Relations @OneToMany(() => Development, d => d.project) developments: Development[]; @OneToMany(() => Budget, b => b.project) budgets: Budget[]; } ``` ### 4. DTOs ```typescript // Create DTO export class CreateProjectDto { @IsString() @MaxLength(20) code: string; @IsString() @MaxLength(200) name: string; @IsOptional() @IsString() description?: string; @IsOptional() @IsDateString() startDate?: string; @IsOptional() @IsDateString() endDate?: string; } // Response DTO export class ProjectDto { id: string; code: string; name: string; description?: string; status: ProjectStatus; progressPercentage: number; startDate?: string; endDate?: string; createdAt: string; } // Query DTO export class ProjectQueryDto extends PaginationDto { @IsOptional() @IsEnum(ProjectStatus) status?: ProjectStatus; @IsOptional() @IsString() search?: string; } ``` --- ## Modulos Implementados | Modulo | Spec | Endpoints | Entidades | Estado | |--------|------|-----------|-----------|--------| | construction | [SPEC-construction.md](modules/SPEC-construction.md) | 25+ | 15 | Documentado | | compliance | [SPEC-compliance.md](modules/SPEC-compliance.md) | 30+ | 10 | Documentado | | finance | [SPEC-finance.md](modules/SPEC-finance.md) | 50+ | 15 | Documentado | | assets | [SPEC-assets.md](modules/SPEC-assets.md) | 45+ | 12 | Documentado | | documents | [SPEC-documents.md](modules/SPEC-documents.md) | 40+ | 10 | Documentado | **Resumen de Endpoints:** 190+ endpoints documentados --- ## Seguridad ### Guards ```typescript // Tenant isolation guard @Injectable() export class TenantGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const tenantId = request.headers['x-tenant-id']; if (!tenantId) { throw new UnauthorizedException('Tenant ID required'); } request.tenantId = tenantId; return true; } } ``` ### RLS Context ```typescript // Middleware para establecer contexto RLS export class RLSMiddleware implements NestMiddleware { constructor(private readonly dataSource: DataSource) {} async use(req: Request, res: Response, next: NextFunction) { const tenantId = req.headers['x-tenant-id']; if (tenantId) { await this.dataSource.query( `SET LOCAL app.current_tenant_id = '${tenantId}'` ); } next(); } } ``` --- ## Testing ### Estructura de Tests ``` __tests__/ +-- unit/ | +-- services/ | +-- controllers/ +-- integration/ | +-- api/ | +-- repositories/ +-- e2e/ +-- projects.e2e-spec.ts ``` ### Ejemplo de Test ```typescript describe('ProjectService', () => { let service: ProjectService; let repository: MockType>; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ ProjectService, { provide: getRepositoryToken(Project), useFactory: repositoryMockFactory } ] }).compile(); service = module.get(ProjectService); repository = module.get(getRepositoryToken(Project)); }); describe('create', () => { it('should create a project', async () => { const dto: CreateProjectDto = { code: 'PRJ-001', name: 'Test Project' }; repository.create.mockReturnValue({ id: 'uuid', ...dto }); repository.save.mockResolvedValue({ id: 'uuid', ...dto }); const result = await service.create('tenant-id', 'user-id', dto); expect(result.code).toBe('PRJ-001'); expect(repository.save).toHaveBeenCalled(); }); }); }); ``` --- ## Referencias - [Domain Models](../04-modelado/domain-models/) - [DDL Specifications](../04-modelado/database-design/schemas/) - [Epicas](../08-epicas/) --- *Ultima actualizacion: 2025-12-05*