Saltar al contenido

Construir un Cart Transform que calcule bien las cantidades

Un cargo que sale bien con una unidad y mal con once es el error característico de este trabajo. Este es el orden de trabajo que lo evita.

En la práctica5 de agosto de 20268 min de lectura

Una Function de Cart Transform son unas treinta líneas de lógica real. Lo difícil no es el código. Es que una línea de carrito es una referencia de mercancía más una cantidad, que tres números del input y del output están denominados por unidad, y que quien aprueba el trabajo casi siempre agrega uno de cada producto.

Así que el cargo se ve perfecto en la tienda de desarrollo, sale a producción y cobra mal la unidad once. Es el fallo característico de este target, y lo que sigue está ordenado para prevenirlo, no para atraparlo después.

El orden en el que trabajamos un cargo o un bundle: la regla en prosa, después la operación, las exclusiones, la configuración y las pruebas. Saltar al input query es como la ambigüedad sobrevive hasta producción.

Escribe la regla como una frase que un comerciante firmaría

Antes del GraphQL, escribe la regla en lenguaje llano y oblígala a responder cinco preguntas que si no responderá por accidente:

  • ¿A qué aplica? No "a productos": a cuáles, identificados cómo. Una colección, un tag, una lista explícita de variantes.
  • ¿El monto es por unidad o por línea? Di "unidad" o "línea" en voz alta. "Por pedido" es una tercera respuesta y otra implementación.
  • ¿Qué queda excluido? Las tarjetas de regalo son la primera respuesta de siempre. Pide la segunda y la tercera.
  • ¿Qué ve el comprador? Una línea aparte con su propio título, o un precio cambiado en la existente. Operaciones distintas.
  • ¿Qué pasa cuando se topa con un descuento? Un descuento porcentual de pedido interactúa con una línea de cargo añadida, y el comerciante tiene una opinión aunque no la haya dicho.

Una buena regla se lee así: "Doce dólares por unidad, sobre cada unidad de un producto con el tag import-duty, excluyendo tarjetas de regalo y excluyendo cualquier cosa con el tag fee-exempt, mostrado como su propia línea titulada Tarifa de importación." Esa frase contiene un input query, una operación y cuatro pruebas. Una más vaga contiene un bug.

Elige la operación antes que los campos

Cart Transform da tres operaciones y no son intercambiables.

lineExpand reemplaza una línea por un conjunto de artículos expandidos. Es lo que quieres para un cargo: expandir la línea original en sí misma más un artículo de cargo, cada uno con su precio. También es como se explota un bundle: la línea se vuelve las partes, y cada parte lleva su propio ajuste de precio.

linesMerge es lo inverso: varias líneas se colapsan en una variante padre con título y precio nuevos, ya sea un fixedPricePerUnit o un percentageDecrease. Es "tres de estos se vuelven un kit con quince por ciento de descuento".

lineUpdate cambia una línea donde está. Es la más pequeña y la más restringida: se rechaza de plano si la tienda no está en Plus o en una tienda de desarrollo. Tenlo en cuenta antes de diseñar alrededor de ella.

Dos cosas que conviene conocer temprano. Una línea de suscripción es un caso que hay que probar explícitamente y no dar por supuesto, porque los selling plans interactúan con las tres operaciones. Y apuntar a una línea que ya se expande o se fusiona es inválido: tu código decide una cosa por línea, no acumula opiniones.

Por unidad contra por línea, en concreto

Toda la trampa cabe en tres nombres de campo. El input te entrega, por línea, un quantity y un cost.amountPerQuantity.amount. El segundo es el precio de una unidad, no el total de la línea. El output, en un artículo expandido, recibe un ajuste fixedPricePerUnit: otra vez, una unidad.

Así que "doce dólares por unidad" se implementa emitiendo un componente de cargo con cantidad igual a la de la línea y un precio fijo por unidad de doce. El cargo escala porque Shopify multiplica, no porque multiplicaras tú.

Las tres formas de equivocarse son cosas que cualquier persona razonable escribe un martes. Multiplicas el doce por la cantidad y además fijas la cantidad: ciento treinta y dos por unidad. Pones la cantidad del cargo en uno y dejas el precio en doce: once unidades llevan un solo cargo de doce. Multiplicas y pones la cantidad en uno: esa línea sale bien, y toda regla que vuelva a correr en la siguiente edición sale mal.

El artículo original también hay que volver a cotizarlo dentro de la expansión. Describes todos sus componentes, el producto incluido, así que su precio se declara y no se hereda. Pasar cost.amountPerQuantity.amount tal cual es lo correcto; olvidarlo convierte un cargo en descuento.

Desliza la figura para verla completa

Las exclusiones son parte de la regla, no un filtro que agregas después

Las exclusiones van en el input query, porque es el único dato que vas a recibir. Tres mecanismos cubren casi todo.

