# Especificacion Tecnica Backend - Modulo Purchases (MGN-012)

## METADATOS

| Campo | Valor |
|-------|-------|
| **Modulo** | MGN-012 |
| **Nombre** | Purchases (Compras) |
| **Version** | 1.0.0 |
| **Fecha** | 2026-01-10 |
| **Estado** | Implementado |
| **Backend Path** | `backend/src/modules/purchases/` |
| **Schema BD** | `purchase` |

---

## SERVICIOS

### 1. PurchasesService

**Archivo:** `purchases.service.ts`

**Descripcion:** Servicio para gestion de ordenes de compra (Purchase Orders).

#### Metodos

| Metodo | Parametros | Retorno | Descripcion |
|--------|------------|---------|-------------|
| `findAll` | `tenantId: string`, `filters: PurchaseOrderFilters` | `Promise<{ data: PurchaseOrder[]; total: number }>` | Lista ordenes de compra con paginacion y filtros |
| `findById` | `id: string`, `tenantId: string` | `Promise<PurchaseOrder>` | Obtiene una orden por ID con sus lineas |
| `create` | `dto: CreatePurchaseOrderDto`, `tenantId: string`, `userId: string` | `Promise<PurchaseOrder>` | Crea nueva orden de compra con lineas (transaccion) |
| `update` | `id: string`, `dto: UpdatePurchaseOrderDto`, `tenantId: string`, `userId: string` | `Promise<PurchaseOrder>` | Actualiza orden en estado draft |
| `confirm` | `id: string`, `tenantId: string`, `userId: string` | `Promise<PurchaseOrder>` | Confirma orden (draft -> confirmed) |
| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise<PurchaseOrder>` | Cancela orden (no aplica a done) |
| `delete` | `id: string`, `tenantId: string` | `Promise<void>` | Elimina orden en estado draft |

#### Tipos de Estado (OrderStatus)

```typescript
type OrderStatus = 'draft' | 'sent' | 'confirmed' | 'done' | 'cancelled';
```

#### Filtros Disponibles (PurchaseOrderFilters)

```typescript
interface PurchaseOrderFilters {
  company_id?: string;
  partner_id?: string;
  status?: OrderStatus;
  date_from?: string;
  date_to?: string;
  search?: string;
  page?: number;     // default: 1
  limit?: number;    // default: 20
}
```

---

### 2. RfqsService

**Archivo:** `rfqs.service.ts`

**Descripcion:** Servicio para gestion de Solicitudes de Cotizacion (Request for Quotation - RFQ).

#### Metodos

| Metodo | Parametros | Retorno | Descripcion |
|--------|------------|---------|-------------|
| `findAll` | `tenantId: string`, `filters: RfqFilters` | `Promise<{ data: Rfq[]; total: number }>` | Lista RFQs con paginacion y filtros |
| `findById` | `id: string`, `tenantId: string` | `Promise<Rfq>` | Obtiene RFQ por ID con lineas y partners |
| `create` | `dto: CreateRfqDto`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Crea nueva RFQ con lineas (transaccion) |
| `update` | `id: string`, `dto: UpdateRfqDto`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Actualiza RFQ en estado draft |
| `addLine` | `rfqId: string`, `dto: CreateRfqLineDto`, `tenantId: string` | `Promise<RfqLine>` | Agrega linea a RFQ en draft |
| `updateLine` | `rfqId: string`, `lineId: string`, `dto: UpdateRfqLineDto`, `tenantId: string` | `Promise<RfqLine>` | Actualiza linea de RFQ en draft |
| `removeLine` | `rfqId: string`, `lineId: string`, `tenantId: string` | `Promise<void>` | Elimina linea (minimo 1 linea requerida) |
| `send` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Envia RFQ (draft -> sent) |
| `markResponded` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Marca como respondida (sent -> responded) |
| `accept` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Acepta RFQ (sent/responded -> accepted) |
| `reject` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Rechaza RFQ (sent/responded -> rejected) |
| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Cancela RFQ (no aplica a accepted) |
| `delete` | `id: string`, `tenantId: string` | `Promise<void>` | Elimina RFQ en estado draft |

#### Tipos de Estado (RfqStatus)

```typescript
type RfqStatus = 'draft' | 'sent' | 'responded' | 'accepted' | 'rejected' | 'cancelled';
```

#### Filtros Disponibles (RfqFilters)

```typescript
interface RfqFilters {
  company_id?: string;
  status?: RfqStatus;
  date_from?: string;
  date_to?: string;
  search?: string;
  page?: number;     // default: 1
  limit?: number;    // default: 20
}
```

---

## ENTIDADES

### Schema: `purchase`

#### Tabla: purchase_orders

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NO | gen_random_uuid() | Clave primaria |
| `tenant_id` | UUID | NO | - | FK a auth.tenants |
| `company_id` | UUID | NO | - | FK a auth.companies |
| `name` | VARCHAR(100) | NO | - | Numero de orden (unico por company) |
| `ref` | VARCHAR(100) | SI | - | Referencia del proveedor |
| `partner_id` | UUID | NO | - | FK a core.partners (proveedor) |
| `order_date` | DATE | NO | - | Fecha de la orden |
| `expected_date` | DATE | SI | - | Fecha esperada de recepcion |
| `effective_date` | DATE | SI | - | Fecha efectiva de recepcion |
| `currency_id` | UUID | NO | - | FK a core.currencies |
| `payment_term_id` | UUID | SI | - | FK a financial.payment_terms |
| `amount_untaxed` | DECIMAL(15,2) | NO | 0 | Monto sin impuestos |
| `amount_tax` | DECIMAL(15,2) | NO | 0 | Monto de impuestos |
| `amount_total` | DECIMAL(15,2) | NO | 0 | Monto total |
| `status` | order_status | NO | 'draft' | Estado de la orden |
| `receipt_status` | VARCHAR(20) | SI | 'pending' | Estado de recepcion |
| `invoice_status` | VARCHAR(20) | SI | 'pending' | Estado de facturacion |
| `picking_id` | UUID | SI | - | FK a inventory.pickings |
| `invoice_id` | UUID | SI | - | FK a financial.invoices |
| `notes` | TEXT | SI | - | Notas adicionales |
| `dest_address_id` | UUID | SI | - | Direccion de envio (dropship) |
| `locked` | BOOLEAN | SI | FALSE | Bloqueo de orden |
| `approval_required` | BOOLEAN | SI | FALSE | Requiere aprobacion |
| `amount_approval_threshold` | DECIMAL(15,2) | SI | - | Umbral de aprobacion |
| `created_at` | TIMESTAMP | NO | CURRENT_TIMESTAMP | Fecha creacion |
| `created_by` | UUID | SI | - | Usuario creador |
| `updated_at` | TIMESTAMP | SI | - | Fecha actualizacion |
| `updated_by` | UUID | SI | - | Usuario actualizacion |
| `confirmed_at` | TIMESTAMP | SI | - | Fecha confirmacion |
| `confirmed_by` | UUID | SI | - | Usuario confirmacion |
| `approved_at` | TIMESTAMP | SI | - | Fecha aprobacion |
| `approved_by` | UUID | SI | - | Usuario aprobacion |
| `cancelled_at` | TIMESTAMP | SI | - | Fecha cancelacion |
| `cancelled_by` | UUID | SI | - | Usuario cancelacion |

**Indices:**
- `idx_purchase_orders_tenant_id`
- `idx_purchase_orders_company_id`
- `idx_purchase_orders_partner_id`
- `idx_purchase_orders_name`
- `idx_purchase_orders_status`
- `idx_purchase_orders_order_date`
- `idx_purchase_orders_expected_date`

**Constraint UNIQUE:** `(company_id, name)`

---

#### Tabla: purchase_order_lines

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NO | gen_random_uuid() | Clave primaria |
| `tenant_id` | UUID | NO | - | FK a auth.tenants |
| `order_id` | UUID | NO | - | FK a purchase.purchase_orders |
| `product_id` | UUID | NO | - | FK a inventory.products |
| `description` | TEXT | NO | - | Descripcion del producto |
| `quantity` | DECIMAL(12,4) | NO | - | Cantidad solicitada |
| `qty_received` | DECIMAL(12,4) | SI | 0 | Cantidad recibida |
| `qty_invoiced` | DECIMAL(12,4) | SI | 0 | Cantidad facturada |
| `uom_id` | UUID | NO | - | FK a core.uom |
| `price_unit` | DECIMAL(15,4) | NO | - | Precio unitario |
| `discount` | DECIMAL(5,2) | SI | 0 | Porcentaje descuento |
| `tax_ids` | UUID[] | SI | '{}' | Array de impuestos |
| `amount_untaxed` | DECIMAL(15,2) | NO | - | Subtotal sin impuestos |
| `amount_tax` | DECIMAL(15,2) | NO | - | Monto impuestos |
| `amount_total` | DECIMAL(15,2) | NO | - | Total linea |
| `expected_date` | DATE | SI | - | Fecha esperada |
| `analytic_account_id` | UUID | SI | - | FK a analytics.analytic_accounts |
| `created_at` | TIMESTAMP | NO | CURRENT_TIMESTAMP | Fecha creacion |
| `updated_at` | TIMESTAMP | SI | - | Fecha actualizacion |

**Constraints:**
- `chk_purchase_order_lines_quantity`: quantity > 0
- `chk_purchase_order_lines_discount`: discount >= 0 AND discount <= 100

---

#### Tabla: rfqs

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NO | gen_random_uuid() | Clave primaria |
| `tenant_id` | UUID | NO | - | FK a auth.tenants |
| `company_id` | UUID | NO | - | FK a auth.companies |
| `name` | VARCHAR(100) | NO | - | Numero RFQ (ej: RFQ-000001) |
| `partner_ids` | UUID[] | NO | - | Array de proveedores |
| `request_date` | DATE | NO | - | Fecha de solicitud |
| `deadline_date` | DATE | SI | - | Fecha limite respuesta |
| `response_date` | DATE | SI | - | Fecha de respuesta |
| `status` | rfq_status | NO | 'draft' | Estado del RFQ |
| `description` | TEXT | SI | - | Descripcion |
| `notes` | TEXT | SI | - | Notas adicionales |
| `created_at` | TIMESTAMP | NO | CURRENT_TIMESTAMP | Fecha creacion |
| `created_by` | UUID | SI | - | Usuario creador |
| `updated_at` | TIMESTAMP | SI | - | Fecha actualizacion |
| `updated_by` | UUID | SI | - | Usuario actualizacion |

**Constraint UNIQUE:** `(company_id, name)`

---

#### Tabla: rfq_lines

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NO | gen_random_uuid() | Clave primaria |
| `tenant_id` | UUID | NO | - | FK a auth.tenants |
| `rfq_id` | UUID | NO | - | FK a purchase.rfqs |
| `product_id` | UUID | SI | - | FK a inventory.products |
| `description` | TEXT | NO | - | Descripcion del producto |
| `quantity` | DECIMAL(12,4) | NO | - | Cantidad solicitada |
| `uom_id` | UUID | NO | - | FK a core.uom |
| `created_at` | TIMESTAMP | NO | CURRENT_TIMESTAMP | Fecha creacion |

**Constraint:** `chk_rfq_lines_quantity`: quantity > 0

---

## DTOs

### Purchase Orders

#### CreatePurchaseOrderDto

```typescript
interface CreatePurchaseOrderDto {
  company_id: string;           // UUID, requerido
  name: string;                 // min: 1, max: 100
  ref?: string;                 // max: 100
  partner_id: string;           // UUID, requerido
  order_date: string;           // formato: YYYY-MM-DD
  expected_date?: string;       // formato: YYYY-MM-DD
  currency_id: string;          // UUID, requerido
  payment_term_id?: string;     // UUID
  notes?: string;
  lines: PurchaseOrderLineDto[]; // min: 1 linea
}

