Inventario

La API de inventario permite gestionar el stock de productos en múltiples ubicaciones. Cannahub utiliza el sistema de inventario multi-ubicación de Medusa para trackear stock por club y calcular totales en gramos para productos de flor.


El modelo de Inventario

ProductLocationStock

Representa el stock de una variante en una ubicación específica.

  • Name
    productId
    Type
    string
    Description

    ID del producto padre.

  • Name
    variantId
    Type
    string
    Description

    ID de la variante específica.

  • Name
    inventoryItemId
    Type
    string
    Description

    ID del item de inventario en Medusa.

  • Name
    locationId
    Type
    string
    Description

    ID de la ubicación de stock.

  • Name
    locationName
    Type
    string
    Description

    Nombre legible de la ubicación.

  • Name
    quantity
    Type
    number
    Description

    Unidades físicas en stock.

  • Name
    reservedQuantity
    Type
    number
    Description

    Unidades reservadas por pedidos pendientes.

  • Name
    availableQuantity
    Type
    number
    Description

    Unidades disponibles para venta (quantity - reserved).

  • Name
    incomingQuantity
    Type
    number
    Description

    Unidades en tránsito/esperadas.

Interface ProductLocationStock

interface ProductLocationStock {
  productId: string
  variantId: string
  inventoryItemId: string
  locationId: string
  locationName: string
  quantity: number
  reservedQuantity: number
  availableQuantity: number
  incomingQuantity: number
}

Ejemplo

{
  "productId": "prod_01HQ8BD5G",
  "variantId": "variant_01HQ_BD_7G",
  "inventoryItemId": "iitem_01HQ_BD_7G",
  "locationId": "sloc_high_up",
  "locationName": "High Up - Dispensario",
  "quantity": 50,
  "reservedQuantity": 5,
  "availableQuantity": 45,
  "incomingQuantity": 0
}

InventoryLevel

Modelo raw de Medusa para niveles de inventario.

  • Name
    id
    Type
    string
    Description

    ID único del nivel de inventario.

  • Name
    inventoryItemId
    Type
    string
    Description

    ID del item de inventario asociado.

  • Name
    locationId
    Type
    string
    Description

    ID de la ubicación.

  • Name
    stockedQuantity
    Type
    number
    Description

    Cantidad total en stock.

  • Name
    reservedQuantity
    Type
    number
    Description

    Cantidad reservada.

  • Name
    incomingQuantity
    Type
    number
    Description

    Cantidad entrante.

Interface InventoryLevel

interface InventoryLevel {
  id: string
  inventoryItemId: string
  locationId: string
  stockedQuantity: number
  reservedQuantity: number
  incomingQuantity: number
  availableQuantity: number
  location?: StockLocation
  metadata?: Record<string, unknown>
  createdAt?: string
  updatedAt?: string
}

Multiplicador de Inventario

Para productos de flor, el inventario se calcula multiplicando las unidades por el peso de la variante.

Lógica del Multiplicador

inventoryTotal = quantity × weight

Ejemplo

VarianteUnidadesPesoTotal (gramos)
Blue Dream 1g1001g100g
Blue Dream 3.5g503.5g175g
Blue Dream 7g207g140g
Total170-415g

Cálculo de inventario

function calculateInventoryTotal(
  variant: ProductVariant,
  inventoryQuantity: number
): number {
  // Obtener peso de metadata
  const weight = variant.metadata?.weight
    ?? variant.metadata?.quantity
    ?? 1

  // Multiplicar para obtener total en gramos
  return inventoryQuantity * weight
}

// Uso
const variant = {
  id: 'variant_bd_7g',
  name: 'Blue Dream 7g',
  metadata: { weight: 7 }
}

const total = calculateInventoryTotal(variant, 20)
// total = 140 (gramos)

GET/api/inventory

Listar inventario

Este endpoint lista los items de inventario con sus niveles por ubicación.

Parámetros de Consulta

  • Name
    locationId
    Type
    string
    Description

    Filtrar por ubicación de stock.

  • Name
    search
    Type
    string
    Description

    Filtrar por SKU o titulo.

  • Name
    limit
    Type
    number
    Description

    Límite de resultados (default: 20).

  • Name
    offset
    Type
    number
    Description

    Offset para paginación.

