Some checks failed
ERP Core CI / Backend Lint (push) Has been cancelled
ERP Core CI / Backend Unit Tests (push) Has been cancelled
ERP Core CI / Backend Integration Tests (push) Has been cancelled
ERP Core CI / Frontend Lint (push) Has been cancelled
ERP Core CI / Frontend Unit Tests (push) Has been cancelled
ERP Core CI / Frontend E2E Tests (push) Has been cancelled
ERP Core CI / Database DDL Validation (push) Has been cancelled
ERP Core CI / Backend Build (push) Has been cancelled
ERP Core CI / Frontend Build (push) Has been cancelled
ERP Core CI / CI Success (push) Has been cancelled
Performance Tests / Lighthouse CI (push) Has been cancelled
Performance Tests / Bundle Size Analysis (push) Has been cancelled
Performance Tests / k6 Load Tests (push) Has been cancelled
Performance Tests / Performance Summary (push) Has been cancelled
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones en modulos CRM y OpenAPI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
21 KiB
21 KiB
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)
type OrderStatus = 'draft' | 'sent' | 'confirmed' | 'done' | 'cancelled';
Filtros Disponibles (PurchaseOrderFilters)
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)
type RfqStatus = 'draft' | 'sent' | 'responded' | 'accepted' | 'rejected' | 'cancelled';
Filtros Disponibles (RfqFilters)
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_ididx_purchase_orders_company_ididx_purchase_orders_partner_ididx_purchase_orders_nameidx_purchase_orders_statusidx_purchase_orders_order_dateidx_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 > 0chk_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
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
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
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
interface UpdateRfqDto {
partner_ids?: string[]; // Array UUID, min: 1 si se proporciona
deadline_date?: string | null;
description?: string | null;
notes?: string | null;
}
UpdateRfqLineDto
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)
{
"success": true,
"data": [...],
"meta": {
"total": 100,
"page": 1,
"limit": 20,
"totalPages": 5
}
}
Exito (Entidad)
{
"success": true,
"data": { ... },
"message": "Operacion exitosa"
}
Error
{
"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
- Multi-tenant: Todas las consultas incluyen filtro por
tenant_id - RLS (Row Level Security): Habilitado en todas las tablas del schema
purchase - Autenticacion: Todos los endpoints requieren autenticacion JWT
- Autorizacion: Roles especificos por endpoint
Validaciones de Negocio
-
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
- Solo se pueden modificar/eliminar ordenes en estado
-
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
- Solo se pueden modificar/eliminar RFQs en estado
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 |