interface PurchaseOrderLineDto {
  product_id: string;           // UUID, requerido
  description: string;          // min: 1
  quantity: number;             // positive
  uom_id: string;               // UUID, requerido
  price_unit: number;           // min: 0
  discount?: number;            // 0-100, default: 0
  amount_untaxed: number;       // min: 0
}
```

#### UpdatePurchaseOrderDto

```typescript
interface UpdatePurchaseOrderDto {
  ref?: string | null;
  partner_id?: string;
  order_date?: string;
  expected_date?: string | null;
  currency_id?: string;
  payment_term_id?: string | null;
  notes?: string | null;
  lines?: PurchaseOrderLineDto[]; // min: 1 si se proporciona
}
```

### RFQs

#### CreateRfqDto

```typescript
interface CreateRfqDto {
  company_id: string;           // UUID, requerido
  partner_ids: string[];        // Array UUID, min: 1
  request_date?: string;        // YYYY-MM-DD, default: hoy
  deadline_date?: string;       // YYYY-MM-DD
  description?: string;
  notes?: string;
  lines: CreateRfqLineDto[];    // min: 1 linea
}

interface CreateRfqLineDto {
  product_id?: string;          // UUID, opcional
  description: string;          // min: 1
  quantity: number;             // positive
  uom_id: string;               // UUID, requerido
}
```

#### UpdateRfqDto

```typescript
interface UpdateRfqDto {
  partner_ids?: string[];       // Array UUID, min: 1 si se proporciona
  deadline_date?: string | null;
  description?: string | null;
  notes?: string | null;
}
```

#### UpdateRfqLineDto

```typescript
interface UpdateRfqLineDto {
  product_id?: string | null;
  description?: string;
  quantity?: number;
  uom_id?: string;
}
```

---

## ENDPOINTS

### Base Path: `/api/purchases`

#### Purchase Orders

| Metodo | Ruta | Roles Permitidos | Descripcion |
|--------|------|------------------|-------------|
| GET | `/` | admin, manager, warehouse, accountant, super_admin | Listar ordenes de compra |
| GET | `/:id` | admin, manager, warehouse, accountant, super_admin | Obtener orden por ID |
| POST | `/` | admin, manager, warehouse, super_admin | Crear orden de compra |
| PUT | `/:id` | admin, manager, warehouse, super_admin | Actualizar orden |
| POST | `/:id/confirm` | admin, manager, super_admin | Confirmar orden |
| POST | `/:id/cancel` | admin, manager, super_admin | Cancelar orden |
| DELETE | `/:id` | admin, super_admin | Eliminar orden (solo draft) |

#### RFQs (Request for Quotation)

| Metodo | Ruta | Roles Permitidos | Descripcion |
|--------|------|------------------|-------------|
| GET | `/rfqs` | admin, manager, warehouse, super_admin | Listar RFQs |
| GET | `/rfqs/:id` | admin, manager, warehouse, super_admin | Obtener RFQ por ID |
| POST | `/rfqs` | admin, manager, warehouse, super_admin | Crear RFQ |
| PUT | `/rfqs/:id` | admin, manager, warehouse, super_admin | Actualizar RFQ |
| DELETE | `/rfqs/:id` | admin, super_admin | Eliminar RFQ (solo draft) |

#### RFQ Lines

| Metodo | Ruta | Roles Permitidos | Descripcion |
|--------|------|------------------|-------------|
| POST | `/rfqs/:id/lines` | admin, manager, warehouse, super_admin | Agregar linea a RFQ |
| PUT | `/rfqs/:id/lines/:lineId` | admin, manager, warehouse, super_admin | Actualizar linea |
| DELETE | `/rfqs/:id/lines/:lineId` | admin, manager, warehouse, super_admin | Eliminar linea |

#### RFQ Workflow

| Metodo | Ruta | Roles Permitidos | Descripcion |
|--------|------|------------------|-------------|
| POST | `/rfqs/:id/send` | admin, manager, super_admin | Enviar RFQ a proveedores |
| POST | `/rfqs/:id/responded` | admin, manager, super_admin | Marcar como respondida |
| POST | `/rfqs/:id/accept` | admin, manager, super_admin | Aceptar RFQ |
| POST | `/rfqs/:id/reject` | admin, manager, super_admin | Rechazar RFQ |
| POST | `/rfqs/:id/cancel` | admin, manager, super_admin | Cancelar RFQ |

### Formato de Respuesta

#### Exito (Listado)

```json
{
  "success": true,
  "data": [...],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20,
    "totalPages": 5
  }
}
```

#### Exito (Entidad)

```json
{
  "success": true,
  "data": { ... },
  "message": "Operacion exitosa"
}
```

#### Error

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Descripcion del error",
    "details": [...]
  }
}
```