Request

GET
/api/inventory
curl -G https://app.cannahub.tech/api/inventory \
  -H "Authorization: Bearer {token}" \
  -d locationId=sloc_high_up

Response

{
  "items": [
    {
      "id": "iitem_01HQ_BD_7G",
      "sku": "BD-7G-001",
      "title": "Blue Dream 7g",
      "stockedQuantity": 50,
      "reservedQuantity": 5,
      "availableQuantity": 45,
      "locationLevels": [
        {
          "id": "ilevel_01HQ",
          "locationId": "sloc_high_up",
          "stockedQuantity": 50,
          "reservedQuantity": 5,
          "incomingQuantity": 0,
          "availableQuantity": 45
        }
      ]
    }
  ],
  "count": 45,
  "offset": 0,
  "limit": 20
}

GET/api/inventory/:id

Obtener inventario de variante

Obtiene el detalle de un item de inventario específico con todos sus niveles.

Request

GET
/api/inventory/:id
curl https://yourapp.com/api/inventory/iitem_01HQ_BD_7G \
  -H "Authorization: Bearer {admin_token}"

Response

{
  "item": {
    "id": "iitem_01HQ_BD_7G",
    "sku": "BD-7G-001",
    "title": "Blue Dream 7g",
    "requiresShipping": true,
    "stockedQuantity": 250,
    "reservedQuantity": 5,
    "availableQuantity": 245,
    "locationLevels": [
      {
        "id": "ilevel_01HQ",
        "locationId": "sloc_high_up",
        "stockedQuantity": 50,
        "reservedQuantity": 5,
        "incomingQuantity": 0,
        "availableQuantity": 45
      },
      {
        "id": "ilevel_02HQ",
        "locationId": "sloc_deposito",
        "stockedQuantity": 200,
        "reservedQuantity": 0,
        "incomingQuantity": 50,
        "availableQuantity": 200
      }
    ]
  }
}

POST/api/inventory/:id/levels/:locationId

Actualizar nivel de inventario

Actualiza el nivel de stock en una ubicación específica.

Body

  • Name
    stockedQuantity
    Type
    number
    Description

    Nueva cantidad en stock.

  • Name
    incomingQuantity
    Type
    number
    Description

    Cantidad entrante esperada.

Request

PATCH
/api/inventory/levels
curl -X PATCH https://app.cannahub.tech/api/inventory/levels \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "inventoryItemId": "iitem_01HQ_BD_7G",
    "locationId": "sloc_high_up",
    "stockedQuantity": 60
  }'

Response

{
  "level": {
    "id": "ilevel_01HQ",
    "locationId": "sloc_high_up",
    "stockedQuantity": 60,
    "reservedQuantity": 5,
    "incomingQuantity": 0,
    "availableQuantity": 55
  }
}

POST/api/inventory/adjust

Ajuste de inventario (incremento/decremento)

Para ajustes de inventario por ventas, devoluciones, o correcciones.

Reservaciones

Las reservaciones son creadas automáticamente cuando se agrega un item al carrito y se liberan si el carrito expira o se cancela.

Flujo de reservación

// 1. Cliente agrega item al carrito
// → Se crea reservación automática
// reserved_quantity += ordered_quantity

// 2. Cliente completa la orden
// → Se confirma la reservación
// stocked_quantity -= ordered_quantity
// reserved_quantity -= ordered_quantity

// 3. Si el carrito expira/cancela
// → Se libera la reservación
// reserved_quantity -= ordered_quantity

Stock Locations (Filiales)

Las ubicaciones de stock (filiales) son fundamentales para el inventario multi-ubicacion. Para documentacion completa sobre gestion de filiales, consulta la API de Filiales.


GET/api/products/:id/with-inventory

BFF Route: Producto con Inventario

Endpoint BFF que retorna un producto con inventario agregado de todas las ubicaciones en una sola llamada.

Beneficio

Reduce el problema N+1 de consultar inventario por cada variante.

Request