Tags. El esquema de input expone hasAnyTag(tags: [...]) en productos y en el cliente, evaluado por Shopify antes que tu código. La exclusión más barata de operar: el comerciante agrega un tag en el admin y el comportamiento cambia, sin despliegue.

Colecciones. inAnyCollection(ids: $collectionIds) recibe IDs de colección como variables del query. Un detalle documentado se gana su prueba: si el conjunto va vacío, el campo devuelve falso, no verdadero. Una regla del tipo "aplica a todo salvo que haya lista de colecciones" no aplicará a nada el día en que se vacíe.

Una lista explícita en la configuración. Para tarjetas de regalo y para el puñado de variantes que siempre son excepción, no te apoyes en la inferencia. Decide cómo se identifica una tarjeta de regalo en este catálogo — un tag, una colección, un ID de variante — y codifícalo. Una línea de carrito no anuncia que es un caso especial.

Una cosa más que hay que manejar en vez de suponer: la mercancía de una línea es una unión. Puede ser un ProductVariant o un CustomProduct, que no tiene producto y por lo tanto ni tags ni colecciones. Ramifica sobre __typename y decide: el defecto es un caso sin manejar dentro de un checkout.

Configuración en metafields, para que los números no sean un despliegue

El monto, los nombres de los tags, los IDs de variante excluidos y el título de la línea de cargo van en un metafield leído por el input query, no en el binario. El namespace reservado $app es el lugar: metafield(namespace: "$app", key: "function-configuration"), con JSON.

query Input {
  cart {
    lines {
      id
      quantity
      cost { amountPerQuantity { amount } }
      merchandise {
        __typename
        ... on ProductVariant {
          id
          product { hasAnyTag(tags: ["import-duty", "fee-exempt"]) }
        }
      }
    }
  }
  cartTransform {
    metafield(namespace: "$app", key: "function-configuration") { jsonValue }
  }
}

Dos reglas mantienen esto honesto. Primera: parsea la configuración una sola vez, arriba, hacia una forma tipada, y falla ruidosamente ante un valor mal formado en vez de caer en silencio a cero — un cargo que se vuelve gratis sin avisar es peor que un error. Segunda: mantén el dinero como cadena decimal hasta el final. El input da los montos como cadenas por una razón, y un cargo calculado con flotantes saldrá algún día desviado por un centavo en una factura que alguien lee.

La moneda de presentación es la otra mitad. Un valor guardado en la moneda de la tienda hay que multiplicarlo por el presentment currency rate antes de que lo vea un comprador de otro mercado. Un cargo correcto en una moneda y equivocado en tres es la versión internacional del bug de cantidad.

Desliza la figura para verla completa

Las pruebas que valen la pena

Un Cart Transform es una función pura de una entrada a una lista de operaciones, de lo más fácil de probar que hay en comercio. Los fixtures son JSON plano y no hay tienda que levantar. El conjunto que se gana su lugar:

  1. Cantidad uno. La base. Va a pasar.
  2. Cantidad once. La prueba que habría atrapado el bug. Afirma el monto, no que "existe un cargo".
  3. Una línea excluida a solas. Una tarjeta de regalo sola no debe producir ninguna operación, ni una con monto cero.
  4. Un carrito mixto. Líneas elegibles y excluidas juntas, la excluida arriba de cantidad uno, para que un filtro que se fuga aparezca como número equivocado.
  5. Una exención por tag sobre un producto por lo demás elegible. Donde se topan las dos reglas de tag viven los bugs de precedencia.
  6. Configuración vacía y mal formada. Metafield ausente, JSON vacío, monto en cero, monto dado como número en vez de cadena.
  7. Mercancía custom. Una línea cuya mercancía no es una variante de producto.
  8. Idempotencia ante una edición del carrito. Corre contra un carrito que ya refleja un resultado anterior y confirma que el cargo no se aplica dos veces.

El conjunto que se gana su sitio:

La octava es la que la gente se salta y la que produce los tickets más raros. Las Functions vuelven a correr con cada cambio del carrito, así que correr dos veces tiene que ser igual que correr una.

Desplegarlo

Publica a través de tu app, verifica contra carritos reales en una tienda de desarrollo — incluido uno con cantidad distinta de uno — y después vigila los primeros pedidos reales que toque. No el primer día: los primeros pedidos. Un Cart Transform es invisible cuando funciona, y el hueco entre "salió" y "alguien lo notó" es por donde se va el dinero.

La Function de tarifa de importación de nuestro portafolio tiene esta forma exacta: configuración por metafields, exclusiones de tarjeta de regalo y de producto, aritmética segura por cantidad. Si aún decides si lo que necesitas es un Cart Transform, la decisión de target y de plan va primero, y nuestra página de desarrollo de Shopify Functions cubre la secuencia completa.

Blog