---

## TESTS

### Archivo: `__tests__/purchases.service.spec.ts`

#### Casos de Prueba - PurchasesService

| Suite | Test Case | Estado |
|-------|-----------|--------|
| findAll | should return paginated orders for a tenant | PASS |
| findAll | should enforce tenant isolation | PASS |
| findAll | should apply pagination correctly | PASS |
| findById | should return order with lines by id | PASS |
| findById | should throw NotFoundError when order does not exist | PASS |
| findById | should enforce tenant isolation | PASS |
| create | should create order with lines in transaction | PASS |
| create | should rollback transaction on error | PASS |
| update | should update draft order successfully | PASS |
| update | should throw ValidationError when updating confirmed order | PASS |
| delete | should delete draft order successfully | PASS |
| delete | should throw ValidationError when deleting confirmed order | PASS |
| confirm | should confirm draft order successfully | PASS |
| confirm | should throw ValidationError when confirming order without lines | PASS |
| confirm | should throw ValidationError when confirming non-draft order | PASS |
| cancel | should cancel draft order successfully | PASS |
| cancel | should throw ValidationError when cancelling done order | PASS |
| cancel | should throw ValidationError when cancelling already cancelled order | PASS |

### Archivo: `__tests__/rfqs.service.spec.ts`

#### Casos de Prueba - RfqsService

