# Backend Specification: Construction Module **Version:** 1.0.0 **Fecha:** 2025-12-05 **Modulos:** MAI-002 (Proyectos), MAI-003 (Presupuestos), MAI-005 (Control de Obra) --- ## Resumen | Metrica | Valor | |---------|-------| | Controllers | 5 | | Services | 6 | | Entities | 15 | | Endpoints | 45+ | | Tests Requeridos | 60+ | --- ## Estructura del Modulo ``` modules/construction/ +-- construction.module.ts +-- controllers/ | +-- project.controller.ts | +-- development.controller.ts | +-- budget.controller.ts | +-- progress.controller.ts | +-- estimation.controller.ts +-- services/ | +-- project.service.ts | +-- development.service.ts | +-- budget.service.ts | +-- apu.service.ts | +-- progress.service.ts | +-- estimation.service.ts +-- entities/ | +-- project.entity.ts | +-- development.entity.ts | +-- section.entity.ts | +-- housing-unit.entity.ts | +-- prototype.entity.ts | +-- budget.entity.ts | +-- budget-partida.entity.ts | +-- budget-concept.entity.ts | +-- apu-item.entity.ts | +-- schedule.entity.ts | +-- schedule-item.entity.ts | +-- progress-record.entity.ts | +-- logbook-entry.entity.ts | +-- estimation.entity.ts | +-- estimation-line.entity.ts +-- dto/ | +-- project/ | +-- budget/ | +-- progress/ +-- repositories/ | +-- project.repository.ts | +-- budget.repository.ts +-- events/ +-- project-created.event.ts +-- progress-recorded.event.ts ``` --- ## Controllers ### 1. ProjectController ```typescript @Controller('api/v1/projects') @ApiTags('projects') export class ProjectController { // GET /api/v1/projects @Get() async findAll(@Query() query: ProjectQueryDto): Promise>; // GET /api/v1/projects/:id @Get(':id') async findOne(@Param('id') id: UUID): Promise; // POST /api/v1/projects @Post() async create(@Body() dto: CreateProjectDto): Promise; // PUT /api/v1/projects/:id @Put(':id') async update(@Param('id') id: UUID, @Body() dto: UpdateProjectDto): Promise; // DELETE /api/v1/projects/:id @Delete(':id') async remove(@Param('id') id: UUID): Promise; // GET /api/v1/projects/:id/summary @Get(':id/summary') async getSummary(@Param('id') id: UUID): Promise; // GET /api/v1/projects/:id/developments @Get(':id/developments') async getDevelopments(@Param('id') id: UUID): Promise; // GET /api/v1/projects/:id/budgets @Get(':id/budgets') async getBudgets(@Param('id') id: UUID): Promise; } ``` ### 2. BudgetController ```typescript @Controller('api/v1/budgets') @ApiTags('budgets') export class BudgetController { // GET /api/v1/budgets @Get() async findAll(@Query() query: BudgetQueryDto): Promise>; // GET /api/v1/budgets/:id @Get(':id') async findOne(@Param('id') id: UUID): Promise; // POST /api/v1/budgets @Post() async create(@Body() dto: CreateBudgetDto): Promise; // PUT /api/v1/budgets/:id @Put(':id') async update(@Param('id') id: UUID, @Body() dto: UpdateBudgetDto): Promise; // POST /api/v1/budgets/:id/approve @Post(':id/approve') async approve(@Param('id') id: UUID): Promise; // GET /api/v1/budgets/:id/partidas @Get(':id/partidas') async getPartidas(@Param('id') id: UUID): Promise; // POST /api/v1/budgets/:id/partidas @Post(':id/partidas') async addPartida(@Param('id') id: UUID, @Body() dto: CreatePartidaDto): Promise; // GET /api/v1/budgets/:id/concepts @Get(':id/concepts') async getConcepts(@Param('id') id: UUID): Promise; // POST /api/v1/budgets/:id/concepts @Post(':id/concepts') async addConcept(@Body() dto: CreateConceptDto): Promise; // GET /api/v1/budgets/:id/explosion @Get(':id/explosion') async getExplosion(@Param('id') id: UUID): Promise; // POST /api/v1/budgets/:id/explode @Post(':id/explode') async explodeMaterials(@Param('id') id: UUID): Promise<{ count: number }>; // GET /api/v1/budgets/:id/comparison @Get(':id/comparison') async getComparison(@Param('id') id: UUID): Promise; // POST /api/v1/budgets/:id/import @Post(':id/import') @UseInterceptors(FileInterceptor('file')) async importFromExcel(@UploadedFile() file: Express.Multer.File): Promise; } ``` ### 3. ProgressController ```typescript @Controller('api/v1/progress') @ApiTags('progress') export class ProgressController { // GET /api/v1/progress @Get() async findAll(@Query() query: ProgressQueryDto): Promise>; // POST /api/v1/progress @Post() async create(@Body() dto: CreateProgressDto): Promise; // POST /api/v1/progress/:id/approve @Post(':id/approve') async approve(@Param('id') id: UUID): Promise; // GET /api/v1/progress/by-project/:projectId @Get('by-project/:projectId') async getByProject(@Param('projectId') projectId: UUID): Promise; // GET /api/v1/progress/by-unit/:unitId @Get('by-unit/:unitId') async getByUnit(@Param('unitId') unitId: UUID): Promise; // GET /api/v1/progress/curve-s/:projectId @Get('curve-s/:projectId') async getCurveS(@Param('projectId') projectId: UUID): Promise; // POST /api/v1/progress/:id/photos @Post(':id/photos') @UseInterceptors(FilesInterceptor('photos')) async uploadPhotos( @Param('id') id: UUID, @UploadedFiles() files: Express.Multer.File[] ): Promise; } ``` ### 4. EstimationController ```typescript @Controller('api/v1/estimations') @ApiTags('estimations') export class EstimationController { // GET /api/v1/estimations @Get() async findAll(@Query() query: EstimationQueryDto): Promise>; // GET /api/v1/estimations/:id @Get(':id') async findOne(@Param('id') id: UUID): Promise; // POST /api/v1/estimations @Post() async create(@Body() dto: CreateEstimationDto): Promise; // PUT /api/v1/estimations/:id @Put(':id') async update(@Param('id') id: UUID, @Body() dto: UpdateEstimationDto): Promise; // POST /api/v1/estimations/:id/submit @Post(':id/submit') async submit(@Param('id') id: UUID): Promise; // POST /api/v1/estimations/:id/approve @Post(':id/approve') async approve(@Param('id') id: UUID): Promise; // POST /api/v1/estimations/:id/reject @Post(':id/reject') async reject(@Param('id') id: UUID, @Body() dto: RejectEstimationDto): Promise; // GET /api/v1/estimations/:id/lines @Get(':id/lines') async getLines(@Param('id') id: UUID): Promise; // POST /api/v1/estimations/:id/lines @Post(':id/lines') async addLine(@Param('id') id: UUID, @Body() dto: CreateEstimationLineDto): Promise; // GET /api/v1/estimations/:id/export/pdf @Get(':id/export/pdf') async exportPdf(@Param('id') id: UUID): Promise; } ``` --- ## Services ### ProjectService ```typescript @Injectable() export class ProjectService { constructor( @InjectRepository(Project) private readonly projectRepo: Repository, private readonly eventEmitter: EventEmitter2 ) {} async findAll(tenantId: UUID, query: ProjectQueryDto): Promise>; async findOne(tenantId: UUID, id: UUID): Promise; async create(tenantId: UUID, userId: UUID, dto: CreateProjectDto): Promise; async update(tenantId: UUID, id: UUID, dto: UpdateProjectDto): Promise; async remove(tenantId: UUID, id: UUID): Promise; async getSummary(tenantId: UUID, id: UUID): Promise; async calculateProgress(projectId: UUID): Promise; } ``` ### BudgetService ```typescript @Injectable() export class BudgetService { constructor( @InjectRepository(Budget) private readonly budgetRepo: Repository, @InjectRepository(BudgetPartida) private readonly partidaRepo: Repository, @InjectRepository(BudgetConcept) private readonly conceptRepo: Repository, private readonly apuService: ApuService ) {} async findAll(tenantId: UUID, query: BudgetQueryDto): Promise>; async findOne(tenantId: UUID, id: UUID): Promise; async create(tenantId: UUID, userId: UUID, dto: CreateBudgetDto): Promise; async approve(tenantId: UUID, id: UUID, userId: UUID): Promise; async calculateTotals(budgetId: UUID): Promise; async explodeMaterials(budgetId: UUID): Promise; async importFromExcel(file: Express.Multer.File, budgetId: UUID): Promise; async getComparison(tenantId: UUID, budgetId: UUID): Promise; } ``` ### ApuService ```typescript @Injectable() export class ApuService { constructor( @InjectRepository(APUItem) private readonly apuRepo: Repository ) {} async getApuItems(conceptId: UUID): Promise; async addApuItem(conceptId: UUID, dto: CreateApuItemDto): Promise; async updateApuItem(id: UUID, dto: UpdateApuItemDto): Promise; async removeApuItem(id: UUID): Promise; async calculateConceptPrice(conceptId: UUID): Promise; } ``` ### ProgressService ```typescript @Injectable() export class ProgressService { constructor( @InjectRepository(ProgressRecord) private readonly progressRepo: Repository, private readonly housingUnitService: HousingUnitService, private readonly eventEmitter: EventEmitter2 ) {} async findAll(tenantId: UUID, query: ProgressQueryDto): Promise>; async create(tenantId: UUID, userId: UUID, dto: CreateProgressDto): Promise; async approve(tenantId: UUID, id: UUID, userId: UUID): Promise; async getByProject(projectId: UUID): Promise; async getByUnit(unitId: UUID): Promise; async getCurveS(projectId: UUID): Promise; async uploadPhotos(progressId: UUID, files: Express.Multer.File[]): Promise; } ``` ### EstimationService ```typescript @Injectable() export class EstimationService { constructor( @InjectRepository(Estimation) private readonly estimationRepo: Repository, @InjectRepository(EstimationLine) private readonly lineRepo: Repository, private readonly financeIntegration: FinanceIntegrationService, private readonly eventEmitter: EventEmitter2 ) {} async findAll(tenantId: UUID, query: EstimationQueryDto): Promise>; async findOne(tenantId: UUID, id: UUID): Promise; async create(tenantId: UUID, userId: UUID, dto: CreateEstimationDto): Promise; async submit(tenantId: UUID, id: UUID, userId: UUID): Promise; async approve(tenantId: UUID, id: UUID, userId: UUID): Promise; async reject(tenantId: UUID, id: UUID, userId: UUID, reason: string): Promise; async calculateTotals(estimationId: UUID): Promise; async generateAR(estimationId: UUID): Promise; // Create Account Receivable async exportPdf(tenantId: UUID, id: UUID): Promise; } ``` --- ## DTOs ### Project DTOs ```typescript export class CreateProjectDto { @IsString() @MaxLength(20) code: string; @IsString() @MaxLength(200) name: string; @IsOptional() @IsString() description?: string; @IsOptional() @IsString() address?: string; @IsOptional() @IsString() city?: string; @IsOptional() @IsString() state?: string; @IsOptional() @IsDateString() startDate?: string; @IsOptional() @IsDateString() endDate?: string; @IsOptional() @IsUUID() projectManagerId?: string; @IsOptional() @IsNumber() totalBudget?: number; } export class ProjectDto { id: string; code: string; name: string; description?: string; address?: string; city?: string; state?: string; status: ProjectStatus; progressPercentage: number; startDate?: string; endDate?: string; totalBudget?: number; totalSpent: number; createdAt: string; } export class ProjectSummaryDto { id: string; code: string; name: string; status: ProjectStatus; progressPercentage: number; totalDevelopments: number; totalUnits: number; completedUnits: number; totalBudget: number; totalSpent: number; budgetVariance: number; budgetVariancePercentage: number; daysRemaining: number; isOnSchedule: boolean; } ``` ### Budget DTOs ```typescript export class CreateBudgetDto { @IsString() @MaxLength(20) code: string; @IsString() @MaxLength(200) name: string; @IsOptional() @IsUUID() projectId?: string; @IsOptional() @IsUUID() prototypeId?: string; @IsOptional() @IsBoolean() isBase?: boolean; @IsOptional() @IsNumber() indirectPercentage?: number; @IsOptional() @IsNumber() profitPercentage?: number; } export class CreatePartidaDto { @IsString() @MaxLength(20) code: string; @IsString() @MaxLength(200) name: string; @IsOptional() @IsUUID() parentId?: string; @IsOptional() @IsNumber() sortOrder?: number; } export class CreateConceptDto { @IsUUID() partidaId: string; @IsString() @MaxLength(30) code: string; @IsString() @MaxLength(300) name: string; @IsString() @MaxLength(10) unitCode: string; @IsNumber() quantity: number; @IsNumber() unitPrice: number; @IsOptional() @IsEnum(ConceptType) conceptType?: ConceptType; } ``` ### Progress DTOs ```typescript export class CreateProgressDto { @IsUUID() projectId: string; @IsOptional() @IsUUID() housingUnitId?: string; @IsOptional() @IsUUID() conceptId?: string; @IsDateString() recordDate: string; @IsEnum(ProgressType) progressType: ProgressType; @IsNumber() @Min(0) @Max(100) currentProgress: number; @IsOptional() @IsNumber() quantityExecuted?: number; @IsOptional() @IsString() notes?: string; } export class CurveSDto { projectId: string; periods: CurveSPeriodDto[]; summary: { plannedProgress: number; actualProgress: number; variance: number; status: 'ahead' | 'on_track' | 'behind'; }; } export class CurveSPeriodDto { period: string; plannedAccumulated: number; actualAccumulated: number; variance: number; } ``` --- ## Events ### ProjectCreatedEvent ```typescript export class ProjectCreatedEvent { constructor( public readonly projectId: string, public readonly code: string, public readonly name: string, public readonly tenantId: string, public readonly createdBy: string, public readonly timestamp: Date = new Date() ) {} } ``` ### ProgressRecordedEvent ```typescript export class ProgressRecordedEvent { constructor( public readonly progressRecordId: string, public readonly projectId: string, public readonly housingUnitId: string | null, public readonly progressIncrement: number, public readonly recordedBy: string, public readonly timestamp: Date = new Date() ) {} } ``` ### EstimationApprovedEvent ```typescript export class EstimationApprovedEvent { constructor( public readonly estimationId: string, public readonly projectId: string, public readonly netAmount: number, public readonly approvedBy: string, public readonly timestamp: Date = new Date() ) {} } ``` --- ## Event Handlers ```typescript @Injectable() export class ConstructionEventHandlers { constructor( private readonly progressService: ProgressService, private readonly financeService: FinanceIntegrationService ) {} @OnEvent('progress.recorded') async handleProgressRecorded(event: ProgressRecordedEvent) { // Update housing unit progress if (event.housingUnitId) { await this.progressService.updateUnitProgress(event.housingUnitId); } // Update project progress await this.progressService.updateProjectProgress(event.projectId); } @OnEvent('estimation.approved') async handleEstimationApproved(event: EstimationApprovedEvent) { // Create Account Receivable in Finance module await this.financeService.createARFromEstimation(event.estimationId); } } ``` --- ## Tests ### Unit Tests Required - ProjectService: 10 tests - BudgetService: 12 tests - ApuService: 6 tests - ProgressService: 8 tests - EstimationService: 10 tests ### Integration Tests Required - Project CRUD: 5 tests - Budget with partidas/concepts: 8 tests - Progress recording workflow: 5 tests - Estimation approval workflow: 6 tests --- ## Referencias - [DDL-SPEC-construction.md](../../04-modelado/database-design/schemas/DDL-SPEC-construction.md) - [PROJECT-CONTEXT.md](../../04-modelado/domain-models/PROJECT-CONTEXT.md) - [EPIC-MAI-002](../../08-epicas/EPIC-MAI-002-proyectos.md) - [EPIC-MAI-003](../../08-epicas/EPIC-MAI-003-presupuestos.md) - [EPIC-MAI-005](../../08-epicas/EPIC-MAI-005-control-obra.md) --- *Ultima actualizacion: 2025-12-05*