| .. | ||
| modules | ||
| README.md | ||
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
@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<PaginatedResponse<ProjectDto>> {
return this.projectService.findAll(tenantId, query);
}
@Get(':id')
@ApiOperation({ summary: 'Get project by ID' })
async findOne(
@Param('id') id: UUID,
@CurrentTenant() tenantId: UUID
): Promise<ProjectDto> {
return this.projectService.findOne(tenantId, id);
}
@Post()
@ApiOperation({ summary: 'Create new project' })
async create(
@Body() dto: CreateProjectDto,
@CurrentTenant() tenantId: UUID,
@CurrentUser() userId: UUID
): Promise<ProjectDto> {
return this.projectService.create(tenantId, userId, dto);
}
}
2. Estructura de Servicio
@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>> {
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<ProjectDto> {
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
@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
// 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 | 25+ | 15 | Documentado |
| compliance | SPEC-compliance.md | 30+ | 10 | Documentado |
| finance | SPEC-finance.md | 50+ | 15 | Documentado |
| assets | SPEC-assets.md | 45+ | 12 | Documentado |
| documents | SPEC-documents.md | 40+ | 10 | Documentado |
Resumen de Endpoints: 190+ endpoints documentados
Seguridad
Guards
// 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
// 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
describe('ProjectService', () => {
let service: ProjectService;
let repository: MockType<Repository<Project>>;
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
Ultima actualizacion: 2025-12-05