| Suite | Test Case | Estado |
|-------|-----------|--------|
| findAll | should return paginated RFQs for a tenant | PASS |
| findAll | should enforce tenant isolation | PASS |
| findAll | should apply pagination correctly | PASS |
| findAll | should filter by company_id, status, date range, search | PASS |
| findById | should return RFQ with lines by id | PASS |
| findById | should return partner names when partner_ids exist | PASS |
| findById | should throw NotFoundError when RFQ does not exist | PASS |
| create | should create RFQ with lines in transaction | PASS |
| create | should generate sequential RFQ name | PASS |
| create | should rollback transaction on error | PASS |
| create | should throw ValidationError when lines or partner_ids empty | PASS |
| update | should update draft RFQ successfully | PASS |
| update | should throw ValidationError when updating non-draft RFQ | PASS |
| delete | should delete draft RFQ successfully | PASS |
| delete | should throw ValidationError when deleting non-draft RFQ | PASS |
| addLine | should add line to draft RFQ successfully | PASS |
| updateLine | should update line in draft RFQ successfully | PASS |
| removeLine | should remove line from draft RFQ (min 1 required) | PASS |
| send | should send draft RFQ successfully | PASS |
| markResponded | should mark sent RFQ as responded | PASS |
| accept | should accept responded/sent RFQ | PASS |
| reject | should reject responded/sent RFQ | PASS |
| cancel | should cancel draft/sent/responded/rejected RFQ | PASS |
| Status Transitions | Validates all valid/invalid state transitions | PASS |
| Tenant Isolation | should not access RFQs from different tenant | PASS |
| Error Handling | should propagate database errors | PASS |

---

## DEPENDENCIAS

### Internas (Modulos del Sistema)

| Modulo | Uso |
|--------|-----|
| `config/database` | Conexion a BD (query, queryOne, getClient) |
| `shared/errors` | NotFoundError, ConflictError, ValidationError |
| `shared/middleware/auth.middleware` | authenticate, requireRoles, AuthenticatedRequest |

### Externas (npm)

| Paquete | Uso |
|---------|-----|
| `express` | Router, Request, Response, NextFunction |
| `zod` | Validacion de DTOs y schemas |
| `pg` | Cliente PostgreSQL (via config/database) |

### Tablas Relacionadas

| Schema | Tabla | Relacion |
|--------|-------|----------|
| auth | tenants | tenant_id (multi-tenant) |
| auth | companies | company_id |
| auth | users | created_by, updated_by, confirmed_by |
| core | partners | partner_id (proveedores) |
| core | currencies | currency_id |
| core | uom | uom_id (unidades de medida) |
| inventory | products | product_id |
| inventory | pickings | picking_id (recepciones) |
| financial | payment_terms | payment_term_id |
| financial | invoices | invoice_id |
| analytics | analytic_accounts | analytic_account_id |

---

## DIAGRAMAS

### Flujo de Estados - Purchase Order

```
                    +--------+
                    | draft  |
                    +---+----+
                        |
         +--------------+---------------+
         |              |               |
         v              v               v
     +-------+     +---------+    +-----------+
     | sent  | --> | confirmed|    | cancelled |
     +-------+     +----+----+    +-----------+
                        |
                        v
                   +--------+
                   |  done  |
                   +--------+
```

