# 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<Concepto> {
  constructor(
    repository: Repository<Concepto>,
    private readonly presupuestoRepo: Repository<Presupuesto>
  ) {
    super(repository);
  }

  // Metodos especificos del dominio
  async findRootConceptos(ctx: ServiceContext): Promise<Concepto[]> {
    return this.repository.find({
      where: {
        tenantId: ctx.tenantId,
        parentId: IsNull(),
        deletedAt: IsNull(),
      },
      order: { code: 'ASC' },
    });
  }
}
```

### 3. BaseService

```typescript
// Metodos disponibles en BaseService<T>
abstract class BaseService<T> {
  // Lectura
  findAll(ctx, options?): Promise<PaginatedResult<T>>
  findById(ctx, id): Promise<T | null>
  findOne(ctx, where): Promise<T | null>
  find(ctx, options): Promise<T[]>
  count(ctx, where?): Promise<number>
  exists(ctx, where): Promise<boolean>

  // Escritura
  create(ctx, data): Promise<T>
  update(ctx, id, data): Promise<T | null>
  softDelete(ctx, id): Promise<boolean>
  hardDelete(ctx, id): Promise<boolean>
}
```

### 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<Estimacion> {
  async submit(ctx: ServiceContext, id: string): Promise<Estimacion | null> {
    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<Estimacion | null> {
    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 <access_token>
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<Repository<Concepto>>;
  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
