Saltar a contenido

Guía: Reserva de stock para e-commerce (ATP)

Cómo funciona el sistema de reservas de OpenTPV y cómo usar sus endpoints desde una tienda web o integración. Audiencia: integradores y equipo OpenTPV.

La idea en 30 segundos

Cuando un cliente web agrega productos al carro o entra a pagar, la tienda reserva esas unidades en OpenTPV. La reserva es una promesa con vencimiento (TTL): descuenta disponibilidad, no stock físico. El stock físico recién baja cuando se emite el documento tributario (boleta/factura), que además consume la reserva. Si el cliente abandona, la reserva vence sola y las unidades vuelven a estar disponibles.

Dos números que no hay que confundir:

Concepto Fórmula Quién lo usa
ATP físico stock bodega − reservas activas ERP / TPV mostrador
ATP publicable ATP físico − colchón lo que la tienda debe publicar

El colchón (buffer) es un margen configurable que no se publica, para absorber el desfase con las ventas de mostrador. Solo aplica cuando la tienda comparte bodega con el mostrador (bodegaEcommerce = 0); con bodega dedicada no hay desfase que absorber.

Flujo completo

1. Tienda arranca      → GET  /reservas/configuracion      (¿cuándo reservar? ¿TTLs?)
2. Catálogo / carro    → GET  /reservas/disponibilidad     (publicar atpPublicable)
3. Carro o checkout    → POST /reservas                    (token = id del carro)
   … cliente navega    → PATCH /reservas/{token}/renovar   (heartbeat)
   … cliente abandona  → DELETE /reservas/{token}          (o vence sola por TTL)
4. Pedido confirmado   → POST /documento/notaventa         (con tokenReserva)
                          la reserva pasa a colgar del pedido, con TTL largo
5. Emisión del DTE     → POST /documento/notaventa/{folio}/documentar
                          AQUÍ baja el stock físico y se consume la reserva
   … o cancelación     → POST /documento/notaventa/{folio}/anular (libera la reserva)

Reglas de oro:

  • Una reserva nunca mueve stock físico. Solo la emisión del documento lo hace.
  • El token es el identificador del carro (máx. 64 caracteres, lo genera la tienda). Reenviar POST /reservas con el mismo token reemplaza las cantidades: es la forma correcta de sincronizar el carro (idempotente).
  • La reserva es todo-o-nada: si falta stock de una línea, no se reserva ninguna y la respuesta 409 detalla los faltantes.
  • Tras confirmar el pedido (paso 4), dejar de renovar el token: la renovación aplica el TTL corto de carro y acortaría la protección del pedido confirmado.

Autenticación

Todos los endpoints aceptan cualquiera de las dos formas:

  • Authorization: Bearer <JWT> (token de empresa)
  • Headers legacy rut / X-Company-Token

Sin empresa resoluble → 401.

Endpoints de reservas

GET /api/Reservas/configuracion

Llamar al iniciar la integración. Devuelve la política del tenant:

{
  "momentoReserva": "CHECKOUT",      // CHECKOUT | CARRO | NINGUNO
  "ttlWebMinutos": 15,
  "ttlNotaVentaMinutos": 1440,
  "ttlMaximoMinutos": 4320,
  "bodegaEcommerce": 0,              // 0 = comparte con mostrador
  "buffer": 0,
  "bufferEsPorcentaje": false
}

momentoReserva indica cuándo debe reservar la tienda (al agregar al carro, al entrar al checkout, o nunca). Es una instrucción para el integrador: la API no lo impone, así que respetarlo es responsabilidad de la tienda.

GET /api/Reservas/disponibilidad?codigos=A001,B002&bodega=1

Disponibilidad por producto. codigos es CSV obligatorio; bodega (1–20) se ignora si el tenant tiene bodega dedicada. Códigos inexistentes simplemente no aparecen.

[
  { "codigo": "A001", "bodega": 1, "stockFisico": 10, "reservado": 2,
    "buffer": 1, "atpFisico": 8, "atpPublicable": 7 }
]

Publica siempre atpPublicable. Errores: 400 (sin códigos / bodega fuera de rango).

POST /api/Reservas

Crea o reemplaza la reserva del carro. Llamar según momentoReserva.

