--- title: Build a Discounts Allocator Function description: Learn how to build a Discounts Allocator Function to implement custom logic for discounts across line items. source_url: html: https://shopify.dev/docs/apps/build/discounts/build-discounts-allocator?extension=javascript md: https://shopify.dev/docs/apps/build/discounts/build-discounts-allocator.md?extension=javascript --- # Build a Discounts Allocator Function Feature Preview This Function API is available only in the [Discounts Allocator feature preview](https://shopify.dev/docs/api/developer-previews#discounts-allocator-preview). The Discounts Allocator Function will be available only to [Shopify Plus](https://www.shopify.com/plus) merchants. Building a Discounts Allocator Function allows for greater customization in defining discount strategies, like implementing custom logic to distribute discounts across line items in an order. ## What you'll learn In this tutorial, you'll create a Discounts Allocator Function that uses metafields to help define its custom allocation logic. The first metafield is applied at the order level. It sets a cap on the discount, ensuring that a 50% discount won't exceed a specified dollar amount. The second metafield is applied at the shop level. It sets a maximum discounted amount for a cart. ## Requirements [Create a Partner account](https://www.shopify.com/partners) [Create a development store](https://shopify.dev/docs/apps/tools/development-stores#create-a-development-store-to-test-your-app) [Create an app](https://shopify.dev/docs/apps/getting-started/create) Create a Remix app that has the `write_discounts` and `read_products` [access scopes](https://shopify.dev/docs/api/usage/access-scopes), using the [latest version of Shopify CLI](https://shopify.dev/docs/api/shopify-cli#upgrade). [Install Node.js](https://nodejs.org/en/download) Install Node.js 22 or higher. [Install the app](https://shopify.dev/docs/apps/build/scaffold-app#step-3-install-your-app-on-your-development-store) Install your app on the development store. ## Project ![](https://shopify.dev/images/logos/JS.svg)![](https://shopify.dev/images/logos/JS-dark.svg) JavaScript [View on GitHub](https://github.com/Shopify/tutorial-discount-functions-with-remix) ## Update your app's access scopes Your app requires the `write_discounts_allocator_functions` [access scope](https://shopify.dev/docs/api/usage/access-scopes) to create and update discounts. ### Update the `shopify.app.toml` file Add the `write_discounts_allocator_functions` scope to your app's `shopify.app.toml` file. ## shopify.app.toml ```toml # Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration client_id = "7e1ce6ae588ac87953ac011795fbecb9" name = "build-a-discount-function" handle = "build-a-discount-function" application_url = "https://costa-tariff-quarter-mega.trycloudflare.com" embedded = true [build] automatically_update_urls_on_dev = true dev_store_url = "quickstart-3a22a586.myshopify.com" include_config_on_deploy = true [access_scopes] scopes = "write_products, write_discounts, write_discounts_allocator_functions, read_products" [auth] redirect_urls = [ "https://costa-tariff-quarter-mega.trycloudflare.com/auth/callback", "https://costa-tariff-quarter-mega.trycloudflare.com/auth/shopify/callback", "https://costa-tariff-quarter-mega.trycloudflare.com/api/auth/callback" ] [webhooks] api_version = "2024-07" [pos] embedded = false ``` ### Start your app to preview changes 1. Save your updated configuration TOML file. 2. Start `app dev` if it's not already running: ## Terminal ```bash shopify app dev ``` The configuration TOML file changes will be applied automatically on the development store. ## Create the Discounts Allocator Function 1. Navigate to your app's directory: ## Terminal ```bash cd directory ``` 2. Run the following command to create a new Discounts Allocator Function: ## Terminal ```bash shopify app generate extension --template discounts_allocator --name discounts-allocator ``` 3. Choose the language that you want to use. For this tutorial, select `JavaScript`. ## Update the configuration ### Replace the content of the extension's `run.graphql` file This GraphQL query is structured to retrieve all necessary data for applying discounts to shopping cart items, including discount details, cart line items, and shop metafields. It ensures the function has access to discount rules, item specifics, and cap limits to accurately calculate and apply discounts according to predefined policies. ## extensions/discounts-allocator-js/src/run.graphql ```graphql query RunInput { discounts { id title code discountApplicationStrategy discountProposals { handle message value { __typename ... on FixedAmount { amount appliesToEachItem } ... on Percentage { value } } targets{ cartLineId quantity } } metafield(namespace: "testing-app-allocator", key: "single-discount-cap") { value } } presentmentCurrencyRate cart { lines { id quantity cost { amountPerQuantity { amount } } merchandise { __typename ... on ProductVariant { id title product { id title } } } } } shop { metafield(namespace: "testing-app-allocator", key: "per-cart-cap") { value } } } ``` Navigate to your extension's directory: ## Terminal ```bash cd extensions/discounts-allocator ``` Run the following command to regenerate types based on your input query: ## Terminal ```bash shopify app function typegen ``` ## Update the function logic ### Replace the content of `run.js` file. This function applies discounts to shopping cart items according to set rules and limits. It calculates discount amounts, enforces caps to prevent excessive reductions, and compiles the results and any errors into a structured output, ensuring fair and policy-compliant discount application. ## extensions/discounts-allocator-js/src/run.js ```javascript import { Decimal } from "decimal.js"; const TOTAL_DISCOUNTS_CAP_REACHED = "Maximum discount limit reached for this cart"; const SINGLE_DISCOUNT_CAP_REACHED = "Maximum discount limit reached for this discount"; // Helper function to extract the line index from a target's cartLineId function getTargetLineIndex(target) { return parseInt(target.cartLineId.slice(-1)); } // Helper function to calculate the price for a specific target based on its quantity function calculateCurrentTargetPrice(inputCartLines, target) { const targetLineIndex = getTargetLineIndex(target); const targetLine = inputCartLines[targetLineIndex]; return targetLine.cost.amountPerQuantity.amount * target.quantity; } export function run(input) { // Read the total discounts cap from the shop's metafield, defaulting to -1 if not set let totalDiscountsCap = parseFloat(input.shop.metafield?.value ?? "-1"); let totalDiscount = 0.0; // Initialize the output structure for line discounts let allLinesOutputDiscounts = input.cart.lines.map((line) => ({ cartLineId: line.id, quantity: line.quantity, allocations: [], })); let displayableErrors = []; // Iterate over each discount in the input for (const discount of input.discounts) { // Read the cap for the current discount from its metafield, defaulting to -1 if not set let currentDiscountCap = parseFloat(discount.metafield?.value ?? "-1"); let currentDiscountTotal = 0.0; // Process each discount proposal within the current discount for (const proposal of discount.discountProposals) { // Calculate the total price of all targets affected by the current proposal const totalTargetsPrice = proposal.targets.reduce((total, target) => { return total + calculateCurrentTargetPrice(input.cart.lines, target); }, 0); // Apply the discount to each target for (const target of proposal.targets) { const currentTargetPrice = calculateCurrentTargetPrice( input.cart.lines, target, ); const currentTargetRatio = currentTargetPrice / totalTargetsPrice; let lineDiscountAmount = 0.0; if (proposal.value.__typename == "FixedAmount") { if (proposal.value.appliesToEachItem) { lineDiscountAmount = proposal.value.amount * target.quantity; } else { lineDiscountAmount = proposal.value.amount * currentTargetRatio; } } else if (proposal.value.__typename == "Percentage") { lineDiscountAmount = (proposal.value.value / 100.0) * totalTargetsPrice * currentTargetRatio; } // Check and apply caps on the discount amount if ( currentDiscountCap >= 0.0 && currentDiscountTotal + lineDiscountAmount > currentDiscountCap ) { lineDiscountAmount = currentDiscountCap - currentDiscountTotal; displayableErrors.push({ discountId: discount.id.toString(), reason: SINGLE_DISCOUNT_CAP_REACHED, }); } if ( totalDiscountsCap >= 0.0 && totalDiscount + lineDiscountAmount > totalDiscountsCap ) { lineDiscountAmount = totalDiscountsCap - totalDiscount; displayableErrors.push({ discountId: discount.id.toString(), reason: TOTAL_DISCOUNTS_CAP_REACHED, }); } if (lineDiscountAmount === 0.0) { continue; } totalDiscount += lineDiscountAmount; currentDiscountTotal += lineDiscountAmount; const targetLineIndex = getTargetLineIndex(target); const targetAllocation = { discountProposalId: proposal.handle, amount: new Decimal(lineDiscountAmount), }; allLinesOutputDiscounts[targetLineIndex].allocations.push( targetAllocation, ); } } } // Filter out lines that have no allocations const lineDiscounts = allLinesOutputDiscounts.filter( (outputDiscount) => outputDiscount.allocations.length > 0, ); // Prepare the final output structure const output = { lineDiscounts, displayableErrors, }; return output; } ``` ## Define discount metafields for orders and the shop Use [the GraphiQL app](https://shopify-graphiql-app.shopifycloud.com/login) to create metafield definitions for the discount and shop. ### Create the discount metafield definition To set the maximum discount amount for each discount, use the [`metafieldDefinitionCreate`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/metafieldDefinitionCreate) mutation with `ownerType: "DISCOUNT"`. ## graphql\_examples/discountMetafieldDefinitionCreate.graphql ```graphql mutation { metafieldDefinitionCreate( definition: { name: "Single discount Cap" namespace: "testing-app-allocator" key: "single-discount-cap" type: "number_decimal" description: "Maximum reduction value for a single discount" validations: [{ name: "min", value: "0" }] ownerType: DISCOUNT } ) { createdDefinition { id name } userErrors { field message code } } } ``` ### Create the shop metafield definition To set the maximum discount amount for the cart, use the [`metafieldDefinitionCreate`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/metafieldDefinitionCreate) mutation with `ownerType: "SHOP"`. ## graphql\_examples/shopMetafieldDefinitionCreate.graphql ```graphql mutation { metafieldDefinitionCreate( definition: { name: "Maximum Discount Per Cart" namespace: "testing-app-allocator" key: "per-cart-cap" type: "number_decimal" description: "The Maximum discount value applied to a single cart" validations: [{ name: "min", value: "0" }] ownerType: SHOP } ) { createdDefinition { id name } userErrors { field message code } } } ``` ## Create the frontend UI for registering your Discounts Allocator Function After a merchant installs your app, they'll need to register the Discounts Allocator Function to actively allocate discounts. Caution You're replacing the Shopify discount engine with the Discounts Allocator Function. Your Function will take precedence over most [discount features that are built by Shopify](https://help.shopify.com/en/manual/discounts/combining-discounts/discount-combinations). ### Create a new route for the Discounts Allocator Function In `app/routes`, create a new file named `app.discounts-allocator.$functionId.jsx`. *** The Shopify Remix app template uses file-based routing, so the filename determines the page's URL. The `$` prefix indicates that `functionId` is a [dynamic segment](https://remix.run/docs/en/main/guides/routing#dynamic-segments). The path for this page is `/app/discounts-allocator/{functionId}`. ## app/routes/app.discounts-allocator.$functionId.jsx ```jsx import { useEffect } from "react"; import { json } from "@remix-run/node"; import { useActionData, useNavigate, useSubmit } from "@remix-run/react"; import { Page, Layout, BlockStack, Card, Banner, Text } from "@shopify/polaris"; import { authenticate } from "../shopify.server"; export const action = async ({ params, request }) => { const functionExtensionId = params.functionId; const registerDiscountsAllocatorMutation = ` #graphql mutation registerDiscountsAllocator($functionExtensionId: String!) { discountsAllocatorFunctionRegister(functionExtensionId: $functionExtensionId) { userErrors { code message field } } } `; if (functionExtensionId !== null) { const { admin } = await authenticate.admin(request); const response = await admin.graphql(registerDiscountsAllocatorMutation, { variables: { functionExtensionId: functionExtensionId, }, }); const responseJson = await response.json(); const errors = responseJson.data.discountsAllocatorFunctionRegister?.userErrors; return json({ errors }); } return json({ errors: ["No functionExtensionId provided"] }); }; export default function DiscountsAllocator() { const actionData = useActionData(); const submitForm = useSubmit(); const navigate = useNavigate(); useEffect(() => { if (actionData?.errors && actionData?.errors.length === 0) { shopify.toast.show( "Discounts Allocator Function registered successfully!", ); } }, [actionData]); const errorBanner = actionData?.errors.length ? ( ) : null; const actions = { backAction: { content: "Home", onAction: () => navigate("/app"), }, primaryAction: { content: "Register Discounts allocator", onAction: () => submitForm({}, { method: "post" }), }, }; return ( Add more awesome details about your allocator here! (Like ability to add metafields) {errorBanner} ); } ``` ### Add a Remix `action` function to handle form submission Add a Remix `action` to `app.discounts-allocator.$functionId.jsx` to handle the form submission. This function calls the `discountsAllocatorFunctionRegister` mutation to register the Discounts Allocator Function. ## app/routes/app.discounts-allocator.$functionId.jsx ```jsx import { useEffect } from "react"; import { json } from "@remix-run/node"; import { useActionData, useNavigate, useSubmit } from "@remix-run/react"; import { Page, Layout, BlockStack, Card, Banner, Text } from "@shopify/polaris"; import { authenticate } from "../shopify.server"; export const action = async ({ params, request }) => { const functionExtensionId = params.functionId; const registerDiscountsAllocatorMutation = ` #graphql mutation registerDiscountsAllocator($functionExtensionId: String!) { discountsAllocatorFunctionRegister(functionExtensionId: $functionExtensionId) { userErrors { code message field } } } `; if (functionExtensionId !== null) { const { admin } = await authenticate.admin(request); const response = await admin.graphql(registerDiscountsAllocatorMutation, { variables: { functionExtensionId: functionExtensionId, }, }); const responseJson = await response.json(); const errors = responseJson.data.discountsAllocatorFunctionRegister?.userErrors; return json({ errors }); } return json({ errors: ["No functionExtensionId provided"] }); }; export default function DiscountsAllocator() { const actionData = useActionData(); const submitForm = useSubmit(); const navigate = useNavigate(); useEffect(() => { if (actionData?.errors && actionData?.errors.length === 0) { shopify.toast.show( "Discounts Allocator Function registered successfully!", ); } }, [actionData]); const errorBanner = actionData?.errors.length ? ( ) : null; const actions = { backAction: { content: "Home", onAction: () => navigate("/app"), }, primaryAction: { content: "Register Discounts allocator", onAction: () => submitForm({}, { method: "post" }), }, }; return ( Add more awesome details about your allocator here! (Like ability to add metafields) {errorBanner} ); } ``` ### Create the UI for registering the Discounts Allocator Function Use the `primaryAction` in the `Page` component to expose a button that calls the `action` function. This button allows merchants to register the Discounts Allocator Function. ## app/routes/app.discounts-allocator.$functionId.jsx ```jsx import { useEffect } from "react"; import { json } from "@remix-run/node"; import { useActionData, useNavigate, useSubmit } from "@remix-run/react"; import { Page, Layout, BlockStack, Card, Banner, Text } from "@shopify/polaris"; import { authenticate } from "../shopify.server"; export const action = async ({ params, request }) => { const functionExtensionId = params.functionId; const registerDiscountsAllocatorMutation = ` #graphql mutation registerDiscountsAllocator($functionExtensionId: String!) { discountsAllocatorFunctionRegister(functionExtensionId: $functionExtensionId) { userErrors { code message field } } } `; if (functionExtensionId !== null) { const { admin } = await authenticate.admin(request); const response = await admin.graphql(registerDiscountsAllocatorMutation, { variables: { functionExtensionId: functionExtensionId, }, }); const responseJson = await response.json(); const errors = responseJson.data.discountsAllocatorFunctionRegister?.userErrors; return json({ errors }); } return json({ errors: ["No functionExtensionId provided"] }); }; export default function DiscountsAllocator() { const actionData = useActionData(); const submitForm = useSubmit(); const navigate = useNavigate(); useEffect(() => { if (actionData?.errors && actionData?.errors.length === 0) { shopify.toast.show( "Discounts Allocator Function registered successfully!", ); } }, [actionData]); const errorBanner = actionData?.errors.length ? ( ) : null; const actions = { backAction: { content: "Home", onAction: () => navigate("/app"), }, primaryAction: { content: "Register Discounts allocator", onAction: () => submitForm({}, { method: "post" }), }, }; return ( Add more awesome details about your allocator here! (Like ability to add metafields) {errorBanner} ); } ``` ### Update the UI path In `extensions/discounts-allocator/shopify.extension.toml`, populate the `create` and `details` setting in the `[extensions.ui.paths]` section. This change is automatically applied as long as you're running `dev`. *** The settings in the `shopify.extension.toml` file define the URLs that Shopify uses to enable merchants to create and edit discounts based on your Function. Shopify automatically fills in any [dynamic tokens](https://shopify.dev/docs/apps/build/functions/input-output/metafields-for-input-queries#dynamic-id-values) in these URLs. ## extensions/discounts-allocator-js/shopify.extension.toml ```toml api_version = "unstable" [[extensions]] handle = "discounts-allocator-js" name = "t:name" description = "t:description" type = "function" [[extensions.targeting]] target = "purchase.discounts-allocator.run" input_query = "src/run.graphql" export = "run" [extensions.build] command = "" path = "dist/function.wasm" [extensions.ui.paths] create = "/" details = "/app/discounts-allocator/:functionId" ``` ## Register your Discounts Allocator Function 1. If your app isn't already running, then start it using Shopify CLI: ## Terminal ```terminal shopify app dev ``` 2. Follow the CLI prompts to preview your app, and install or view it on your development store. 3. In your development store, navigate to `/app/discounts-allocator/function-handle` and register your Discounts Allocator Function. ![The UI for register the discounts allocator](https://cdn.shopify.com/shopifycloud/shopify-dev/production/assets/assets/apps/discounts/discounts-allocator-register-CypcgBRi.png) Info The `functionId` is a dynamic segment in the route. It's used to identify the Discounts Allocator Function that the merchant is registering. You can find your `functionId` in your app's `.env` file or in your [Partner Dashboard](https://partners.shopify.com/current/apps) if you navigate to `Apps > "your app" > Extensions > discounts-allocator-js`. Troubleshooting If you receive a `Could not find Function` error, then confirm the following: * The Function handle is correct. * You've installed the app on your development store. * [Development store preview](#preview-the-function-on-a-development-store) is enabled in the Partner Dashboard. * Your app has the `write_discounts` access scope. ## Create a discount and build a cart To test the `single-discount-cap` capability of your allocator, prepare an automatic discount and build a cart to apply the discount. 1. In your Shopify admin, go to **Discounts**. 2. Click the **Create discount** button near the top right of your page and select **Amount off products**. Fill in the values for the discount: * For **Method**, use **Automatic**. * For **Discount percentage**, use **50**. 3. Open your development store and build a cart with some eligible products. The discount amount applied should be **50 percent off**. ## Test your Discounts Allocator Function with a Single Discount Cap ### Set the maximum eligible amount for individual discounts Use the [`metafieldsSet`](https://shopify.dev/docs/api/customer/latest/mutations/metafieldsSet) mutation to set the `single-discount-cap` metafield for the discount you created. ## graphql\_examples/singleCapDiscountMutation.graphql ```graphql mutation { metafieldsSet( metafields: [ { key: "single-discount-cap" namespace: "testing-app-allocator" ownerId: "gid://shopify/DiscountNode/" type: "number_decimal" value: "" } ] ) { metafields { key namespace value createdAt updatedAt } userErrors { field message code } } } ``` ### Replace the placeholders Replace `DISCOUNT_ID` with the `ID` of the discount you created and replace `SINGLE_DISCOUNT_CAP` with the desired cap, for example `500`. ## graphql\_examples/singleCapDiscountMutation.graphql ```graphql mutation { metafieldsSet( metafields: [ { key: "single-discount-cap" namespace: "testing-app-allocator" ownerId: "gid://shopify/DiscountNode/" type: "number_decimal" value: "" } ] ) { metafields { key namespace value createdAt updatedAt } userErrors { field message code } } } ``` *** Notice that the discount amount applied to your cart is capped at the value you set in the metafield when you refresh the cart. ## Create a maximum discount amount for a cart ### Set the maximum discount amount for a cart Use the [`metafieldsSet`](https://shopify.dev/docs/api/customer/latest/mutations/metafieldsSet) mutation in your shop to utilize the `per-cart-cap` capability of your allocator. ## graphql\_examples/perCartDiscountMetafieldMutation.graphql ```graphql mutation { metafieldsSet( metafields: [ { key: "per-cart-cap" namespace: "testing-app-allocator" ownerId: "gid://shopify/Shop/" type: "number_decimal" value: "" } ] ) { metafields { key namespace value createdAt updatedAt } userErrors { field message code } } } ``` ### Replace the placeholders Replace `SHOP_ID` with the `ID` of your shop and replace `PER_CART_CAP` with the desired per cart cap, for example `50`. ## graphql\_examples/perCartDiscountMetafieldMutation.graphql ```graphql mutation { metafieldsSet( metafields: [ { key: "per-cart-cap" namespace: "testing-app-allocator" ownerId: "gid://shopify/Shop/" type: "number_decimal" value: "" } ] ) { metafields { key namespace value createdAt updatedAt } userErrors { field message code } } } ``` ## Test your Discounts Allocator Function with a Per Cart Cap 1. Click the **Create discount** button and select **Amount off order**. Fill in the values for the discount: * For **Method**, use **Automatic**. * For **Fixed Amount**, use **500** or any amount higher than your `PER_CART_CAP`. 1. Go to the cart you created and refresh the page, the total savings will be capped at the `PER_CART_CAP` value you set in the metafield. ## Deploy your app When you're ready to release your changes to users, you can create and release an [app version](https://shopify.dev/docs/apps/launch/deployment/app-versions). An app version is a snapshot of your app configuration and all extensions. 1. Navigate to your app directory. 2. Run the following command. Optionally, you can provide a name or message for the version using the `--version` and `--message` flags. ## Terminal ```terminal shopify app deploy ``` Releasing an app version replaces the current active version that's served to stores that have your app installed. It might take several minutes for app users to be upgraded to the new version. Tip If you want to create a version, but avoid releasing it to users, then run the `deploy` command with a `--no-release` flag. You can release the unreleased app version using Shopify CLI's [`release`](https://shopify.dev/docs/api/shopify-cli/app/app-release) command, or through the Dev Dashboard. ## shopify.app.toml ```toml # Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration client_id = "7e1ce6ae588ac87953ac011795fbecb9" name = "build-a-discount-function" handle = "build-a-discount-function" application_url = "https://costa-tariff-quarter-mega.trycloudflare.com" embedded = true [build] automatically_update_urls_on_dev = true dev_store_url = "quickstart-3a22a586.myshopify.com" include_config_on_deploy = true [access_scopes] scopes = "write_products, write_discounts, write_discounts_allocator_functions, read_products" [auth] redirect_urls = [ "https://costa-tariff-quarter-mega.trycloudflare.com/auth/callback", "https://costa-tariff-quarter-mega.trycloudflare.com/auth/shopify/callback", "https://costa-tariff-quarter-mega.trycloudflare.com/api/auth/callback" ] [webhooks] api_version = "2024-07" [pos] embedded = false ``` ## extensions/discounts-allocator-js/src/run.graphql ```graphql query RunInput { discounts { id title code discountApplicationStrategy discountProposals { handle message value { __typename ... on FixedAmount { amount appliesToEachItem } ... on Percentage { value } } targets{ cartLineId quantity } } metafield(namespace: "testing-app-allocator", key: "single-discount-cap") { value } } presentmentCurrencyRate cart { lines { id quantity cost { amountPerQuantity { amount } } merchandise { __typename ... on ProductVariant { id title product { id title } } } } } shop { metafield(namespace: "testing-app-allocator", key: "per-cart-cap") { value } } } ``` ## extensions/discounts-allocator-js/src/run.js ```javascript import { Decimal } from "decimal.js"; const TOTAL_DISCOUNTS_CAP_REACHED = "Maximum discount limit reached for this cart"; const SINGLE_DISCOUNT_CAP_REACHED = "Maximum discount limit reached for this discount"; // Helper function to extract the line index from a target's cartLineId function getTargetLineIndex(target) { return parseInt(target.cartLineId.slice(-1)); } // Helper function to calculate the price for a specific target based on its quantity function calculateCurrentTargetPrice(inputCartLines, target) { const targetLineIndex = getTargetLineIndex(target); const targetLine = inputCartLines[targetLineIndex]; return targetLine.cost.amountPerQuantity.amount * target.quantity; } export function run(input) { // Read the total discounts cap from the shop's metafield, defaulting to -1 if not set let totalDiscountsCap = parseFloat(input.shop.metafield?.value ?? "-1"); let totalDiscount = 0.0; // Initialize the output structure for line discounts let allLinesOutputDiscounts = input.cart.lines.map((line) => ({ cartLineId: line.id, quantity: line.quantity, allocations: [], })); let displayableErrors = []; // Iterate over each discount in the input for (const discount of input.discounts) { // Read the cap for the current discount from its metafield, defaulting to -1 if not set let currentDiscountCap = parseFloat(discount.metafield?.value ?? "-1"); let currentDiscountTotal = 0.0; // Process each discount proposal within the current discount for (const proposal of discount.discountProposals) { // Calculate the total price of all targets affected by the current proposal const totalTargetsPrice = proposal.targets.reduce((total, target) => { return total + calculateCurrentTargetPrice(input.cart.lines, target); }, 0); // Apply the discount to each target for (const target of proposal.targets) { const currentTargetPrice = calculateCurrentTargetPrice( input.cart.lines, target, ); const currentTargetRatio = currentTargetPrice / totalTargetsPrice; let lineDiscountAmount = 0.0; if (proposal.value.__typename == "FixedAmount") { if (proposal.value.appliesToEachItem) { lineDiscountAmount = proposal.value.amount * target.quantity; } else { lineDiscountAmount = proposal.value.amount * currentTargetRatio; } } else if (proposal.value.__typename == "Percentage") { lineDiscountAmount = (proposal.value.value / 100.0) * totalTargetsPrice * currentTargetRatio; } // Check and apply caps on the discount amount if ( currentDiscountCap >= 0.0 && currentDiscountTotal + lineDiscountAmount > currentDiscountCap ) { lineDiscountAmount = currentDiscountCap - currentDiscountTotal; displayableErrors.push({ discountId: discount.id.toString(), reason: SINGLE_DISCOUNT_CAP_REACHED, }); } if ( totalDiscountsCap >= 0.0 && totalDiscount + lineDiscountAmount > totalDiscountsCap ) { lineDiscountAmount = totalDiscountsCap - totalDiscount; displayableErrors.push({ discountId: discount.id.toString(), reason: TOTAL_DISCOUNTS_CAP_REACHED, }); } if (lineDiscountAmount === 0.0) { continue; } totalDiscount += lineDiscountAmount; currentDiscountTotal += lineDiscountAmount; const targetLineIndex = getTargetLineIndex(target); const targetAllocation = { discountProposalId: proposal.handle, amount: new Decimal(lineDiscountAmount), }; allLinesOutputDiscounts[targetLineIndex].allocations.push( targetAllocation, ); } } } // Filter out lines that have no allocations const lineDiscounts = allLinesOutputDiscounts.filter( (outputDiscount) => outputDiscount.allocations.length > 0, ); // Prepare the final output structure const output = { lineDiscounts, displayableErrors, }; return output; } ``` ## graphql\_examples/discountMetafieldDefinitionCreate.graphql ```graphql mutation { metafieldDefinitionCreate( definition: { name: "Single discount Cap" namespace: "testing-app-allocator" key: "single-discount-cap" type: "number_decimal" description: "Maximum reduction value for a single discount" validations: [{ name: "min", value: "0" }] ownerType: DISCOUNT } ) { createdDefinition { id name } userErrors { field message code } } } ``` ## graphql\_examples/shopMetafieldDefinitionCreate.graphql ```graphql mutation { metafieldDefinitionCreate( definition: { name: "Maximum Discount Per Cart" namespace: "testing-app-allocator" key: "per-cart-cap" type: "number_decimal" description: "The Maximum discount value applied to a single cart" validations: [{ name: "min", value: "0" }] ownerType: SHOP } ) { createdDefinition { id name } userErrors { field message code } } } ``` ## app/routes/app.discounts-allocator.$functionId.jsx ```jsx import { useEffect } from "react"; import { json } from "@remix-run/node"; import { useActionData, useNavigate, useSubmit } from "@remix-run/react"; import { Page, Layout, BlockStack, Card, Banner, Text } from "@shopify/polaris"; import { authenticate } from "../shopify.server"; export const action = async ({ params, request }) => { const functionExtensionId = params.functionId; const registerDiscountsAllocatorMutation = ` #graphql mutation registerDiscountsAllocator($functionExtensionId: String!) { discountsAllocatorFunctionRegister(functionExtensionId: $functionExtensionId) { userErrors { code message field } } } `; if (functionExtensionId !== null) { const { admin } = await authenticate.admin(request); const response = await admin.graphql(registerDiscountsAllocatorMutation, { variables: { functionExtensionId: functionExtensionId, }, }); const responseJson = await response.json(); const errors = responseJson.data.discountsAllocatorFunctionRegister?.userErrors; return json({ errors }); } return json({ errors: ["No functionExtensionId provided"] }); }; export default function DiscountsAllocator() { const actionData = useActionData(); const submitForm = useSubmit(); const navigate = useNavigate(); useEffect(() => { if (actionData?.errors && actionData?.errors.length === 0) { shopify.toast.show( "Discounts Allocator Function registered successfully!", ); } }, [actionData]); const errorBanner = actionData?.errors.length ? (
    {actionData?.errors?.map((error, index) => { return
  • {error.message}
  • ; })}
) : null; const actions = { backAction: { content: "Home", onAction: () => navigate("/app"), }, primaryAction: { content: "Register Discounts allocator", onAction: () => submitForm({}, { method: "post" }), }, }; return ( Add more awesome details about your allocator here! (Like ability to add metafields) {errorBanner} ); } ``` ## extensions/discounts-allocator-js/shopify.extension.toml ```toml api_version = "unstable" [[extensions]] handle = "discounts-allocator-js" name = "t:name" description = "t:description" type = "function" [[extensions.targeting]] target = "purchase.discounts-allocator.run" input_query = "src/run.graphql" export = "run" [extensions.build] command = "" path = "dist/function.wasm" [extensions.ui.paths] create = "/" details = "/app/discounts-allocator/:functionId" ``` ## graphql\_examples/singleCapDiscountMutation.graphql ```graphql mutation { metafieldsSet( metafields: [ { key: "single-discount-cap" namespace: "testing-app-allocator" ownerId: "gid://shopify/DiscountNode/" type: "number_decimal" value: "" } ] ) { metafields { key namespace value createdAt updatedAt } userErrors { field message code } } } ``` ## graphql\_examples/perCartDiscountMetafieldMutation.graphql ```graphql mutation { metafieldsSet( metafields: [ { key: "per-cart-cap" namespace: "testing-app-allocator" ownerId: "gid://shopify/Shop/" type: "number_decimal" value: "" } ] ) { metafields { key namespace value createdAt updatedAt } userErrors { field message code } } } ``` ## Tutorial complete! You've successfully created a Discounts Allocator Function that uses metafields to help define its custom allocation logic. Now, you can use this Function to apply discounts. *** ### Next Steps [![](https://shopify.dev/images/icons/32/build.png)![](https://shopify.dev/images/icons/32/build-dark.png)](https://shopify.dev/docs/apps/build/discounts/build-ui-extension) [Build a UI extension to configure your discount Function](https://shopify.dev/docs/apps/build/discounts/build-ui-extension) [Add configuration to your discounts experience using metafields and build a user interface for your Function.](https://shopify.dev/docs/apps/build/discounts/build-ui-extension) [![](https://shopify.dev/images/icons/32/gear.png)![](https://shopify.dev/images/icons/32/gear-dark.png)](https://shopify.dev/docs/apps/build/discounts/build-ui-with-react-router) [Build a React Router app to configure your discount Function](https://shopify.dev/docs/apps/build/discounts/build-ui-with-react-router) [Add configuration to your discounts experience using metafields and build a user interface for your Function.](https://shopify.dev/docs/apps/build/discounts/build-ui-with-react-router) [![](https://shopify.dev/images/icons/32/gear.png)![](https://shopify.dev/images/icons/32/gear-dark.png)](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [Review the UX guidelines](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [Review the UX guidelines to learn how to implement discounts in user interfaces.](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [![](https://shopify.dev/images/icons/32/app.png)![](https://shopify.dev/images/icons/32/app-dark.png)](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions) [Learn more about deploying app versions](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions) [Learn more about deploying app versions to Shopify](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions)