Tienda con productos físicos

Productos con tallas y colores, existencias, retiro, delivery o envío nacional, y la confirmación de que el cliente recibió su compra.

Para un negocio que vende cosas que el cliente recibe en la mano. El ejemplo es una tienda de franelas con tallas y colores que entrega por retiro en la tienda, delivery en su ciudad y envío nacional por Zoom.

El modelo

En tu aplicaciónEn Billete
La franela básicaUn producto con category: "physical" y lookupKey: "franela-basica"
Cada talla y colorUna variante, con su precio y sus existencias
El carritoUn checkout con items
El pedido pagadoUna entrega (ful_…) que sigue la compra hasta que llega
  • Las formas de entrega se activan en dos lugares. El negocio las ofrece en Configuración › Entregas (dónde se retira, en qué zonas hace delivery, si envía por Zoom) y cada producto dice cuáles admite en fulfillmentMethods. El checkout ofrece las que están en los dos. Un producto que no es physical, o que no admite ninguna, no pasa por entregas: su compra termina como cualquier otra.
  • Billete lleva las existencias. Las unidades se apartan cuando el cliente inicia el pago, se descuentan al confirmarse y vuelven si el pago falla; así dos clientes no pagan la última M. Abrir el checkout no aparta nada, y un reembolso no repone unidades.
  • El cliente confirma que la recibió. La entrega pasa a received. Si reporta un problema, Billete lo revisa y media entre tu negocio y tu cliente. El neto del pago ya está en tu Pago Móvil desde que se confirmó. Mira confirmar recepción.
  • El costo del delivery o del envío no se cobra en el checkout. Explica cómo se paga en las instrucciones de cada forma de entrega (por ejemplo, el envío se le paga a Zoom al retirarlo).

1. Configura las entregas

En Configuración › Entregas activa las formas que ofreces: para retiro, la dirección y el horario; para delivery, las zonas; para envío nacional, las instrucciones para el cliente. arrange (acordarlo con el negocio) no necesita configuración: inclúyelo en el producto si quieres ofrecer esa alternativa.

const franela = await billete.products.create({
  name: "Franela básica",
  lookupKey: "franela-basica",
  category: "physical",
  fulfillmentMethods: ["pickup", "delivery", "shipping"],
  currency: "USD",
  variants: [
    { name: "M negra", attributes: { talla: "M", color: "Negro" }, amount: 1500, stock: 20, isDefault: true },
    { name: "L negra", attributes: { talla: "L", color: "Negro" }, amount: 1500, stock: 12 },
    { name: "XL negra", attributes: { talla: "XL", color: "Negro" }, amount: 1800, stock: 5 },
  ],
}, { idempotencyKey: "producto-franela-basica" });

Con variantes, el precio y el stock van en cada una, nunca en el producto. stock: null es ilimitado. Para encontrar el producto sin guardar su ID, búscalo por su clave: billete.products.list({ lookupKey: "franela-basica" }). Con precios en dólares, el cliente paga en bolívares a la tasa BCV (precios en divisas).

Para reponer, actualiza el producto con todas sus variantes (cada una con su id) y el stock nuevo en la que repones: omitir una variante guardada responde variant_deletion_required. Para dejar de vender una talla sin borrarla, active: false en esa variante; para pausar todo el producto, active: false en él.

3. Cobra el carrito

const checkout = await billete.checkouts.create({
  items: cart.lines.map((line) => ({
    product: line.productId,
    variant: line.variantId,
    quantity: line.quantity,
  })),
  customer: { email: user.email, externalId: user.id },
  successUrl: "https://tutienda.com/pedidos/gracias?checkout={CHECKOUT_ID}",
  cancelUrl: "https://tutienda.com/carrito",
  metadata: { orderId: order.id },
}, { idempotencyKey: `pedido-${order.id}` });

await db.orders.update(order.id, { checkoutId: checkout.id });
return redirect(checkout.url);

El checkout le pide al cliente cómo quiere recibir la compra, su nombre y su teléfono, y según la forma, la dirección del delivery o la oficina de Zoom y su cédula. No hace falta collect para eso; úsalo solo para pedir datos en compras sin entrega.

Si un carrito trae más unidades de las que hay, responde insufficient_stock. Si sus productos no comparten ninguna forma de entrega (uno solo se retira y otro solo se envía), responde fulfillment_methods_conflict: véndelos en checkouts separados.

4. Entrega el pedido

Todo esto se hace también desde la sección Entregas del dashboard, sin código.

Retiro, delivery o acordado. La página del pedido del cliente muestra un código QR. Al entregarle, escanéalo: es la prueba de que estuvo ahí, y le da 24 horas para confirmar o reportar un problema.

const { data: [found] } = await billete.fulfillments.list({ handoverCode: valorEscaneado });
await billete.fulfillments.handOver(found.id, { code: valorEscaneado });

// Sin el código (lo llevó un motorizado): el cliente tiene 48 horas.
await billete.fulfillments.deliver(fulfillment.id);

Si el cliente no responde en ese plazo, la compra cuenta como recibida. Nunca la confirmes en su nombre.

Envío nacional. Tienes 2 días hábiles desde el pago (trackingDueAt) para cargar la guía. Billete verifica con Zoom que existe y va a la oficina que eligió el cliente, sigue el envío y lo da por recibido cuando Zoom lo entrega. Sin una guía válida a tiempo, la entrega se cancela y le debes al cliente lo que pagó: se lo devuelves desde tu banco (reembolsos pendientes).

await billete.fulfillments.submitTracking(fulfillment.id, { number: guia });

5. Sigue el pedido

Las entregas de los eventos pueden repetirse y llegar en otro orden: cada evento solo dispara una consulta.

switch (event.type) {
  case "checkout.completed":
    await db.orders.update(event.data.metadata.orderId, { status: "paid" });
    break;
  case "fulfillment.created":
  case "fulfillment.shipped":
  case "fulfillment.ready_for_pickup":
  case "fulfillment.delivered":
  case "fulfillment.received":
  case "fulfillment.problem_reported":
  case "fulfillment.canceled": {
    const fulfillment = await billete.fulfillments.retrieve(event.data.id);
    await db.orders.updateByCheckout(fulfillment.checkoutId, { delivery: fulfillment.status });
  }
}
EventoQué pasóQué hace tu aplicación
fulfillment.createdPagó y espera su compra.Prepara el pedido. Si es un envío, carga la guía antes de trackingDueAt.
fulfillment.shippedZoom aceptó la guía y lo lleva a la oficina.Muestra el pedido en camino.
fulfillment.ready_for_pickupEstá en la oficina de Zoom desde readyAt.Muestra que espera al cliente. Billete le recuerda retirarlo a los 3 y 7 días y te avisa el día 7.
fulfillment.deliveredLo entregaste; el cliente tiene hasta confirmBy para confirmar.Muestra el pedido entregado.
fulfillment.receivedEl cliente confirmó, venció el plazo, Zoom lo entregó o Billete resolvió un problema a tu favor.Cierra el pedido.
fulfillment.problem_reportedEl cliente reportó un problema: es un reclamo y Billete lo revisa.Atiéndelo (reembolsos y reclamos).
fulfillment.canceledSe reembolsó el pago completo, o el envío no salió a tiempo (cancellationReason: "not_shipped") y le debes el pago al cliente.Cancela el pedido.

Una entrega cancelada no dice que el dinero ya volvió: el reembolso se sigue en el pago y sus eventos.

Referencia de productos · Referencia de entregas · Reglas de entregas