{
  "token": "carro-web-8f3a2b",       // requerido, ≤64 chars, id del carro
  "bodega": 1,                       // opcional, default 1; ignorado con bodega dedicada
  "ttlMinutos": 15,                  // opcional; acotado por ttlMaximoMinutos
  "origen": "ECOMMERCE",             // opcional
  "usuario": "cliente@correo.cl",    // opcional, trazabilidad
  "lineas": [
    { "codigo": "A001", "cantidad": 2 },
    { "codigo": "B002", "cantidad": 1 }
  ]
}
  • 200: { exito, token, expiraEn, segundosRestantes, lineas[] }
  • 409 stock insuficiente (no se reservó nada):
{ "exito": false, "faltantes": [
    { "codigo": "A001", "solicitado": 2, "disponible": 1, "stockFisico": 3 } ] }

Ante 409, mostrar los faltantes al cliente y reintentar con cantidades menores.

PATCH /api/Reservas/{token}/renovar?ttlMinutos=15

Heartbeat mientras el cliente sigue activo. 404 si el carro ya no tiene reservas activas (venció o se consumió) → volver a reservar con POST /reservas. No renovar después de confirmar el pedido.

GET /api/Reservas/{token}

Estado actual del carro (lista de líneas con estado, expiraEn, idNotaVenta…). Lista vacía si no existe. Útil para reconstruir el carro al volver el cliente.

DELETE /api/Reservas/{token} · DELETE /api/Reservas/{token}/lineas/{codigo}

Liberan todo el carro o una línea. Idempotentes (devuelven liberadas: 0 si no había nada). Llamar al vaciar el carro; si no se llama, el TTL limpia solo.

GET /api/Reservas/incumplidas?soloNoAcusadas=true · POST /api/Reservas/incumplidas/acusar

Cuando el mostrador vende unidades que estaban reservadas (quiebre), la reserva queda INCUMPLIDA. La tienda hace polling de incumplidas, avisa al cliente antes de cobrar, y confirma con acusar ({ "ids": [1,2,3] }) para no reprocesarlas.

Del carro al documento

POST /api/Documento/notaventa (con tokenReserva)

Al confirmar el pedido, crear la nota de venta pasando el token:

{
  "tokenReserva": "carro-web-8f3a2b",
  "origen": "ECOMMERCE",
  "cliente": { "...": "..." },
  "items": [ { "codigo": "A001", "cantidad": 2, "...": "..." } ]
}

La reserva pasa a colgar de la nota de venta y toma el TTL largo (ttlNotaVentaMinutos, default 24 h). No mueve stock. Si el carro ya venció, la respuesta incluye advertencia: validar disponibilidad de nuevo antes de cobrar.

POST /api/Documento/notaventa/{folio}/documentar

Emite el DTE (tipoDte: 39 boleta, 41 exenta, 33 factura, 34 exenta, 52 guía). Es el único punto donde baja el stock físico (solo si el tenant activó apiDescuentaInventario) y donde la reserva se marca CONSUMIDA. Errores 409: ya documentada, en proceso o anulada.

Campos opcionales del request:

  • codigoClienteReceptor (33/34/52): receptor distinto al cliente de la NV (caso multi-razón-social; ver abajo).
  • traslado (solo 52): datos del traslado según la Res. Ex. SII N°154/2025 (vigente desde el 01-11-2026 por Res. N°52/2026):
{
  "tipoDte": 52,
  "codigoTerminal": "CAJA01",
  "codigoClienteReceptor": 8021,
  "traslado": {
    "indTraslado": 1,                  // 1 venta · 2 ventas por efectuar · 3 consignación
                                       // 4 entrega gratuita · 5 traslado interno · 6 otro no venta
                                       // 7 venta exportación · 8 traslado exportación
    "direccionDestino": "Av. Siempre Viva 742",
    "comunaDestino": "Ñuñoa",
    "fechaSalida": "2026-08-10",       // yyyy-MM-dd
    "horaSalida": "16:30",
    "fechaLlegada": "2026-08-11",      // obligatoria si el traslado dura más de un día
    "rutTransportista": "77111222-3",
    "rutChofer": "12345678-9",
    "nombreChofer": "Juan Pérez",
    "patente": "ABCD12",
    "patenteCarro": ""
  }
}

Qué hace la API si se omite el bloque traslado (defaults seguros que cumplen los obligatorios de la resolución): indTraslado = 1 (venta), destino = domicilio del cliente de la nota de venta, leyenda con la fecha/hora de inicio del traslado en las observaciones (mecanismo transitorio del resolutivo 3°d), y si no se informa patente, la constancia expresa "Patente del vehículo no conocida al momento de la emisión" (resolutivo 1°c). Chofer/transportista/patentes deben informarse cuando se conocen.

