462 lines
12 KiB
Markdown
462 lines
12 KiB
Markdown
# Backend - ERP Construccion
|
|
|
|
API REST para sistema de administracion de obra e INFONAVIT.
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Stack** | Node.js 20 + Express 4 + TypeScript 5 + TypeORM 0.3 |
|
|
| **Version** | 1.0.0 |
|
|
| **Entidades** | 30 |
|
|
| **Services** | 8 |
|
|
| **Arquitectura** | Multi-tenant con RLS |
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# Instalar dependencias
|
|
npm install
|
|
|
|
# Configurar variables de entorno
|
|
cp ../.env.example .env
|
|
|
|
# Desarrollo con hot-reload
|
|
npm run dev
|
|
|
|
# El servidor estara en http://localhost:3000
|
|
```
|
|
|
|
---
|
|
|
|
## Estructura del Proyecto
|
|
|
|
```
|
|
src/
|
|
├── modules/
|
|
│ ├── auth/ # Autenticacion JWT
|
|
│ │ ├── dto/
|
|
│ │ │ └── auth.dto.ts # DTOs tipados
|
|
│ │ ├── middleware/
|
|
│ │ │ └── auth.middleware.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── auth.service.ts
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ ├── budgets/ # MAI-003 Presupuestos
|
|
│ │ ├── entities/
|
|
│ │ │ ├── concepto.entity.ts
|
|
│ │ │ ├── presupuesto.entity.ts
|
|
│ │ │ └── presupuesto-partida.entity.ts
|
|
│ │ ├── services/
|
|
│ │ │ ├── concepto.service.ts
|
|
│ │ │ └── presupuesto.service.ts
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ ├── progress/ # MAI-005 Control de Obra
|
|
│ │ ├── entities/
|
|
│ │ │ ├── avance-obra.entity.ts
|
|
│ │ │ ├── foto-avance.entity.ts
|
|
│ │ │ ├── bitacora-obra.entity.ts
|
|
│ │ │ ├── programa-obra.entity.ts
|
|
│ │ │ └── programa-actividad.entity.ts
|
|
│ │ ├── services/
|
|
│ │ │ ├── avance-obra.service.ts
|
|
│ │ │ └── bitacora-obra.service.ts
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ ├── estimates/ # MAI-008 Estimaciones
|
|
│ │ ├── entities/
|
|
│ │ │ ├── estimacion.entity.ts
|
|
│ │ │ ├── estimacion-concepto.entity.ts
|
|
│ │ │ ├── generador.entity.ts
|
|
│ │ │ ├── anticipo.entity.ts
|
|
│ │ │ ├── amortizacion.entity.ts
|
|
│ │ │ ├── retencion.entity.ts
|
|
│ │ │ ├── fondo-garantia.entity.ts
|
|
│ │ │ └── estimacion-workflow.entity.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── estimacion.service.ts
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ ├── construction/ # MAI-002 Proyectos
|
|
│ │ └── entities/
|
|
│ │ ├── proyecto.entity.ts
|
|
│ │ └── fraccionamiento.entity.ts
|
|
│ │
|
|
│ ├── hr/ # MAI-007 RRHH
|
|
│ │ └── entities/
|
|
│ │ ├── employee.entity.ts
|
|
│ │ ├── puesto.entity.ts
|
|
│ │ └── employee-fraccionamiento.entity.ts
|
|
│ │
|
|
│ ├── hse/ # MAA-017 Seguridad HSE
|
|
│ │ └── entities/
|
|
│ │ ├── incidente.entity.ts
|
|
│ │ ├── incidente-involucrado.entity.ts
|
|
│ │ ├── incidente-accion.entity.ts
|
|
│ │ └── capacitacion.entity.ts
|
|
│ │
|
|
│ └── core/ # Entidades base
|
|
│ └── entities/
|
|
│ ├── user.entity.ts
|
|
│ └── tenant.entity.ts
|
|
│
|
|
└── shared/
|
|
├── constants/ # SSOT
|
|
│ ├── database.constants.ts
|
|
│ ├── api.constants.ts
|
|
│ ├── enums.constants.ts
|
|
│ └── index.ts
|
|
├── services/
|
|
│ └── base.service.ts # CRUD multi-tenant
|
|
└── database/
|
|
└── typeorm.config.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Modulos Implementados
|
|
|
|
### Auth Module
|
|
|
|
Autenticacion JWT con refresh tokens y multi-tenancy.
|
|
|
|
```typescript
|
|
// Services
|
|
AuthService
|
|
├── login(dto) // Login con email/password
|
|
├── register(dto) // Registro de usuarios
|
|
├── refresh(dto) // Renovar tokens
|
|
├── logout(token) // Revocar refresh token
|
|
└── changePassword(dto) // Cambiar password
|
|
|
|
// Middleware
|
|
AuthMiddleware
|
|
├── authenticate // Validar JWT (requerido)
|
|
├── optionalAuthenticate // Validar JWT (opcional)
|
|
├── authorize(...roles) // Autorizar por roles
|
|
├── requireAdmin // Solo admin/super_admin
|
|
└── requireSupervisor // Solo supervisores+
|
|
```
|
|
|
|
### Budgets Module (MAI-003)
|
|
|
|
Catalogo de conceptos y presupuestos de obra.
|
|
|
|
```typescript
|
|
// Entities
|
|
Concepto // Catalogo jerarquico (arbol)
|
|
Presupuesto // Presupuestos versionados
|
|
PresupuestoPartida // Lineas con calculo automatico
|
|
|
|
// Services
|
|
ConceptoService
|
|
├── createConcepto(ctx, dto) // Crear con nivel/path automatico
|
|
├── findRootConceptos(ctx) // Conceptos raiz
|
|
├── findChildren(ctx, parentId) // Hijos de un concepto
|
|
├── getConceptoTree(ctx, rootId) // Arbol completo
|
|
└── search(ctx, term) // Busqueda por codigo/nombre
|
|
|
|
PresupuestoService
|
|
├── createPresupuesto(ctx, dto)
|
|
├── findByFraccionamiento(ctx, id)
|
|
├── findWithPartidas(ctx, id)
|
|
├── addPartida(ctx, id, dto)
|
|
├── updatePartida(ctx, id, dto)
|
|
├── removePartida(ctx, id)
|
|
├── recalculateTotal(ctx, id)
|
|
├── createNewVersion(ctx, id) // Versionamiento
|
|
└── approve(ctx, id)
|
|
```
|
|
|
|
### Progress Module (MAI-005)
|
|
|
|
Control de avances fisicos y bitacora de obra.
|
|
|
|
```typescript
|
|
// Entities
|
|
AvanceObra // Avances con workflow
|
|
FotoAvance // Evidencias fotograficas con GPS
|
|
BitacoraObra // Bitacora diaria
|
|
ProgramaObra // Programa maestro
|
|
ProgramaActividad // Actividades WBS
|
|
|
|
// Services
|
|
AvanceObraService
|
|
├── createAvance(ctx, dto)
|
|
├── findByLote(ctx, loteId)
|
|
├── findByDepartamento(ctx, deptoId)
|
|
├── findWithFilters(ctx, filters)
|
|
├── findWithFotos(ctx, id)
|
|
├── addFoto(ctx, id, dto)
|
|
├── review(ctx, id) // Workflow: revisar
|
|
├── approve(ctx, id) // Workflow: aprobar
|
|
├── reject(ctx, id, reason) // Workflow: rechazar
|
|
└── getAccumulatedProgress(ctx) // Acumulado por concepto
|
|
|
|
BitacoraObraService
|
|
├── createEntry(ctx, dto) // Numero automatico
|
|
├── findByFraccionamiento(ctx, id)
|
|
├── findWithFilters(ctx, id, filters)
|
|
├── findByDate(ctx, id, date)
|
|
├── findLatest(ctx, id)
|
|
└── getStats(ctx, id) // Estadisticas
|
|
```
|
|
|
|
### Estimates Module (MAI-008)
|
|
|
|
Estimaciones periodicas con workflow de aprobacion.
|
|
|
|
```typescript
|
|
// Entities
|
|
Estimacion // Estimaciones con workflow
|
|
EstimacionConcepto // Lineas con acumulados
|
|
Generador // Numeros generadores
|
|
Anticipo // Anticipos
|
|
Amortizacion // Amortizaciones
|
|
Retencion // Retenciones
|
|
FondoGarantia // Fondo de garantia
|
|
EstimacionWorkflow // Historial de estados
|
|
|
|
// Services
|
|
EstimacionService
|
|
├── createEstimacion(ctx, dto) // Numero automatico
|
|
├── findByContrato(ctx, contratoId)
|
|
├── findWithFilters(ctx, filters)
|
|
├── findWithDetails(ctx, id) // Con relaciones
|
|
├── addConcepto(ctx, id, dto)
|
|
├── addGenerador(ctx, conceptoId, dto)
|
|
├── recalculateTotals(ctx, id) // Llama funcion PG
|
|
├── submit(ctx, id) // Workflow
|
|
├── review(ctx, id) // Workflow
|
|
├── approve(ctx, id) // Workflow
|
|
├── reject(ctx, id, reason) // Workflow
|
|
└── getContractSummary(ctx, id) // Resumen financiero
|
|
```
|
|
|
|
---
|
|
|
|
## Base Service
|
|
|
|
Servicio base con CRUD multi-tenant.
|
|
|
|
```typescript
|
|
// Uso
|
|
class MiService extends BaseService<MiEntity> {
|
|
constructor(repository: Repository<MiEntity>) {
|
|
super(repository);
|
|
}
|
|
}
|
|
|
|
// Metodos disponibles
|
|
BaseService<T>
|
|
├── findAll(ctx, options?) // Paginado
|
|
├── findById(ctx, id)
|
|
├── findOne(ctx, where)
|
|
├── find(ctx, options)
|
|
├── create(ctx, data)
|
|
├── update(ctx, id, data)
|
|
├── softDelete(ctx, id)
|
|
├── hardDelete(ctx, id)
|
|
├── count(ctx, where?)
|
|
└── exists(ctx, where)
|
|
|
|
// ServiceContext
|
|
interface ServiceContext {
|
|
tenantId: string;
|
|
userId: string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## SSOT Constants
|
|
|
|
Sistema de constantes centralizadas.
|
|
|
|
```typescript
|
|
// database.constants.ts
|
|
import { DB_SCHEMAS, DB_TABLES, TABLE_REFS } from '@shared/constants';
|
|
|
|
DB_SCHEMAS.CONSTRUCTION // 'construction'
|
|
DB_TABLES.construction.CONCEPTOS // 'conceptos'
|
|
TABLE_REFS.FRACCIONAMIENTOS // 'construction.fraccionamientos'
|
|
|
|
// api.constants.ts
|
|
import { API_ROUTES } from '@shared/constants';
|
|
|
|
API_ROUTES.PRESUPUESTOS.BASE // '/api/v1/presupuestos'
|
|
API_ROUTES.ESTIMACIONES.BY_ID(id) // '/api/v1/estimaciones/:id'
|
|
|
|
// enums.constants.ts
|
|
import { ROLES, PROJECT_STATUS } from '@shared/constants';
|
|
|
|
ROLES.ADMIN // 'admin'
|
|
PROJECT_STATUS.IN_PROGRESS // 'in_progress'
|
|
```
|
|
|
|
---
|
|
|
|
## Scripts NPM
|
|
|
|
```bash
|
|
# Desarrollo
|
|
npm run dev # Hot-reload con ts-node-dev
|
|
npm run build # Compilar TypeScript
|
|
npm run start # Produccion (dist/)
|
|
|
|
# Calidad
|
|
npm run lint # ESLint
|
|
npm run lint:fix # ESLint con autofix
|
|
npm run test # Jest
|
|
npm run test:watch # Jest watch mode
|
|
npm run test:coverage # Jest con cobertura
|
|
|
|
# Base de datos
|
|
npm run migration:generate # Generar migracion
|
|
npm run migration:run # Ejecutar migraciones
|
|
npm run migration:revert # Revertir ultima
|
|
|
|
# SSOT
|
|
npm run validate:constants # Validar no hardcoding
|
|
npm run sync:enums # Sincronizar a frontend
|
|
npm run precommit # lint + validate
|
|
```
|
|
|
|
---
|
|
|
|
## Convenciones
|
|
|
|
### Nomenclatura
|
|
|
|
| Tipo | Convencion | Ejemplo |
|
|
|------|------------|---------|
|
|
| Archivos | kebab-case.tipo.ts | `concepto.entity.ts` |
|
|
| Clases | PascalCase + sufijo | `ConceptoService` |
|
|
| Variables | camelCase | `totalAmount` |
|
|
| Constantes | UPPER_SNAKE_CASE | `DB_SCHEMAS` |
|
|
| Metodos | camelCase + verbo | `findByContrato` |
|
|
|
|
### Entity Pattern
|
|
|
|
```typescript
|
|
@Entity({ schema: 'construction', name: 'conceptos' })
|
|
@Index(['tenantId', 'code'], { unique: true })
|
|
export class Concepto {
|
|
@PrimaryGeneratedColumn('uuid')
|
|
id: string;
|
|
|
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
|
tenantId: string;
|
|
|
|
// ... columnas con name: 'snake_case'
|
|
|
|
// Soft delete
|
|
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
|
deletedAt: Date | null;
|
|
|
|
// Relations
|
|
@ManyToOne(() => Tenant)
|
|
@JoinColumn({ name: 'tenant_id' })
|
|
tenant: Tenant;
|
|
}
|
|
```
|
|
|
|
### Service Pattern
|
|
|
|
```typescript
|
|
export class MiService extends BaseService<MiEntity> {
|
|
constructor(
|
|
repository: Repository<MiEntity>,
|
|
private readonly otroRepo: Repository<OtroEntity>
|
|
) {
|
|
super(repository);
|
|
}
|
|
|
|
async miMetodo(ctx: ServiceContext, data: MiDto): Promise<MiEntity> {
|
|
// ctx tiene tenantId y userId
|
|
return this.create(ctx, data);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Seguridad
|
|
|
|
- Helmet para HTTP security headers
|
|
- CORS configurado por dominio
|
|
- Rate limiting por IP
|
|
- JWT con refresh tokens
|
|
- Bcrypt (12 rounds) para passwords
|
|
- class-validator para inputs
|
|
- RLS para aislamiento de tenants
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
# Ejecutar tests
|
|
npm test
|
|
|
|
# Con cobertura
|
|
npm run test:coverage
|
|
|
|
# Watch mode
|
|
npm run test:watch
|
|
```
|
|
|
|
```typescript
|
|
// Ejemplo de test
|
|
describe('ConceptoService', () => {
|
|
let service: ConceptoService;
|
|
let mockRepo: jest.Mocked<Repository<Concepto>>;
|
|
|
|
beforeEach(() => {
|
|
mockRepo = createMockRepository();
|
|
service = new ConceptoService(mockRepo);
|
|
});
|
|
|
|
it('should create concepto with level', async () => {
|
|
const ctx = { tenantId: 'uuid', userId: 'uuid' };
|
|
const dto = { code: '001', name: 'Test' };
|
|
|
|
mockRepo.save.mockResolvedValue({ ...dto, level: 0 });
|
|
|
|
const result = await service.createConcepto(ctx, dto);
|
|
expect(result.level).toBe(0);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging
|
|
|
|
### VS Code
|
|
|
|
```json
|
|
{
|
|
"type": "node",
|
|
"request": "launch",
|
|
"name": "Debug Backend",
|
|
"runtimeArgs": ["-r", "ts-node/register"],
|
|
"args": ["${workspaceFolder}/src/server.ts"],
|
|
"env": { "NODE_ENV": "development" }
|
|
}
|
|
```
|
|
|
|
### Logs
|
|
|
|
```typescript
|
|
// Configurar en .env
|
|
LOG_LEVEL=debug
|
|
LOG_FORMAT=dev
|
|
```
|
|
|
|
---
|
|
|
|
**Ultima actualizacion:** 2025-12-12
|