Listas de precios

Da a empresas y clientes sus propios precios - un porcentaje sobre el catálogo más precios fijos por producto - mientras tu tienda mantiene el catálogo en caché.

Una lista de precios - Gold, Silver, Dealer - da a un grupo de compradores sus propios precios. La asignas a una empresa o a un cliente, y cmssy calcula en el servidor el carrito, el checkout y los pedidos de ese comprador a partir de ella. El catálogo que lee un visitante no cambia, así que tus páginas de producto siguen siendo anónimas y cacheables.

Qué contiene una lista

  • Un porcentaje de descuento sobre el precio de catálogo, de 0 a 99,99 con dos decimales como máximo. Se aplica a cada producto para el que la lista no tiene precio fijo, y a cada uno de sus tramos por volumen. Se guarda en puntos básicos y se calcula con enteros: redondeo a la mitad hacia arriba a una unidad menor entera, un producto gratuito se queda en 0 y uno de pago nunca baja de 1 unidad menor.
  • Precios fijos por producto, en unidades menores, cada uno con tramos por volumen opcionales. Un producto con variantes recibe un precio fijo por variante, nunca uno para todo el producto. Un precio fijo puede estar por encima del de catálogo: un precio contractual sigue siendo un precio.

Un precio fijo gana al porcentaje; sin ninguno de los dos, el comprador paga el precio de catálogo. La moneda es siempre la del producto: una lista cambia importes, no monedas. Un precio fijo para un producto o variante eliminado después no fija ningún precio, y la lista lo marca como eliminado para que puedas borrarlo.

Quién paga qué lista

Una lista por comprador, nunca dos acumuladas:

  • Un cliente que pertenece a una empresa paga siempre la lista de la empresa, o el precio de catálogo si la empresa no tiene ninguna. cmssy rechaza asignar una lista directamente a ese cliente: asígnala a la empresa. Una lista que el cliente tenía antes de unirse a la empresa se ignora mientras pertenezca a ella.
  • Un cliente sin empresa puede tener su propia lista: un VIP o un comprador profesional suelto.
  • Un invitado paga el precio de catálogo.

Gestionar listas

Las listas están en Commerce → Listas de precios. Crea una con un nombre y un porcentaje y ábrela para añadir precios fijos por producto o variante. Eliminar una lista borra sus precios fijos y la desasigna de todas las empresas y clientes; los pedidos conservan el nombre con el que se calcularon.

Asigna una lista en la página del cliente, o en un registro de empresa dentro de su modelo. Los permisos:

  • models:edit: crear, editar y eliminar listas y sus precios fijos; models:view las lee.
  • customers:manage: asignar una lista a un cliente o a una empresa. customers:view basta para ver qué listas existen.

Una sincronización con el ERP escribe precios fijos en bloque: priceList.setEntries en la API de administración escribe hasta 1000 de una vez, todo o nada: una fila incorrecta rechaza la llamada entera, y el error nombra cada fila incorrecta por su índice.

Desde Spotlight y el servidor MCP

Spotlight y el servidor MCP (desde 0.108.0) ofrecen las mismas operaciones, con los mismos permisos: list_price_lists, get_price_list, create_price_list, update_price_list, delete_price_list, set_price_list_entry, set_price_list_entries, remove_price_list_entries, set_customer_price_list y set_company_price_list.

Precios en una tienda headless

El precio de un comprador es personal, así que nunca va en una consulta de producto: cada lectura del catálogo pasaría a depender del miembro y no se podría cachear. El patrón es otro:

  • Renderiza y cachea el catálogo de forma anónima. Los listados y las páginas de producto leen public.model.records exactamente como para un visitante sin sesión, con precios de catálogo. Nunca uses al miembro como clave de una página en caché.
  • Tras el inicio de sesión, superpone los precios del comprador. Pide a account.prices los productos en pantalla, como máximo 100 elementos por llamada, enviando el Authorization: Bearer <accessToken> del miembro. Sin token de miembro, la llamada se rechaza.
  • Nunca cachees esa respuesta para otro visitante. Nada de caché de fetch compartida ni CDN. Léela en una ruta dinámica o en una server action e inserta los precios en la página ya renderizada.
