# Arquitectura Tecnica - ERP Construccion **Version:** 1.0.0 **Fecha:** 2025-12-12 **Para:** Desarrolladores --- ## Stack Tecnologico | Capa | Tecnologia | Version | |------|------------|---------| | **Runtime** | Node.js | 20 LTS | | **Backend Framework** | Express.js | 4.x | | **ORM** | TypeORM | 0.3.x | | **Base de Datos** | PostgreSQL + PostGIS | 15 | | **Cache** | Redis | 7 | | **Frontend** | React + Vite | 18.x / 5.x | | **Lenguaje** | TypeScript | 5.x | | **Validacion** | class-validator | 0.14.x | | **Autenticacion** | JWT + Refresh Tokens | - | --- ## Estructura de Directorios ``` construccion/ ├── backend/ │ ├── src/ │ │ ├── modules/ # Modulos de dominio │ │ │ ├── auth/ # Autenticacion JWT │ │ │ ├── budgets/ # MAI-003 Presupuestos │ │ │ ├── progress/ # MAI-005 Control de Obra │ │ │ ├── estimates/ # MAI-008 Estimaciones │ │ │ ├── construction/ # MAI-002 Proyectos │ │ │ ├── hr/ # MAI-007 RRHH │ │ │ ├── hse/ # MAA-017 HSE │ │ │ └── core/ # User, Tenant │ │ │ │ │ ├── shared/ │ │ │ ├── constants/ # SSOT - Constantes centralizadas │ │ │ │ ├── database.constants.ts │ │ │ │ ├── api.constants.ts │ │ │ │ ├── enums.constants.ts │ │ │ │ └── index.ts │ │ │ ├── services/ │ │ │ │ └── base.service.ts │ │ │ └── database/ │ │ │ └── typeorm.config.ts │ │ │ │ │ ├── config/ # Configuracion de app │ │ ├── types/ # Tipos globales │ │ └── server.ts # Entry point │ │ │ ├── package.json │ └── tsconfig.json │ ├── frontend/ │ └── web/ # App React │ ├── database/ │ └── schemas/ # DDL SQL │ ├── 01-construction-schema-ddl.sql │ ├── 02-hr-schema-ddl.sql │ ├── 03-hse-schema-ddl.sql │ ├── 04-estimates-schema-ddl.sql │ ├── 05-infonavit-schema-ddl.sql │ ├── 06-inventory-ext-schema-ddl.sql │ └── 07-purchase-ext-schema-ddl.sql │ ├── docs/ # Documentacion ├── devops/ # Scripts CI/CD └── docker-compose.yml ``` --- ## Arquitectura Multi-tenant ### Row Level Security (RLS) El sistema utiliza RLS de PostgreSQL para aislamiento de datos por tenant. ```sql -- Ejemplo de politica RLS CREATE POLICY tenant_isolation ON construction.fraccionamientos FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID); ``` ### Contexto de Tenant ```typescript // El middleware establece el contexto RLS await dataSource.query(` SELECT set_config('app.current_tenant_id', $1, true) `, [tenantId]); ``` ### ServiceContext Todas las operaciones de servicio requieren un contexto: ```typescript interface ServiceContext { tenantId: string; // UUID del tenant actual userId: string; // UUID del usuario autenticado } ``` --- ## Patrones de Codigo ### 1. Entity Pattern ```typescript // Convencion de entidades TypeORM @Entity({ schema: 'construction', name: 'conceptos' }) @Index(['tenantId', 'code'], { unique: true }) export class Concepto { // Primary key UUID @PrimaryGeneratedColumn('uuid') id: string; // Tenant discriminador (siempre presente) @Column({ name: 'tenant_id', type: 'uuid' }) tenantId: string; // Columnas con snake_case en DB @Column({ name: 'created_at', type: 'timestamptz', default: () => 'NOW()' }) createdAt: Date; // Soft delete @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) deletedAt: Date | null; // Relaciones @ManyToOne(() => Tenant) @JoinColumn({ name: 'tenant_id' }) tenant: Tenant; } ``` ### 2. Service Pattern ```typescript // Extender BaseService para CRUD automatico export class ConceptoService extends BaseService { constructor( repository: Repository, private readonly presupuestoRepo: Repository ) { super(repository); } // Metodos especificos del dominio async findRootConceptos(ctx: ServiceContext): Promise { return this.repository.find({ where: { tenantId: ctx.tenantId, parentId: IsNull(), deletedAt: IsNull(), }, order: { code: 'ASC' }, }); } } ``` ### 3. BaseService ```typescript // Metodos disponibles en BaseService abstract class BaseService { // Lectura findAll(ctx, options?): Promise> findById(ctx, id): Promise findOne(ctx, where): Promise find(ctx, options): Promise count(ctx, where?): Promise exists(ctx, where): Promise // Escritura create(ctx, data): Promise update(ctx, id, data): Promise softDelete(ctx, id): Promise hardDelete(ctx, id): Promise } ``` ### 4. DTO Pattern ```typescript // DTOs con class-validator export class CreateConceptoDto { @IsString() @MinLength(1) @MaxLength(20) code: string; @IsString() @MinLength(1) @MaxLength(200) name: string; @IsOptional() @IsUUID() parentId?: string; @IsOptional() @IsNumber({ maxDecimalPlaces: 4 }) @Min(0) unitPrice?: number; } ``` --- ## Workflows de Estado ### Patron de Workflow ```typescript // Entidad con estado y workflow @Entity({ schema: 'estimates', name: 'estimaciones' }) export class Estimacion { @Column({ type: 'enum', enum: EstimacionStatus, default: EstimacionStatus.DRAFT, }) status: EstimacionStatus; // Timestamps de workflow @Column({ name: 'submitted_at', type: 'timestamptz', nullable: true }) submittedAt: Date | null; @Column({ name: 'reviewed_at', type: 'timestamptz', nullable: true }) reviewedAt: Date | null; @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) approvedAt: Date | null; @Column({ name: 'rejected_at', type: 'timestamptz', nullable: true }) rejectedAt: Date | null; } ``` ### Transiciones Validas ```typescript // Servicio con metodos de transicion export class EstimacionService extends BaseService { async submit(ctx: ServiceContext, id: string): Promise { const estimacion = await this.findById(ctx, id); if (!estimacion || estimacion.status !== EstimacionStatus.DRAFT) { return null; } return this.update(ctx, id, { status: EstimacionStatus.SUBMITTED, submittedAt: new Date(), submittedById: ctx.userId, }); } async approve(ctx: ServiceContext, id: string): Promise { const estimacion = await this.findById(ctx, id); if (!estimacion || estimacion.status !== EstimacionStatus.REVIEWED) { return null; } return this.update(ctx, id, { status: EstimacionStatus.APPROVED, approvedAt: new Date(), approvedById: ctx.userId, }); } } ``` --- ## Autenticacion y Autorizacion ### JWT Flow ``` 1. Cliente envia credenciales → POST /auth/login 2. Backend valida y genera: - Access Token (15min) - Refresh Token (7 dias) 3. Cliente almacena tokens 4. Requests incluyen: Authorization: Bearer 5. Al expirar access token: POST /auth/refresh ``` ### Middleware de Auth ```typescript // Uso en rutas router.get('/conceptos', AuthMiddleware.authenticate, // Requerido: validar JWT AuthMiddleware.authorize('admin', 'engineer'), // Opcional: roles conceptosController.findAll ); // Para rutas publicas router.get('/health', AuthMiddleware.optionalAuthenticate, healthController.check ); ``` ### Roles del Sistema | Rol | Permisos | |-----|----------| | `super_admin` | Todo el sistema | | `admin` | Todo dentro del tenant | | `project_manager` | Proyectos asignados | | `supervisor` | Control de obra | | `engineer` | Operaciones de campo | | `accountant` | Estimaciones y finanzas | | `viewer` | Solo lectura | --- ## Base de Datos ### Schemas PostgreSQL | Schema | Tablas | Descripcion | |--------|--------|-------------| | `auth` | 10 | Usuarios, roles, tokens | | `construction` | 24 | Proyectos, presupuestos | | `hr` | 8 | Empleados, asistencias | | `hse` | 58 | Seguridad industrial | | `estimates` | 8 | Estimaciones | | `infonavit` | 8 | Integracion INFONAVIT | | `inventory` | 4 | Inventarios | ### Convenciones de Tablas - Nombres en **snake_case** plural: `conceptos`, `presupuestos` - Todas tienen `tenant_id` (excepto auth global) - Todas tienen `deleted_at` para soft delete - Indices en `(tenant_id, ...)` para performance ### Tipos Especiales PostgreSQL ```sql -- Columnas generadas (calculated) total_amount DECIMAL(18,2) GENERATED ALWAYS AS (quantity * unit_price) STORED -- Tipos geometricos (PostGIS) location GEOGRAPHY(POINT, 4326) -- Enums status estimate_status_enum NOT NULL DEFAULT 'draft' ``` --- ## SSOT (Single Source of Truth) ### database.constants.ts ```typescript // Schemas export const DB_SCHEMAS = { AUTH: 'auth', CONSTRUCTION: 'construction', HR: 'hr', HSE: 'hse', ESTIMATES: 'estimates', } as const; // Tablas export const DB_TABLES = { construction: { CONCEPTOS: 'conceptos', PRESUPUESTOS: 'presupuestos', FRACCIONAMIENTOS: 'fraccionamientos', }, // ... } as const; // Referencias completas export const TABLE_REFS = { CONCEPTOS: 'construction.conceptos', PRESUPUESTOS: 'construction.presupuestos', } as const; ``` ### api.constants.ts ```typescript export const API_ROUTES = { AUTH: { BASE: '/api/v1/auth', LOGIN: '/api/v1/auth/login', REGISTER: '/api/v1/auth/register', REFRESH: '/api/v1/auth/refresh', }, PRESUPUESTOS: { BASE: '/api/v1/presupuestos', BY_ID: (id: string) => `/api/v1/presupuestos/${id}`, }, } as const; ``` ### enums.constants.ts ```typescript export const ROLES = { SUPER_ADMIN: 'super_admin', ADMIN: 'admin', PROJECT_MANAGER: 'project_manager', // ... } as const; export const ESTIMATE_STATUS = { DRAFT: 'draft', SUBMITTED: 'submitted', REVIEWED: 'reviewed', APPROVED: 'approved', REJECTED: 'rejected', } as const; ``` --- ## Testing ### Estructura de Tests ``` backend/ ├── src/ │ └── modules/ │ └── budgets/ │ └── services/ │ └── concepto.service.spec.ts └── test/ ├── setup.ts # Jest setup └── fixtures/ # Datos de prueba ``` ### Ejemplo de Test ```typescript describe('ConceptoService', () => { let service: ConceptoService; let mockRepo: jest.Mocked>; const ctx: ServiceContext = { tenantId: 'tenant-uuid', userId: 'user-uuid', }; beforeEach(() => { mockRepo = createMockRepository(); service = new ConceptoService(mockRepo); }); it('should create concepto with level 0 for root', async () => { const dto = { code: '001', name: 'Test' }; mockRepo.save.mockResolvedValue({ id: 'uuid', ...dto, level: 0, tenantId: ctx.tenantId, }); const result = await service.createConcepto(ctx, dto); expect(result.level).toBe(0); expect(result.tenantId).toBe(ctx.tenantId); }); }); ``` --- ## Docker ### docker-compose.yml ```yaml services: postgres: image: postgis/postgis:15-3.3 ports: - "5432:5432" environment: POSTGRES_DB: erp_construccion POSTGRES_USER: construccion POSTGRES_PASSWORD: ${DB_PASSWORD} redis: image: redis:7-alpine ports: - "6379:6379" backend: build: ./backend ports: - "3000:3000" depends_on: - postgres - redis environment: NODE_ENV: development DB_HOST: postgres ``` ### Comandos Docker ```bash # Desarrollo docker-compose up -d # Con herramientas dev (Adminer, Mailhog) docker-compose --profile dev up -d # Logs docker-compose logs -f backend # Rebuild docker-compose build --no-cache backend ``` --- ## CI/CD ### GitHub Actions Pipeline ```yaml # .github/workflows/ci.yml jobs: lint: - npm run lint - npm run validate:constants test: - npm run test:coverage build: - npm run build - docker build ``` ### Scripts de Validacion ```bash # Validar que no hay hardcoding de constantes npm run validate:constants # Sincronizar enums backend → frontend npm run sync:enums # Pre-commit hook npm run precommit # lint + validate ``` --- ## Performance ### Indices Recomendados ```sql -- Siempre indexar tenant_id CREATE INDEX idx_conceptos_tenant ON construction.conceptos(tenant_id); -- Indices compuestos para queries frecuentes CREATE INDEX idx_conceptos_tenant_code ON construction.conceptos(tenant_id, code); -- Indices parciales para soft delete CREATE INDEX idx_conceptos_active ON construction.conceptos(tenant_id) WHERE deleted_at IS NULL; ``` ### Query Optimization ```typescript // Eager loading para evitar N+1 const presupuesto = await this.repository.findOne({ where: { id, tenantId: ctx.tenantId }, relations: ['partidas', 'partidas.concepto'], }); // Select especifico const totals = await this.repository .createQueryBuilder('p') .select('SUM(p.totalAmount)', 'total') .where('p.tenantId = :tenantId', { tenantId: ctx.tenantId }) .getRawOne(); ``` --- ## Debugging ### VS Code Launch Config ```json { "type": "node", "request": "launch", "name": "Debug Backend", "runtimeArgs": ["-r", "ts-node/register"], "args": ["${workspaceFolder}/backend/src/server.ts"], "env": { "NODE_ENV": "development", "LOG_LEVEL": "debug" } } ``` ### Logs ```bash # Variables de entorno LOG_LEVEL=debug # debug | info | warn | error LOG_FORMAT=dev # dev | json # Ver SQL queries TYPEORM_LOGGING=true ``` --- ## Referencias - [Backend README](../backend/README.md) - [API OpenAPI Spec](./api/openapi.yaml) - [Arquitectura SaaS](./00-vision-general/ARQUITECTURA-SAAS.md) - [Mapa de Base de Datos](../database/_MAP.md) --- **Ultima actualizacion:** 2025-12-12