GET
/api/products/:id/with-inventory
curl https://app.cannahub.tech/api/products/prod_01HQ8BD5G/with-inventory \
  -H "Authorization: Bearer {token}"

Response

{
  "product": {
    "id": "prod_01HQ8BD5G",
    "name": "Blue Dream Indoor",
    "variants": [
      {
        "id": "variant_bd_1g",
        "name": "Blue Dream 1g",
        "weight": 1,
        "price": 350
      },
      {
        "id": "variant_bd_7g",
        "name": "Blue Dream 7g",
        "weight": 7,
        "price": 2100
      }
    ]
  },
  "inventory": {
    "variant_bd_1g": {
      "quantity": 100,
      "available": 95,
      "reserved": 5,
      "totalGrams": 100,
      "byLocation": [
        {
          "locationId": "sloc_high_up",
          "locationName": "High Up",
          "quantity": 100,
          "available": 95
        }
      ]
    },
    "variant_bd_7g": {
      "quantity": 50,
      "available": 45,
      "reserved": 5,
      "totalGrams": 350,
      "byLocation": [
        {
          "locationId": "sloc_high_up",
          "locationName": "High Up",
          "quantity": 50,
          "available": 45
        }
      ]
    }
  },
  "totalInventoryGrams": 450
}

POST/api/inventory/batch-update

BFF Route: Actualización Batch

Permite actualizar inventario de múltiples variantes en una sola llamada. Esta ruta reduce N llamadas individuales a 1 sola llamada.

Problema que resuelve

Flujo actual (N calls):
Para 10 variantes:
1. POST /admin/inventory-items/:id1/location-levels/:loc
2. POST /admin/inventory-items/:id2/location-levels/:loc
3. POST /admin/inventory-items/:id3/location-levels/:loc
...10 llamadas secuenciales

Body

  • Name
    updates
    Type
    array
    Description

    Array de actualizaciones a aplicar.

    • Name
      variantId
      Type
      string
      Description

      ID de la variante a actualizar.

    • Name
      locationId
      Type
      string
      Description

      ID de la ubicación de stock.

    • Name
      quantity
      Type
      number
      Description

      Nueva cantidad en stock.

    • Name
      adjustment
      Type
      number
      Description

      Ajuste relativo (+/-) en lugar de cantidad absoluta.

    • Name
      reason
      Type
      string
      Description

      Razón del ajuste para auditoría.

  • Name
    validateStock
    Type
    boolean
    Description

    Si es true, valida que haya stock suficiente para decrementos.

Response

  • Name
    success
    Type
    boolean
    Description

    Si todas las actualizaciones fueron exitosas.

  • Name
    updated
    Type
    number
    Description

    Número de items actualizados exitosamente.

  • Name
    failed
    Type
    number
    Description

    Número de items que fallaron.

  • Name
    results
    Type
    array
    Description

    Detalle de cada actualización.

Request

POST
/api/inventory/batch-update
curl -X POST https://app.cannahub.tech/api/inventory/batch-update \
  -H "Authorization: Bearer {admin_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "updates": [
      {
        "variantId": "variant_bd_1g",
        "locationId": "sloc_high_up",
        "quantity": 150
      },
      {
        "variantId": "variant_bd_7g",
        "locationId": "sloc_high_up",
        "quantity": 75
      }
    ]
  }'

Response

{
  "success": true,
  "updated": 2,
  "failed": 0,
  "results": [
    {
      "variantId": "variant_bd_1g",
      "locationId": "sloc_high_up",
      "previousQuantity": 100,
      "newQuantity": 150,
      "totalGrams": 150,
      "status": "success"
    },
    {
      "variantId": "variant_bd_7g",
      "locationId": "sloc_high_up",
      "previousQuantity": 50,
      "newQuantity": 75,
      "totalGrams": 525,
      "status": "success"
    }
  ],
  "summary": {
    "totalUnitsUpdated": 75,
    "totalGramsUpdated": 525
  }
}

Response con error parcial

{
  "success": false,
  "updated": 1,
  "failed": 1,
  "results": [
    {
      "variantId": "variant_bd_1g",
      "status": "success",
      "previousQuantity": 100,
      "newQuantity": 150
    },
    {
      "variantId": "variant_invalid",
      "status": "error",
      "error": "Variant not found"
    }
  ]
}

