Comercio
Los productos son registros de modelo, el carrito es un espacio de mutaciones público que llama tu tienda y los pedidos viven en el panel.
cmssy trae carrito, pedidos, descuentos y un pipeline de pedidos. Nada de eso es un área de producto aparte: un producto es un registro de modelo, la tienda habla con la misma API de entrega que todo lo demás y el escritorio de pedidos es el panel.
La capacidad es una bandera del plan -canUseCart en get_workspace_info-, así que léela en vez de suponerla.
Los productos son registros
En cmssy no hay un tipo "producto". Creas un modelo -product, plan, ticket, lo que sea tu catálogo- y la pestaña Producto del modelo declara qué campos cumplen qué rol: nombre, precio, imagen, SKU, tipo impositivo, stock y ejes de variantes. Un rol sin definir se resuelve al campo de visualización del modelo o a la clave convencional (price, sku, inventory…), así que un catálogo sencillo no necesita ningún mapeo. La configuración de carrito del workspace solo lista los modelos en venta como fuentes de productos.
El campo de precio guarda el precio en la moneda por defecto del workspace. Los precios en otras monedas no son un campo del modelo: se fijan por moneda (consulta Monedas más abajo), y un modelo ya no puede indicar un campo de moneda.
Los agentes leen los roles resueltos con get_model (product.roles) y fijan las sobrescrituras con update_model.
Por eso un catálogo puede venir de más de un modelo a la vez, y por eso el mismo bloque que renderiza cualquier registro renderiza un producto.
Leer el catálogo
Un producto vuelve de public.model.records como cualquier otro registro, con algunos campos que solo significan algo en un producto.
variants(limit, offset)- una página de las variantes del producto, 50 como máximo;variantCountlas cuenta todas. Un producto con más variantes que una página lee la siguiente por offset.variantPriceRange { min max }- la variante más barata y la más cara, en unidades menores (4900 = 49,00), null en un producto sin variantes. Una tarjeta de listado que dice «desde 49,00» lee esto en lugar de las variantes.records(sku: "TEE-M")- el producto que lleva este SKU, como propio o en una de sus variantes. Añadevariants(sku: "TEE-M", limit: 1)para obtener solo esa variante - un formulario de pedido rápido no necesita nada más.available- unidades que un comprador puede pedir ahora: existencias menos lo que reservan los carritos abiertos, sumado entre variantes. Null cuando el producto no controla existencias, incluido cuando alguna de sus variantes no las controla.records(stockState: "in" | "low" | "out")- filtra por ese mismo número frente apublic.siteConfig.lowStockThreshold: bajo es más de cero y no más que el umbral. Colorea una insignia de existencias a partir deavailabley el mismo umbral, y la insignia no puede contradecir al filtro.
Un campo oculto no se revela a través de otro. El SKU propio de un producto solo se encuentra cuando la API de entrega sirve el campo que lo guarda. Cuando el campo de existencias está oculto, available y el inventory de cada variante son null, y stockState se rechaza.
Qué puede llamar la tienda
El espacio cart es público: sin token, con alcance de sesión, y es la única parte de la API de entrega que escribe.
cart.get(workspaceId): líneas,subtotal,tax,shippingTotal,discountedTotal,totalGross,currency,pricesIncludeTax,availableShippingMethods,appliedDiscount,taxSummary.cart.addItem:{ recordId, quantity, variantSelections, notes }. La línea referencia al registro, no lo copia.cart.updateItem,cart.removeItem(itemId),cart.clear.cart.applyDiscount(code),cart.removeDiscount,cart.setShippingMethod(shippingMethodId).cart.merge: funde el carrito anónimo de sesión con el del miembro tras iniciar sesión. Llámalo una vez, justo después de la autenticación.cart.checkout:{ customerEmail, customerNote, poNumber, shippingAddress, customFields }convierte el carrito en pedido.
Los totales vuelven calculados. No sumes precios en el navegador: el modo de impuestos, los descuentos y el envío se deciden en el servidor, y una segunda implementación en tu frontend es una segunda respuesta.
Una mutación de carrito por petición. Los campos dentro de cart { … } son campos de un objeto de espacio de nombres, no campos raíz, así que GraphQL los ejecuta en paralelo: addItem y setShippingAddress en un mismo documento compiten entre sí, y una sesión recién creada por la primera llamada no es visible para la segunda. Envía cada escritura del carrito en su propia petición.
Estado del pedido sin cuenta
public.order.byToken devuelve un pedido -orderNumber, items, paymentStatus, fulfillmentStatus, amountPaid, balanceDue, customFields, invoiceUrl, shipments (transportista, número y enlace de seguimiento, estado)- para el token emitido en el checkout. Así funciona una página de "sigue tu pedido" para un invitado.
Qué configura el workspace
public.siteConfig.publicCart es la proyección segura de los ajustes del carrito: defaultCurrency, additionalCurrencies, pricesIncludeTax, taxRates, shippingMethods, productSources, maxItemsPerCart, maxQuantityPerItem, orderFields, enableSavedCarts, enableQuoteRequests. Léela una vez y deja que dé forma a la interfaz: formato de moneda, límites de cantidad y si se ofrece o no una solicitud de presupuesto.
Reglas de envío
Cada método de envío lleva sus reglas: los países que atiende (countries, códigos ISO, vacío = todos), una ventana de subtotal (minSubtotal / maxSubtotal) y freeAbove, el subtotal a partir del cual es gratis. availableShippingMethods en el carrito ya las aplica a la dirección de envío y al subtotal: la tienda nunca filtra métodos por su cuenta. En una moneda adicional, un método usa los importes fijados para esa moneda en Precios en otras monedas; un método sin ellos no se ofrece a los carritos en esa moneda.
Campos de pedido
Campos propios en cada pedido: un mensaje de regalo, una franja de entrega, una referencia interna. Se definen una vez en Configuración → Campos de pedido (o con update_cart_config y orderFields), 30 como máximo, cada uno con clave, etiqueta por idioma, tipo (text, number, date, select, boolean, email, phone, url) y límites opcionales.
Cada campo es del cliente o solo del equipo. publicCart.orderFields lista solo los que el cliente puede rellenar: muestra una entrada para cada uno y envía los valores como customFields en cart.checkout, por clave. El servidor los valida -tipo, límites, obligatoriedad- y rechaza la clave de un campo del equipo; un valor vacío se descarta. Solo puede ser obligatorio un campo que el cliente puede rellenar, así que un campo del equipo nunca bloquea un checkout.
Los valores se guardan en el pedido como customFields. public.order.byToken devuelve solo los campos del cliente; el panel, get_order y los webhooks order.* los llevan todos. El equipo los edita en el pedido o con update_order_details: una clave puesta a null borra el valor. Eliminar una definición no toca los valores ya guardados en los pedidos.
Monedas
Un workspace tiene una moneda por defecto, la base del catálogo, y hasta 9 monedas adicionales. Ambas se fijan en Ajustes → Región e impuestos, o con update_cart_config (defaultCurrency, additionalCurrencies). La moneda por defecto no se puede cambiar mientras existan monedas adicionales: quítalas primero. Una moneda en la que todavía vende un canal de venta no se puede quitar.
Cada canal de venta vende en una moneda: la moneda por defecto o una adicional (currency en create_sales_channel y update_sales_channel). El comprador nunca elige moneda: la tienda elige el canal, y la moneda del canal fija todos los precios. El carrito, su pedido, el envío, el descuento, los pagos y los reembolsos están en esa única moneda. Cambiar la moneda de un canal abandona los carritos abiertos en él.
No se convierte nada. cmssy no tiene tipos de cambio. Cada importe en una moneda adicional es uno que fijas tú:
- Productos: Precios por moneda en un producto del catálogo, un precio y tramos por volumen por moneda, y para un producto con variantes un precio por variante. Guardarlo nunca cambia el stock. Los agentes usan
set_product_currency_prices, que pide confirmación, ycurrencyPricesenset_product_variants. - Listas de precios: una fila de precio fijo por moneda, y el porcentaje de la lista se descuenta del precio en esa moneda. Consulta Listas de precios.
- Métodos de envío: precio, ventana de subtotal y umbral de envío gratis por moneda, en Precios en otras monedas.
- Códigos de descuento: Mínimo por moneda (
minSubtotalByCurrency) en códigos de porcentaje y de envío gratis. Un código con mínimo solo se aplica en las monedas que nombran sus mínimos. Un código de importe fijo tiene una moneda y solo se aplica a carritos en ella.
Un producto sin precio en la moneda del canal no se vende en ese canal: una lectura de precios lo devuelve con price: null, un listado con precios lo omite, y cart.addItem lo rechaza con extensions.reason NO_PRICE_IN_CURRENCY. Los precios en una moneda que desactivas se conservan; no fijan ningún precio hasta que vuelvas a activar la moneda.
Los pedidos conservan su moneda. La lista de pedidos tiene una columna y un filtro de moneda (currency en list_orders), un pedido manual elige su moneda (currency en create_manual_order), y la analítica y el gasto de los clientes se informan por moneda, nunca sumados entre monedas.
Monedas en la tienda
Lee los precios de un canal en public.pricing.prices y public.pricing.catalogPrices con la clave del canal, o en account.prices y account.catalogPrices para un comprador con sesión iniciada. El campo de precio del registro y variantPriceRange guardan solo el precio en la moneda por defecto. Cada fila de precio lleva currency: formatea cada importe con la moneda de su propia fila, nunca con una fija en el código, y divide por el número de decimales de esa moneda, no por 100. En un canal en una moneda adicional, el currency del resultado la nombra, incluso en una página vacía. publicCart lista defaultCurrency y additionalCurrencies.
Los pedidos son back-office
Todo lo posterior al checkout va autorizado: el panel, o el servidor MCP con un token cuyo rol lo permita. Los pedidos se listan y se leen, se crean a mano, se editan línea a línea, se marcan pagados total o parcialmente, se reembolsan, se cancelan, avanzan por el fulfillment, reciben factura y se mueven por un pipeline configurable. Cada envío se registra en el pedido con create_shipment y update_shipment: transportista, número de seguimiento, enlace de seguimiento y un estado de pending a delivered con su historial. Añadir uno marca el pedido como enviado, o parcialmente enviado en una entrega dividida. Los descuentos se crean, actualizan y activan igual; los productos se actualizan o borran en masa, o reciben tramos de precio.
Los proveedores de pago no forman parte de esto: cmssy registra qué pasó -mark_order_paid, record_order_payment, refund_order- y tu integración decide cuándo pasó.
Anonimizar un pedido
Ante una solicitud de borrado, el equipo con orders:manage anonimiza un pedido finalizado - cancelado, entregado, devuelto o reembolsado - desde su página, o todos los pedidos finalizados de un cliente desde la página del cliente. El correo, la dirección de envío, el número de orden de compra, las notas, los campos de pedido y las notas de entrega del comprador se borran del pedido, su carrito, las entregas de webhooks guardadas y el historial de Spotlight que los mostró. Los artículos, importes, pagos, la factura y el número de pedido se conservan, así que la contabilidad sigue cuadrando. No se puede deshacer, un pedido en curso se rechaza y el webhook order.anonymized avisa a tus sistemas para que borren su copia.
Eliminar un pedido de prueba
Un pedido cancelado sin pago y sin factura - de prueba o spam - lo puede eliminar por completo desde su página el equipo con orders:manage. Los datos del comprador se borran del carrito, las entregas de webhooks guardadas y el historial de Spotlight igual que al anonimizar, y el registro de auditoría conserva el número de pedido. Cualquier otro pedido se rechaza: si hay dinero o una factura de por medio, anonimízalo.
Siguientes pasos
- Modelos de datos: los registros de los que se compone un catálogo.
- API de entrega GraphQL: cómo se acota una escritura pública.
- Clientes: las cuentas detrás de los pedidos, y empresas en B2B.
- Listas de precios: precios B2B por empresa o cliente, y cómo los muestra una tienda.
- Webhooks: los diez eventos
order.*, yshipment.*para los envíos.