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ón | En Billete |
|---|---|
| La franela básica | Un producto con category: "physical" y lookupKey: "franela-basica" |
| Cada talla y color | Una variante, con su precio y sus existencias |
| El carrito | Un checkout con items |
| El pedido pagado | Una 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 esphysical, 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.
2. Crea el catálogo
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 });
}
}| Evento | Qué pasó | Qué hace tu aplicación |
|---|---|---|
fulfillment.created | Pagó y espera su compra. | Prepara el pedido. Si es un envío, carga la guía antes de trackingDueAt. |
fulfillment.shipped | Zoom aceptó la guía y lo lleva a la oficina. | Muestra el pedido en camino. |
fulfillment.ready_for_pickup | Está 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.delivered | Lo entregaste; el cliente tiene hasta confirmBy para confirmar. | Muestra el pedido entregado. |
fulfillment.received | El cliente confirmó, venció el plazo, Zoom lo entregó o Billete resolvió un problema a tu favor. | Cierra el pedido. |
fulfillment.problem_reported | El cliente reportó un problema: es un reclamo y Billete lo revisa. | Atiéndelo (reembolsos y reclamos). |
fulfillment.canceled | Se 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