erp-construccion/docs/05-backend-specs/README.md

383 lines
8.6 KiB
Markdown

# 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<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
```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>> {
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
```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<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
- [Domain Models](../04-modelado/domain-models/)
- [DDL Specifications](../04-modelado/database-design/schemas/)
- [Epicas](../08-epicas/)
---
*Ultima actualizacion: 2025-12-05*