Comparación de rendimiento

MétricaSin BFF (10 items)Con BFFMejora
Llamadas API101-90%
Latencia típica~2000ms~300ms-85%
Manejo de erroresManual por itemCentralizadoSimplificado
AuditoríaN registros separados1 batch logTrazabilidad

POST/api/stock-locations/setup

BFF Route: Stock Location Setup

Esta ruta crea una ubicación de stock completa con todo su fulfillment chain en una sola llamada. Normalmente esto requiere 7+ llamadas secuenciales a Medusa.

Problema que resuelve

Flujo actual (7+ calls secuenciales):
1. POST /admin/stock-locations           → Crear ubicación
2. POST /admin/fulfillment-sets          → Crear fulfillment set
3. POST /admin/service-zones             → Crear service zone
4. POST /admin/geo-zones                 → Crear geo zone
5. POST /admin/shipping-options          → Crear opción de envío
6. POST /admin/shipping-options          → Crear opción de pickup
7. POST /admin/stock-locations/:id/sales-channels
                                         → Asociar sales channel

Request Body

  • Name
    name
    Type
    string
    Description

    Nombre de la ubicación.

  • Name
    address
    Type
    object
    Description

    Dirección física de la ubicación.

    • Name
      address_1
      Type
      string
      Description

      Dirección principal.

    • Name
      city
      Type
      string
      Description

      Ciudad.

    • Name
      province
      Type
      string
      Description

      Provincia/Estado.

    • Name
      postal_code
      Type
      string
      Description

      Código postal.

    • Name
      country_code
      Type
      string
      Description

      Código de país (ej: "AR").

  • Name
    enableShipping
    Type
    boolean
    Description

    Habilitar envíos desde esta ubicación (default: false).

  • Name
    enablePickup
    Type
    boolean
    Description

    Habilitar retiro en esta ubicación (default: true).

  • Name
    salesChannelId
    Type
    string
    Description

    ID del canal de ventas a asociar.

  • Name
    metadata
    Type
    object
    Description

    Datos adicionales (club, tipo, horarios, etc).

Response

  • Name
    stockLocation
    Type
    StockLocation
    Description

    La ubicación creada.

  • Name
    fulfillmentSet
    Type
    FulfillmentSet
    Description

    El fulfillment set asociado.

  • Name
    serviceZone
    Type
    ServiceZone
    Description

    La service zone creada.

  • Name
    shippingOptions
    Type
    array
    Description

    Las opciones de envío configuradas.

Request

POST
/api/stock-locations/setup
curl -X POST https://app.cannahub.tech/api/stock-locations/setup \
  -H "Authorization: Bearer {admin_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "High Up - Dispensario Palermo",
    "address": {
      "address_1": "Av. Santa Fe 3200",
      "city": "Buenos Aires",
      "province": "CABA",
      "postal_code": "C1425",
      "country_code": "AR"
    },
    "enableShipping": false,
    "enablePickup": true,
    "salesChannelId": "sc_01KBCZXJCPVWX8REW8880R45SW",
    "metadata": {
      "club": "high-up",
      "type": "dispensary",
      "schedule": {
        "monday": "10:00-20:00",
        "tuesday": "10:00-20:00",
        "wednesday": "10:00-20:00",
        "thursday": "10:00-20:00",
        "friday": "10:00-20:00",
        "saturday": "12:00-18:00",
        "sunday": "closed"
      }
    }
  }'

Response