⚠️ fechaSalida/horaSalida/fechaLlegada y patenteCarro son tags del formato actualizado del SII: se emiten en el XML solo si se informan (opt-in), para no afectar la validación de schema mientras el SII termina de habilitar el formato.

POST /api/Documento/notaventa/{folio}/anular

Cancelación del pedido: libera las reservas asociadas. No "repone" stock porque nunca lo descontó.

Casos según la estructura de la empresa

Caso 1: Una sola razón social (el flujo estándar)

La tienda y OpenTPV son la misma empresa. El flujo es el descrito arriba: reserva → nota de venta con los datos del cliente final → documentar como boleta (39) o factura (33) al mismo cliente. Nada más que decidir.

Caso 2: Varias razones sociales (e-commerce con ERP propio)

Estructura típica: la razón social del e-commerce (con su propio ERP) vende al cliente final y emite su boleta; el stock vive en una o más razones sociales que usan OpenTPV. La operación entre empresas es: la RS OpenTPV le vende a la RS e-commerce, y la mercadería viaja directo al cliente final (dropship).

Flujo por cada OpenTPV que aporta stock:

1. Reserva por API contra la bodega OpenTPV        (igual que el caso 1)
2. Nota de venta a nombre del CLIENTE FINAL        (el pedido, con su trazabilidad)
3. Documentar con tipoDte 52 (guía de despacho) y
   codigoClienteReceptor = código del cliente "RS e-commerce" en OpenTPV:
   → la guía sale a nombre de la RS e-commerce (receptor tributario)
   → el domicilio del cliente final queda como DESTINO del traslado en la guía
   → baja el stock y consume la reserva
4. Fin de mes, en el ERP: facturación masiva de guías → una factura a la
   RS e-commerce que referencia todas las guías del período (sin mover stock)

Reglas de este caso:

  • Una guía por pedido, no consolidada por día/semana. La guía ampara un traslado físico concreto; cada paquete que viaja al cliente final necesita la suya. La consolidación va en la factura mensual, que referencia N guías (es el patrón estándar de los ERP: delivery por embarque, factura colectiva).
  • La RS e-commerce debe existir como cliente en OpenTPV (codigoClienteReceptor); la guía queda asociada a ella, que es lo que permite la facturación masiva.
  • codigoClienteReceptor solo aplica a tipos 33, 34 y 52; las boletas no admiten receptor distinto al de la nota de venta.
  • Ajustar el TTL de pedido confirmado (ttlReservaNvMin) al ciclo real de despacho (p.ej. 2880 min = 2 días); si la reserva vence antes de emitir la guía, el mostrador podría vender ese stock.
  • El motivo de traslado de la operación entre razones sociales conviene validarlo con el contador del cliente.

Configuración (ERP → Opciones → E-Commerce)

Opción Default Qué hace
API descuenta stock al emitir apagado Compuerta global. Activarla es un cambio de comportamiento para integradores existentes (el ERP pide confirmación).
Reservar en CHECKOUT CARRO / CHECKOUT / NINGUNO
Duración reserva (min) 15 TTL del carro
Reserva pedido confirmado (min) 1440 TTL tras asociar a nota de venta
Bodega web 0 0 = compartida; 1–20 = dedicada
Stock a no publicar (colchón) 0 Unidades o % ; solo con bodega compartida
ttlReservaMaxMin 4320 Techo del TTL que puede pedir el integrador. Solo editable por SQL.

Requisitos: migración sql/89.reservas_stock.sql aplicada (el sistema igual crea las columnas en caliente como red de seguridad). El barrido (Cronjob:BarridoReservas en appsettings de la API) hace dos cosas: vence reservas caducadas y reconcilia consumos (si una nota de venta ya tiene documento emitido pero su reserva quedó activa por un fallo transitorio, la consume). La correctitud no depende del cron (cada operación de reservas ejecuta el mismo barrido de forma perezosa), pero con e-commerce activo conviene habilitarlo para que la reconciliación corra aunque la tienda esté quieta.

Errores comunes del integrador

  • Publicar atpFisico o stockFisico en vez de atpPublicable → sobreventa del colchón.
  • No reenviar el POST /reservas completo al cambiar el carro (el token reemplaza, no suma).
  • Seguir renovando el token después de confirmar el pedido.
  • Cobrar sin revisar advertencia de la nota de venta ni el polling de incumplidas.
  • Asumir que anular repone stock (nunca se descontó) o que reservar lo descuenta.