18 KiB
18 KiB
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
@Controller('api/v1/projects')
@ApiTags('projects')
export class ProjectController {
// GET /api/v1/projects
@Get()
async findAll(@Query() query: ProjectQueryDto): Promise<PaginatedResponse<ProjectDto>>;
// GET /api/v1/projects/:id
@Get(':id')
async findOne(@Param('id') id: UUID): Promise<ProjectDto>;
// POST /api/v1/projects
@Post()
async create(@Body() dto: CreateProjectDto): Promise<ProjectDto>;
// PUT /api/v1/projects/:id
@Put(':id')
async update(@Param('id') id: UUID, @Body() dto: UpdateProjectDto): Promise<ProjectDto>;
// DELETE /api/v1/projects/:id
@Delete(':id')
async remove(@Param('id') id: UUID): Promise<void>;
// GET /api/v1/projects/:id/summary
@Get(':id/summary')
async getSummary(@Param('id') id: UUID): Promise<ProjectSummaryDto>;
// GET /api/v1/projects/:id/developments
@Get(':id/developments')
async getDevelopments(@Param('id') id: UUID): Promise<DevelopmentDto[]>;
// GET /api/v1/projects/:id/budgets
@Get(':id/budgets')
async getBudgets(@Param('id') id: UUID): Promise<BudgetDto[]>;
}
2. BudgetController
@Controller('api/v1/budgets')
@ApiTags('budgets')
export class BudgetController {
// GET /api/v1/budgets
@Get()
async findAll(@Query() query: BudgetQueryDto): Promise<PaginatedResponse<BudgetDto>>;
// GET /api/v1/budgets/:id
@Get(':id')
async findOne(@Param('id') id: UUID): Promise<BudgetDetailDto>;
// POST /api/v1/budgets
@Post()
async create(@Body() dto: CreateBudgetDto): Promise<BudgetDto>;
// PUT /api/v1/budgets/:id
@Put(':id')
async update(@Param('id') id: UUID, @Body() dto: UpdateBudgetDto): Promise<BudgetDto>;
// POST /api/v1/budgets/:id/approve
@Post(':id/approve')
async approve(@Param('id') id: UUID): Promise<BudgetDto>;
// GET /api/v1/budgets/:id/partidas
@Get(':id/partidas')
async getPartidas(@Param('id') id: UUID): Promise<PartidaTreeDto[]>;
// POST /api/v1/budgets/:id/partidas
@Post(':id/partidas')
async addPartida(@Param('id') id: UUID, @Body() dto: CreatePartidaDto): Promise<PartidaDto>;
// GET /api/v1/budgets/:id/concepts
@Get(':id/concepts')
async getConcepts(@Param('id') id: UUID): Promise<ConceptDto[]>;
// POST /api/v1/budgets/:id/concepts
@Post(':id/concepts')
async addConcept(@Body() dto: CreateConceptDto): Promise<ConceptDto>;
// GET /api/v1/budgets/:id/explosion
@Get(':id/explosion')
async getExplosion(@Param('id') id: UUID): Promise<MaterialExplosionDto[]>;
// 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<BudgetComparisonDto>;
// POST /api/v1/budgets/:id/import
@Post(':id/import')
@UseInterceptors(FileInterceptor('file'))
async importFromExcel(@UploadedFile() file: Express.Multer.File): Promise<ImportResultDto>;
}
3. ProgressController
@Controller('api/v1/progress')
@ApiTags('progress')
export class ProgressController {
// GET /api/v1/progress
@Get()
async findAll(@Query() query: ProgressQueryDto): Promise<PaginatedResponse<ProgressRecordDto>>;
// POST /api/v1/progress
@Post()
async create(@Body() dto: CreateProgressDto): Promise<ProgressRecordDto>;
// POST /api/v1/progress/:id/approve
@Post(':id/approve')
async approve(@Param('id') id: UUID): Promise<ProgressRecordDto>;
// GET /api/v1/progress/by-project/:projectId
@Get('by-project/:projectId')
async getByProject(@Param('projectId') projectId: UUID): Promise<ProgressRecordDto[]>;
// GET /api/v1/progress/by-unit/:unitId
@Get('by-unit/:unitId')
async getByUnit(@Param('unitId') unitId: UUID): Promise<ProgressRecordDto[]>;
// GET /api/v1/progress/curve-s/:projectId
@Get('curve-s/:projectId')
async getCurveS(@Param('projectId') projectId: UUID): Promise<CurveSDto>;
// POST /api/v1/progress/:id/photos
@Post(':id/photos')
@UseInterceptors(FilesInterceptor('photos'))
async uploadPhotos(
@Param('id') id: UUID,
@UploadedFiles() files: Express.Multer.File[]
): Promise<PhotoDto[]>;
}
4. EstimationController
@Controller('api/v1/estimations')
@ApiTags('estimations')
export class EstimationController {
// GET /api/v1/estimations
@Get()
async findAll(@Query() query: EstimationQueryDto): Promise<PaginatedResponse<EstimationDto>>;
// GET /api/v1/estimations/:id
@Get(':id')
async findOne(@Param('id') id: UUID): Promise<EstimationDetailDto>;
// POST /api/v1/estimations
@Post()
async create(@Body() dto: CreateEstimationDto): Promise<EstimationDto>;
// PUT /api/v1/estimations/:id
@Put(':id')
async update(@Param('id') id: UUID, @Body() dto: UpdateEstimationDto): Promise<EstimationDto>;
// POST /api/v1/estimations/:id/submit
@Post(':id/submit')
async submit(@Param('id') id: UUID): Promise<EstimationDto>;
// POST /api/v1/estimations/:id/approve
@Post(':id/approve')
async approve(@Param('id') id: UUID): Promise<EstimationDto>;
// POST /api/v1/estimations/:id/reject
@Post(':id/reject')
async reject(@Param('id') id: UUID, @Body() dto: RejectEstimationDto): Promise<EstimationDto>;
// GET /api/v1/estimations/:id/lines
@Get(':id/lines')
async getLines(@Param('id') id: UUID): Promise<EstimationLineDto[]>;
// POST /api/v1/estimations/:id/lines
@Post(':id/lines')
async addLine(@Param('id') id: UUID, @Body() dto: CreateEstimationLineDto): Promise<EstimationLineDto>;
// GET /api/v1/estimations/:id/export/pdf
@Get(':id/export/pdf')
async exportPdf(@Param('id') id: UUID): Promise<StreamableFile>;
}
Services
ProjectService
@Injectable()
export class ProjectService {
constructor(
@InjectRepository(Project)
private readonly projectRepo: Repository<Project>,
private readonly eventEmitter: EventEmitter2
) {}
async findAll(tenantId: UUID, query: ProjectQueryDto): Promise<PaginatedResponse<ProjectDto>>;
async findOne(tenantId: UUID, id: UUID): Promise<ProjectDto>;
async create(tenantId: UUID, userId: UUID, dto: CreateProjectDto): Promise<ProjectDto>;
async update(tenantId: UUID, id: UUID, dto: UpdateProjectDto): Promise<ProjectDto>;
async remove(tenantId: UUID, id: UUID): Promise<void>;
async getSummary(tenantId: UUID, id: UUID): Promise<ProjectSummaryDto>;
async calculateProgress(projectId: UUID): Promise<number>;
}
BudgetService
@Injectable()
export class BudgetService {
constructor(
@InjectRepository(Budget)
private readonly budgetRepo: Repository<Budget>,
@InjectRepository(BudgetPartida)
private readonly partidaRepo: Repository<BudgetPartida>,
@InjectRepository(BudgetConcept)
private readonly conceptRepo: Repository<BudgetConcept>,
private readonly apuService: ApuService
) {}
async findAll(tenantId: UUID, query: BudgetQueryDto): Promise<PaginatedResponse<BudgetDto>>;
async findOne(tenantId: UUID, id: UUID): Promise<BudgetDetailDto>;
async create(tenantId: UUID, userId: UUID, dto: CreateBudgetDto): Promise<BudgetDto>;
async approve(tenantId: UUID, id: UUID, userId: UUID): Promise<BudgetDto>;
async calculateTotals(budgetId: UUID): Promise<void>;
async explodeMaterials(budgetId: UUID): Promise<number>;
async importFromExcel(file: Express.Multer.File, budgetId: UUID): Promise<ImportResultDto>;
async getComparison(tenantId: UUID, budgetId: UUID): Promise<BudgetComparisonDto>;
}
ApuService
@Injectable()
export class ApuService {
constructor(
@InjectRepository(APUItem)
private readonly apuRepo: Repository<APUItem>
) {}
async getApuItems(conceptId: UUID): Promise<APUItemDto[]>;
async addApuItem(conceptId: UUID, dto: CreateApuItemDto): Promise<APUItemDto>;
async updateApuItem(id: UUID, dto: UpdateApuItemDto): Promise<APUItemDto>;
async removeApuItem(id: UUID): Promise<void>;
async calculateConceptPrice(conceptId: UUID): Promise<number>;
}
ProgressService
@Injectable()
export class ProgressService {
constructor(
@InjectRepository(ProgressRecord)
private readonly progressRepo: Repository<ProgressRecord>,
private readonly housingUnitService: HousingUnitService,
private readonly eventEmitter: EventEmitter2
) {}
async findAll(tenantId: UUID, query: ProgressQueryDto): Promise<PaginatedResponse<ProgressRecordDto>>;
async create(tenantId: UUID, userId: UUID, dto: CreateProgressDto): Promise<ProgressRecordDto>;
async approve(tenantId: UUID, id: UUID, userId: UUID): Promise<ProgressRecordDto>;
async getByProject(projectId: UUID): Promise<ProgressRecordDto[]>;
async getByUnit(unitId: UUID): Promise<ProgressRecordDto[]>;
async getCurveS(projectId: UUID): Promise<CurveSDto>;
async uploadPhotos(progressId: UUID, files: Express.Multer.File[]): Promise<PhotoDto[]>;
}
EstimationService
@Injectable()
export class EstimationService {
constructor(
@InjectRepository(Estimation)
private readonly estimationRepo: Repository<Estimation>,
@InjectRepository(EstimationLine)
private readonly lineRepo: Repository<EstimationLine>,
private readonly financeIntegration: FinanceIntegrationService,
private readonly eventEmitter: EventEmitter2
) {}
async findAll(tenantId: UUID, query: EstimationQueryDto): Promise<PaginatedResponse<EstimationDto>>;
async findOne(tenantId: UUID, id: UUID): Promise<EstimationDetailDto>;
async create(tenantId: UUID, userId: UUID, dto: CreateEstimationDto): Promise<EstimationDto>;
async submit(tenantId: UUID, id: UUID, userId: UUID): Promise<EstimationDto>;
async approve(tenantId: UUID, id: UUID, userId: UUID): Promise<EstimationDto>;
async reject(tenantId: UUID, id: UUID, userId: UUID, reason: string): Promise<EstimationDto>;
async calculateTotals(estimationId: UUID): Promise<void>;
async generateAR(estimationId: UUID): Promise<void>; // Create Account Receivable
async exportPdf(tenantId: UUID, id: UUID): Promise<Buffer>;
}
DTOs
Project DTOs
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
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
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
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
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
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
@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
Ultima actualizacion: 2025-12-05