query BuyerPrices($workspaceId: ID!, $items: [PriceQueryItem!]!) {
  account {
    prices(workspaceId: $workspaceId, items: $items) {
      recordId
      variantKey
      currency
      price
      priceMax
      listed
      tiers { minQty price }
    }
  }
}

Cada elemento es { recordId, variantKey }, donde variantKey es el ProductVariant.id de la API de entrega. Omítelo en un producto con variantes para obtener un rango: price es el más bajo, priceMax el más alto y tiers va vacío: el precio "desde" de una tarjeta de listado. Los importes están en unidades menores. listed es true cuando una lista fijó el precio, para una etiqueta "tu precio". Un producto desconocido u oculto se omite, así que empareja las filas por recordId y variantKey, no por posición.

Para una insignia como "Tus precios: Dealer Gold", lee el nombre de la lista en el perfil del miembro. Es null cuando no se aplica ninguna lista, y un miembro solo ve la suya:

query MyPriceList($workspaceId: ID!) {
  account {
    profile(workspaceId: $workspaceId) {
      priceList { name }
    }
  }
}

Con @cmssy/core 19.11.0 o posterior, getBuyerPrices envía la consulta de precios por ti.

Carrito y checkout

El carrito se calcula para quien lo lee. En cada lectura y escritura del carrito, cmssy recalcula las líneas que se calcularon para otra lista: un invitado que inicia sesión, un miembro que la cierra, una lista asignada o retirada. unitPrice es lo que se cobrará; snapshot.basePrice es el precio de catálogo antes de la lista y los tramos, para un precio tachado. Es null en una línea añadida antes de que existieran las listas de precios.

query CartPrices($workspaceId: ID!) {
  cart {
    get(workspaceId: $workspaceId) {
      items {
        id
        quantity
        unitPrice
        currentPrice
        priceMismatch
        snapshot { price basePrice currency }
      }
      subtotal
    }
  }
}

Cambiar el porcentaje o los precios fijos de una lista no recalcula los carritos ya calculados con ella. La línea muestra el precio nuevo en currentPrice con priceMismatch: true, y el checkout cobra lo que hay en el carrito.

Cuando el checkout rechaza

Si el recálculo cambió el carrito desde que el comprador lo vio por última vez, cart.checkout se niega en lugar de cobrar un importe que nadie mostró. El motivo está en extensions.reason del error GraphQL:

  • CART_REPRICED (código BAD_USER_INPUT): los precios del comprador cambiaron. El carrito ya está guardado con los precios nuevos: léelo de nuevo, muestra los totales nuevos, pide al comprador que confirme y vuelve a llamar a cart.checkout.
  • CART_CURRENCY_CHANGED (código CONFLICT): una línea se calcularía ahora en otra moneda distinta de la del carrito; extensions.itemId la identifica. Quita esa línea y vuelve a añadir el producto.

Con @cmssy/core 19.11.0 o posterior, checkoutErrorReason(error) devuelve cualquiera de los dos motivos.

Los códigos de descuento se aplican sobre la lista: un código se descuenta del subtotal calculado con la lista, y el subtotal mínimo de un código también se mide sobre él.

Pedidos

Un pedido congela la lista con la que se calculó. Su nombre aparece en el pedido en el panel y en get_order, y se mantiene aunque la lista se renombre o se elimine. En cada línea del pedido:

  • price: el precio unitario cobrado, con lista y tramo aplicados.
  • listPrice: el precio unitario de catálogo antes de la lista y los tramos. Sin lista es el precio antes de tramos, como hasta ahora; con un precio contractual por encima del catálogo puede ser menor que price.

Editar un pedido en el panel recalcula sus líneas de producto con los valores actuales de la lista congelada, o a precios de catálogo si esa lista se eliminó. Un pedido manual se calcula con la lista del cliente seleccionado.

Próximos pasos

  • Comercio: el carrito, el checkout y los pedidos que calcula una lista.
  • Clientes: clientes y empresas, a los que se asigna una lista.
  • Autenticación de miembros: el inicio de sesión y el token de acceso que necesita account.prices.