### Flujo de Estados - RFQ

```
                    +--------+
                    | draft  |
                    +---+----+
                        |
                        v
                    +-------+
                    | sent  |
                    +---+---+
                        |
         +--------------+---------------+
         |              |               |
         v              v               v
   +-----------+  +----------+   +-----------+
   | responded |  | accepted |   | rejected  |
   +-----+-----+  +----+-----+   +-----+-----+
         |             |               |
         v             |               |
   +-----------+       |               |
   | accepted  |<------+               |
   +-----------+                       |
         |                             |
         v                             v
   +-----------+                 +-----------+
   |   (end)   |                 | cancelled |
   +-----------+                 +-----------+
```

---

## NOTAS ADICIONALES

### Seguridad

1. **Multi-tenant:** Todas las consultas incluyen filtro por `tenant_id`
2. **RLS (Row Level Security):** Habilitado en todas las tablas del schema `purchase`
3. **Autenticacion:** Todos los endpoints requieren autenticacion JWT
4. **Autorizacion:** Roles especificos por endpoint

### Validaciones de Negocio

1. **Ordenes de Compra:**
   - Solo se pueden modificar/eliminar ordenes en estado `draft`
   - Minimo 1 linea requerida para confirmar
   - No se puede cancelar una orden `done`

2. **RFQs:**
   - Solo se pueden modificar/eliminar RFQs en estado `draft`
   - Minimo 1 proveedor (partner_id) requerido
   - Minimo 1 linea requerida
   - No se puede cancelar un RFQ `accepted`

### Transacciones

- Creacion de ordenes/RFQs con lineas utiliza transacciones (BEGIN/COMMIT/ROLLBACK)
- Rollback automatico en caso de error

---

## CHANGELOG

| Version | Fecha | Cambios |
|---------|-------|---------|
| 1.0.0 | 2026-01-10 | Version inicial - PurchasesService y RfqsService implementados |
