383 lines
8.6 KiB
Markdown
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*
|