{
  "stockLocation": {
    "id": "sloc_01KDPALERMO",
    "name": "High Up - Dispensario Palermo",
    "address": {
      "address_1": "Av. Santa Fe 3200",
      "city": "Buenos Aires",
      "province": "CABA",
      "postal_code": "C1425",
      "country_code": "AR"
    },
    "metadata": {
      "club": "high-up",
      "type": "dispensary"
    }
  },
  "fulfillmentSet": {
    "id": "fset_01KDPALERMO",
    "name": "High Up - Dispensario Palermo Fulfillment",
    "type": "pickup"
  },
  "serviceZone": {
    "id": "szone_01KDPALERMO",
    "name": "High Up Palermo Zone",
    "geo_zones": [
      {
        "id": "gzone_01KDPALERMO",
        "type": "country",
        "country_code": "AR"
      }
    ]
  },
  "shippingOptions": [
    {
      "id": "so_01KDPALERMO_PICKUP",
      "name": "Retiro en Dispensario Palermo",
      "price_type": "flat",
      "amount": 0,
      "is_return": false,
      "metadata": {
        "type": "pickup",
        "locationId": "sloc_01KDPALERMO"
      }
    }
  ],
  "summary": {
    "totalApiCalls": 7,
    "savedCalls": 6,
    "executionTimeMs": 450
  }
}

Flujo interno del BFF

Loading diagram...

Comparación de rendimiento

MétricaSin BFFCon BFFMejora
Llamadas API7+1-86%
Latencia típica~1400ms~450ms-68%
Complejidad clienteAlta (secuencial)Baja (1 request)Simplificado
Rollback en errorManualAutomáticoTransaccional

Mapeo Medusa

La API BFF transforma las respuestas de Medusa de snake_case a camelCase:

UI (camelCase)Medusa (snake_case)
inventoryItemIdinventory_item_id
locationIdlocation_id
stockedQuantitystocked_quantity
reservedQuantityreserved_quantity
incomingQuantityincoming_quantity
availableQuantityavailable_quantity
requiresShippingrequires_shipping
originCountryorigin_country
hsCodehs_code
midCodemid_code
createdAtcreated_at
updatedAtupdated_at
locationLevelslocation_levels

Diagrama de Flujo de Inventario

Loading diagram...

Origen de Cosecha (Seed-to-Sale)

Al editar stock, los modales permiten seleccionar una cosecha como origen para trazabilidad. Esto habilita el flujo completo de seed-to-sale.

Funcionalidad

  • Selector de Cosecha: Combobox que filtra cosechas por la genética del producto
  • Solo STOCKED: Solo muestra cosechas con estado STOCKED (listas para dispensar)
  • Peso Disponible: Muestra los gramos disponibles de cada cosecha
  • Warning de Exceso: Alerta si el stock a agregar excede el peso disponible de la cosecha

Modales Actualizados

  1. EditProductStockModal - Stock por ubicación
  2. EditStockDialog - Stock multi-ubicación con acordeón
  3. EditStockModal - Nivel de inventario individual

HarvestsCombobox Props

type Props = {
  value: string | undefined
  setValue: (value: string | undefined) => void
  strainId?: string  // Filtra por genética
  setHarvest?: (harvest: Harvest | undefined) => void
  disabled?: boolean
  placeholder?: string
}

StockUpdateWithSource Type

// src/api/custom/stock.ts
type StockUpdateWithSource = {
  inventoryItemId: string
  locationId: string
  quantity: number
  sourceHarvestId?: string // Trazabilidad seed-to-sale
}

Uso en Modal

// Estado para selección de cosecha
const [selectedHarvestId, setSelectedHarvestId] =
  useState<string | undefined>()
const [selectedHarvest, setSelectedHarvest] =
  useState<Harvest | undefined>()

// Warning cuando stock > disponible
const harvestWarning = useMemo(() => {
  if (!selectedHarvest) return null

  const available = selectedHarvest.trimmedWeight || 0
  const stockChange = newQty - originalQty

  if (stockChange > available) {
    return { change: stockChange, available }
  }
  return null
}, [selectedHarvest, formValues])

Flujo de Trazabilidad

Loading diagram...

Traducciones Agregadas

ClaveESENDE
sourceHarvestCosecha origenSource HarvestQuell-Ernte
selectHarvestSeleccionar cosecha a descontarSelect harvest to discount fromErnte zum Abzug auswählen
availableFromHarvestDisponible de cosechaAvailable from harvestVerfügbar von Ernte
harvestExceededWarningLa cantidad supera el peso disponibleStock quantity exceeds harvest availableBestandsmenge übersteigt verfügbares Erntegewicht

Próximos Pasos

Was this page helpful?