678 lines
18 KiB
Markdown
678 lines
18 KiB
Markdown
# 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<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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```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*
|