Shopify Functions

cultiva-vip-discount

cart.lines.discounts.generate.run · TypeScript · cultiva-cafe-app

Validation Passed
API Target
Discount
Lenguaje
TypeScript
Descuento VIP
15 %
Mínimo carrito
50,00 €
Flujo de la función en checkout
🛒
Trigger
Cart updated
📋
GraphQL Input
run.graphql
Function Logic
run.ts
🏷️
Output
FunctionResult
Applied
–15% VIP
Scaffolding con Shopify CLI
$ shopify app generate extension --template discount --flavor typescript --name cultiva-vip-discount
✔ Extension generated at: extensions/cultiva-vip-discount/ ├── shopify.extension.toml ├── src/ │ ├── run.ts │ └── run.graphql ├── schema.graphql └── package.json ✔ Function registered in Shopify Partners Dashboard ℹ API version: 2025-01 ℹ Target: cart.lines.discounts.generate.run
Implementación completa
run.ts
run.graphql
input.json
output.json
// extensions/cultiva-vip-discount/src/run.ts
// Shopify Function: VIP Collection Discount — Cultiva Café

import type { RunInput, FunctionRunResult } from "../generated/api";

const NO_CHANGES: FunctionRunResult = { discounts: [], discountApplicationStrategy: "FIRST" };

interface Configuration {
  minimumAmount: number;
  discountPercentage: number;
}

export function run(input: RunInput): FunctionRunResult {
  // 1. Leer configuración del metafield ($app:config)
  const config = JSON.parse(
    input.discount?.metafield?.jsonValue ?? '{}'
  ) as Partial<Configuration>;

  const minAmount       = config.minimumAmount       ?? 50.0;
  const discountPercent = config.discountPercentage  ?? 15.0;

  // 2. Filtrar líneas de la colección VIP
  const vipLines = input.cart.lines.filter((line) => {
    if (line.merchandise.__typename !== "ProductVariant") return false;
    return line.merchandise.product.inVipCollection;
  });

  if (vipLines.length === 0) return NO_CHANGES;

  // 3. Calcular subtotal VIP
  const vipSubtotal = vipLines.reduce((acc, line) => {
    const price = parseFloat(line.cost.amountPerQuantity.amount);
    return acc + price * line.quantity;
  }, 0);

  // 4. Verificar umbral mínimo
  if (vipSubtotal < minAmount) return NO_CHANGES;

  // 5. Construir operaciones de descuento por línea
  const targets = vipLines.map((line) => ({
    cartLine: { id: line.id },
  }));

  return {
    discounts: [
      {
        targets,
        value: {
          percentage: { value: String(discountPercent) },
        },
        message: `☕ Descuento VIP ${discountPercent}% — Cultiva Café`,
      },
    ],
    discountApplicationStrategy: "FIRST",
  };
}
run.graphql
# extensions/cultiva-vip-discount/src/run.graphql

query RunInput($vipCollectionId: [ID!]) {
  discount {
    metafield(
      namespace: "$app"
      key: "config"
    ) {
      jsonValue
    }
  }
  cart {
    lines {
      id
      quantity
      cost {
        amountPerQuantity {
          amount
          currencyCode
        }
      }
      merchandise {
        __typename
        ... on ProductVariant {
          id
          title
          product {
            inVipCollection: inAnyCollection(
              ids: $vipCollectionId
            )
          }
        }
      }
    }
  }
}
input.json (sample)
{
  "discount": {
    "metafield": {
      "jsonValue": "{\"minimumAmount\":50,\"discountPercentage\":15}"
    }
  },
  "cart": {
    "lines": [
      {
        "id": "gid://shopify/CartLine/1",
        "quantity": 2,
        "cost": {
          "amountPerQuantity": {
            "amount": "18.00",
            "currencyCode": "EUR"
          }
        },
        "merchandise": {
          "__typename": "ProductVariant",
          "id": "gid://shopify/ProductVariant/101",
          "title": "Ethiopia Yirgacheffe 250g",
          "product": {
            "inVipCollection": true
          }
        }
      },
      {
        "id": "gid://shopify/CartLine/2",
        "quantity": 1,
        "cost": {
          "amountPerQuantity": {
            "amount": "24.00",
            "currencyCode": "EUR"
          }
        },
        "merchandise": {
          "__typename": "ProductVariant",
          "id": "gid://shopify/ProductVariant/205",
          "title": "Kit Barista Pro V60
          "product": {
            "inVipCollection": true
          }
        }
      }
    ]
  }
}
FunctionRunResult — Output esperado (subtotal VIP: 60,00 € → aplica 15%)
output.json
{
  "discounts": [
    {
      "targets": [
        { "cartLine": { "id": "gid://shopify/CartLine/1" } },
        { "cartLine": { "id": "gid://shopify/CartLine/2" } }
      ],
      "value": {
        "percentage": { "value": "15" }
      },
      "message": "☕ Descuento VIP 15% — Cultiva Café"
    }
  ],
  "discountApplicationStrategy": "FIRST"
}

// Ahorro aplicado:
//   CartLine/1: 2 × 18,00€ = 36,00€  →  -5,40€  →  30,60€
//   CartLine/2: 1 × 24,00€ = 24,00€  →  -3,60€  →  20,40€
//   ─────────────────────────────────────────────────
//   Total ahorro: -9,00€
//   Total final:   51,00€ (antes 60,00€)
Build & Test
$ cd extensions/cultiva-vip-discount shopify app function build
✔ Compiled extensions/cultiva-vip-discount/dist/index.wasm Size: 48.3 kB (gzip: 18.7 kB) WebAssembly binary ready $ shopify app function run \ --input src/input.json --export run ✔ Function executed in 1.2ms Output matches expected FunctionRunResult ✔ Discount applied: 15% to 2 cart lines
scripts/validate.mjs
All checks passed
cultiva-vip-discount · revision 1 · artifact-id: c7f3a91b
Target API válido (Discount)
Output type correcto
GraphQL query bien formada
__typename en UNION types
camelCase en input query
Metafield config accedido
GID format en sample IDs
Estrategia FIRST declarada
Umbrales opcionales con ??
No network access (pure fn)
Metafield $app:config — configurable sin redeploy
# Actualizar config vía Admin API (GraphQL)
mutation {
  discountAutomaticAppCreate(automaticAppDiscount: {
    title: "VIP Collection 15%"
    functionId: "01J8XKZP7..."
    metafields: [{
      namespace: "$app"
      key: "config"
      type: "json"
      value: "{\"minimumAmount\":50,\"discountPercentage\":15}"
    }]
  }) {
    automaticAppDiscount { id title }
    userErrors { field message }
  }
}