Ajustes de inventario

Los ajustes corrigen el stock de un SKU en una ubicación, con una razón de negocio auditable (merma, daño, corrección de conteo, etc.). Todos los endpoints de este recurso requieren el permiso inventory:adjust.

El ajuste aplica el cambio de stock AL CREARSE, no al aprobarse

A diferencia de lo que el nombre approve/reject sugiere, la creación de un ajuste ya modificó el stock. La aprobación es hoy, en la práctica, un no-op sobre el stock — solo cambia el campo status. El rechazo sí tiene efecto: revierte el stock. Lee la sección "Semántica de escritura" completa antes de construir tu flujo de aprobación.

Listar ajustes

Lista los ajustes del tenant, con filtros opcionales

pagenumber

Página, 1-indexada. Default: 1.

limitnumber

Elementos por página. Default: 20.

statusstring

Filtro por estado: pending_review, approved, rejected.

locationIdstring

Filtro por ubicación.

skustring

Filtro por SKU.

reasonstring

Filtro por razón (ver enum abajo).

200
{
  "adjustments": [
    {
      "_id": "670a1b2c3d4e5f6a7b8c9d0e",
      "locationId": "loc_abc123",
      "sku": "CAM-ROJO-M",
      "oldQuantity": 42,
      "newQuantity": 40,
      "adjustmentQuantity": -2,
      "reason": "damage",
      "status": "pending_review"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 6, "pages": 1 }
}

Consultar un ajuste

Consulta un ajuste por ID

idstringrequired

ID del ajuste.

200
{ "_id": "670a1b2c3d4e5f6a7b8c9d0e", "sku": "CAM-ROJO-M", "status": "pending_review" }
404
{ "code": "not-found" }

Resumen de ajustes

Totales agregados de ajustes en un rango de fechas

startDatestring

Fecha ISO 8601 de inicio.

endDatestring

Fecha ISO 8601 de fin.

200
{
  "totalAdjustments": 34,
  "positiveAdjustments": 12,
  "negativeAdjustments": 22,
  "totalQuantityAdjusted": -187,
  "totalCostImpact": 0
}

Crear un ajuste

Crea un ajuste y aplica el cambio de stock inmediatamente

locationIdstringrequired

Ubicación afectada.

skustringrequired

SKU a ajustar.

newQuantitynumberrequired

Nueva cantidad absoluta (no un delta).

reasonstringrequired

Una de: count_correction, damage, theft, expired, found, return, supplier_error, restock, shrinkage, sample, other.

notesstring

Nota libre.

productSkustring

SKU del producto padre, si aplica (ver resolución automática abajo).

{
  "locationId": "loc_abc123",
  "sku": "CAM-ROJO-M",
  "newQuantity": 40,
  "reason": "damage",
  "notes": "2 piezas dañadas en almacén"
}
201
{
  "_id": "670a1b2c3d4e5f6a7b8c9d0e",
  "locationId": "loc_abc123",
  "sku": "CAM-ROJO-M",
  "oldQuantity": 42,
  "newQuantity": 40,
  "adjustmentQuantity": -2,
  "reason": "damage",
  "status": "pending_review"
}
400
{ "code": "bad-request/invalid-reason" }

Permiso requerido: inventory:adjust

reason acepta 11 valores — 'sale' NO es uno de ellos

El endpoint público valida reason contra esta lista exacta: count_correction, damage, theft, expired, found, return, supplier_error, restock, shrinkage, sample, other. El valor sale (venta) existe en el modelo de datos pero está reservado para el decremento automático que genera el sistema al procesar una orden — pasar reason: "sale" a este endpoint responde 400 bad-request/invalid-reason.

Puntos clave sobre la creación:

  • oldQuantity no lo defines tú. El servidor siempre lee el stock actual del registro de inventario en el momento de crear el ajuste, ignorando cualquier oldQuantity que envíes en el body.
  • adjustmentQuantity se calcula en el servidor como newQuantity − oldQuantity.
  • Si omites productSku, el servidor lo resuelve en este orden: (1) el productSku ya existente en el registro de inventario de ese SKU/ubicación, (2) una búsqueda en el catálogo de productos, (3) usa el sku tal cual como último recurso.

Semántica de escritura — "aplicar primero, revisar después"

Este es el patrón de negocio más importante del recurso:

  1. La creación (POST /inventory/adjustments) aplica el cambio de stock de inmediato. El registro nace con status: 'pending_review', pero el stock ya fue modificado en ese momento — no cuando alguien lo aprueba. Este POST dispara el evento Inventory Updated (y una posible Low Stock Alert), porque internamente usa la misma ruta de escritura que PUT de stock.
  2. approve sobre un ajuste pending_review (el flujo normal hoy) no vuelve a tocar el stock. Solo cambia status → approved y estampa los campos de auditoría (approvedBy, approvedAt). El stock ya se movió en el paso 1.
  3. reject sí tiene efecto sobre el stock: lo revierte a oldQuantity (disparando otro evento Inventory Updated por la reversión), y además crea automáticamente un segundo registro de ajuste: reason: 'count_correction', sourceType: 'reversal', sourceId apuntando al ajuste original, adjustedBy: 'system', ya en estado approved.

Rechazar un ajuste crea DOS registros, no uno

Si listas ajustes esperando ver únicamente el que rechazaste, verás dos: el original (ahora status: 'rejected') y un ajuste sintético de reversión generado por el sistema. Ambos cuentan en GET /inventory/adjustments y en /summary.

Aprobar

Marca el ajuste como aprobado (no mueve stock — ver arriba)

idstringrequired

ID del ajuste.

No requiere cuerpo — el handler no lee event.body.

{}
200
{ "_id": "670a1b2c3d4e5f6a7b8c9d0e", "status": "approved" }
404
{ "code": "not-found" }

Rechazar (revierte el stock)

Revierte el stock a oldQuantity y crea un ajuste de reversión

idstringrequired

ID del ajuste.

rejectionReasonstringrequired

Motivo del rechazo (o el campo alterno `reason`).

{ "rejectionReason": "Conteo físico no coincide con el reporte" }
200
{ "_id": "670a1b2c3d4e5f6a7b8c9d0e", "status": "rejected" }
400
{ "code": "bad-request/missing-reason" }
404
{ "code": "not-found" }

Ejemplo — crear y consultar el impacto

curl -X POST 'https://api.fenicia.io/inventory/adjustments' \
  -H 'Authorization: Bearer fkapi_tu_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "locationId": "loc_abc123",
    "sku": "CAM-ROJO-M",
    "newQuantity": 40,
    "reason": "damage",
    "notes": "2 piezas dañadas en almacén"
  }'

Errores

CódigoStatusDescripción
bad-request/missing-tenant401No se pudo resolver el tenant de la API key.
bad-request/missing-location400Falta locationId.
bad-request/missing-sku400Falta sku.
bad-request/missing-quantity400Falta newQuantity.
bad-request/missing-reason400Falta reason (crear) o rejectionReason/reason (rechazar).
bad-request/invalid-reason400reason no es uno de los 11 valores aceptados.
not-found404No existe un ajuste con ese ID.
internal-error500Error interno — incluye el caso en que newQuantity resultaría en stock negativo (ver stock negativo).

Siguientes pasos