--- title: Build a Discount Function that rejects invalid codes description: >- Learn how to build a Discount Function that can reject discount codes with custom messaging. source_url: html: >- https://shopify.dev/docs/apps/build/discounts/discount-rejections?extension=rust md: >- https://shopify.dev/docs/apps/build/discounts/discount-rejections.md?extension=rust --- # Build a Discount Function that rejects invalid codes Coming soon This feature will be rolling out in the coming weeks. In this tutorial, you'll build a Shopify Function that rejects discount codes with custom error messages by performing custom validation logic. ## What you'll learn In this tutorial, you'll learn how to do the following tasks: * Use the Shopify CLI to add a Discount Function to an app * Query entered discount codes in your Function input * Implement custom validation logic for discount codes * Return rejection operations with custom error messages * Use GraphiQL to create a discount and configure it to work with your Function * Test discount code rejections in checkout * Review Function execution logs for rejected discount codes * Deploy your Function to Shopify ## Requirements [Create a Partner account](https://www.shopify.com/partners) [Create a dev 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 an app that has the `write_discounts` [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. [You've installed rust](https://www.rust-lang.org/tools/install) On Windows, Rust requires the [Microsoft C++ Build Tools](https://docs.microsoft.com/en-us/windows/dev-environment/rust/setup). Make sure to select the **Desktop development with C++** workload when installing the tools. [You've installed the `wasm32-unknown-unknown` target](https://doc.rust-lang.org/rustc/platform-support/wasm32-unknown-unknown.html) To ensure compatibility with the latest version of Rust, install the `wasm32-unknown-unknown` target. ## Project Rust [View on GitHub](https://github.com/Shopify/discounts-reference-app/tree/main/examples/rust/discount-rejection) ## Create the Discount Function To create a Discount Function that can reject invalid discount codes, you'll use the Shopify CLI, which generates starter code for your Function. 1. Navigate to your app's directory: ## Terminal ```bash cd directory ``` 2. Run the following command to create a new Discount Function: ## Terminal ```bash shopify app generate extension --template discount --name discount-rejection-function-rs ``` 3. Choose the language that you want to use. For this tutorial, select `Rust`. ## Review the Discount Function configuration The `shopify.extension.toml` file defines the Discount Function's metadata, targets, and build configuration. ### Targets The `[[extensions.targeting]]` sections define the Function targets. Each section includes a `target`, `input_query`, and `export` property. The [`target`](https://shopify.dev/docs/apps/build/app-extensions/configure-app-extensions#targets) property defines where the Function is applied. Note For simplicity in this tutorial, we will focus exclusively on the `cart.lines.discounts.generate.run` target. You can remove the following: * Remove `cart.delivery-options.discounts.generate.run` target from the `shopify.extension.toml` file. * Remove the target's associated files `./src/cart_delivery_options*` - Remove `pub mod cart_delivery_options_discounts_generate_run;` from the `src/main.rs` file and its associated schema typegen block `#[query("src/cart_delivery_options_discounts_generate_run.graphql")]` . The `input_query` property is the path to the Function's input query file. This file defines the Function's [input parameters](https://shopify.dev/docs/api/functions/reference/discount/graphql/input), including the `enteredDiscountCodes` field that you'll use for validation. The `export` property specifies the build entrypoint that will run for a specified target. This will usually be the function name of your main export. ## extensions/discount-function/shopify.extension.toml ```toml api_version = "unstable" [[extensions]] name = "t:name" handle = "discount-function-rs" uid = "a1a9900e-8776-2411-a444-bf3a2aff01a7c360e941" type = "function" description = "t:description" [[extensions.targeting]] target = "cart.lines.discounts.generate.run" input_query = "src/cart_lines_discounts_generate_run.graphql" export = "cart_lines_discounts_generate_run" [extensions.build] command = "cargo build --target=wasm32-wasip1 --release" path = "target/wasm32-wasip1/release/discount-function-rs.wasm" watch = [ "src/**/*.rs" ] ``` Note Depending on your app type, you might have to define the `[extensions.ui]` or `[extensions.ui.paths]` section. Refer to [Build a discounts UI with Admin UI Extensions](https://shopify.dev/docs/apps/build/discounts/build-ui-extension) or [Build a discounts UI with React Router](https://shopify.dev/docs/apps/build/discounts/build-ui-with-react-router) to learn more about building UIs to configure Functions. ## Update your Function to reject invalid discount codes Rejecting invalid discount codes allows to provide a custom error message to the customer when a discount code is not meeting validation rules. ### Update your input query to include entered discount codes Update your `cart_lines_discounts_generate_run.graphql` file to query the `enteredDiscountCodes` field. `enteredDiscountCodes` returns an array of active discount codes. Each code includes a [rejectable](https://shopify.dev/docs/api/functions/unstable/discount?contentToggled=true#Input.fields.enteredDiscountCodes.rejectable) boolean indicating whether your Function can reject this discount code. ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.graphql ```graphql query Input { enteredDiscountCodes { code rejectable } } ``` ### Implement discount rejection logic Update your `cart_lines_discounts_generate_run` Function to implement custom validation logic and return rejection operations. This example keeps only the last entered discount code with the `INF-` prefix. ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.rs ```rust use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[shopify_function] fn cart_lines_discounts_generate_run( input: schema::cart_lines_discounts_generate_run::Input, ) -> Result { let all_influencer_codes: Vec<_> = input .entered_discount_codes() .iter() .filter(|code| code.code().to_lowercase().starts_with("inf-")) .collect(); let rejectable_influencer_codes: Vec = all_influencer_codes .iter() .filter(|code| *code.rejectable()) .map(|code| code.code().to_string()) .collect(); let non_rejectable_exists = all_influencer_codes.iter().any(|code| !*code.rejectable()); let codes_to_reject = if non_rejectable_exists { rejectable_influencer_codes } else { // Take all except the last one rejectable_influencer_codes .split_last() .map(|(_, all_but_last)| all_but_last.to_vec()) .unwrap_or_default() }; if codes_to_reject.is_empty() { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![schema::CartOperation::EnteredDiscountCodesReject( schema::EnteredDiscountCodesRejectOperation { codes: codes_to_reject .iter() .map(|code| schema::RejectedDiscountCode { code: code.to_string(), }) .collect(), message: format!( "Only one influencer code allowed. Rejected: {}", codes_to_reject.join(", ") ), }, )], }) } ``` ### Review the rejection operation The `enteredDiscountCodesReject` operation has the following structure: ```json { "enteredDiscountCodesReject": { "codes": [ { "code": "INVALID_CODE_1" }, { "code": "INVALID_CODE_2" } ], "message": "INVALID_CODE_1 and INVALID_CODE_2 are not valid at the moment." } } ``` To reject multiple discount codes with different messages, you can return multiple rejection operations. Automatic discount Functions can only reject discount codes that are part of the `enteredDiscountCodes` input and are `rejectable`. Note The message in the rejection operation displays in checkout and should be localized. See more about [providing localized content](https://shopify.dev/docs/apps/build/functions/localization-practices-shopify-functions#providing-translated-content). ### 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 a discount in your store In this step, you'll use GraphiQL to create an automatic discount. This discount will be linked to your Function that validates and rejects discount codes. ### Open the Graphi​QL interface for the Graph​QL Admin API 1. Open the GraphiQL interface by pressing `g` in the terminal window where you started your app. 2. On the GraphiQL interface, for **API Version**, select the latest stable release. ### Get the handle of your Function 1. Use your function handle in GraphQL mutations instead of querying for the function ID. Your function handle is defined in `shopify.extension.toml` as `handle`: ## shopify.extension.toml ```toml [[extensions]] name = "discount-rejection-function" handle = "discount-rejection-function-rs" type = "function" uid = "3d664979-ccd6-e9dd-4497-41289ece62373715032a" ``` Note If you're upgrading to API version 2025-10 or later, you no longer need to query for function IDs. Use your stable function handle instead, which remains consistent across environments. ### Create the discount To create a discount in your store, use the [`discountAutomaticAppCreate`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/discountAutomaticAppCreate) mutation. Discount rejection operations are only supported for automatic discounts. 1. For `functionHandle`, provide the Function handle from the [previous step](#get-the-handle-of-your-function). 2. Add `PRODUCT`, `ORDER` or both [`discountClasses`](https://shopify.dev/docs/api/functions/reference/discount/graphql/common-objects/discountclass) to your discount input. You should receive a GraphQL response that includes the ID of the created discount. If the response includes any `userErrors`, then review them, check that your mutation and `functionHandle` are correct, and try the request again. ## discountAutomaticAppCreate.graphql ```graphql mutation { discountAutomaticAppCreate( automaticAppDiscount: { title: "Discount Rejection" functionHandle: "YOUR_FUNCTION_HANDLE_HERE" discountClasses: [PRODUCT, ORDER] startsAt: "2025-01-01T00:00:00" } ) { automaticAppDiscount { discountId } userErrors { field message } } } ``` Troubleshooting If you receive a `Could not find Function` error, then confirm the following: * `shopify app dev` is running. * The function handle is correct. * Your app has the `write_discounts` access scope. ## Test discount code rejections ### Create codes that can be rejected To test your function's rejection logic, you'll need to create two discount codes. [Create two product or order discount codes](https://help.shopify.com/manual/discounts/create-discount-codes) (for example, `INF-ALICE` and `INF-BOB`). Both codes must: * Use the prefix `INF-` * Be set to combine with all other discounts ### Reject discount codes in checkout 1. Create a cart and navigate to checkout. 2. Enter one of the two discount codes so it applies to the cart. 3. Enter the other discount code. This new code applies and the former code is rejected. When a discount code is rejected, a message displays as a warning banner in checkout. ![An image showing a rejected discount code in checkout.](https://shopify.dev/assets/assets/apps/discounts/rejected-discount-code-CYk5Hdci.png) ### Review the Function execution 1. In the terminal where `shopify app dev` is running, review your Function executions. When [testing Functions on development stores](https://shopify.dev/docs/apps/build/functions/test-debug-functions#test-your-function-on-a-development-store), the `dev` output shows Function executions, debug logs you've added, and a link to a local file containing full execution details. 2. In a new terminal window, use the Shopify CLI command [`app function replay`](https://shopify.dev/docs/api/shopify-cli/app/app-function-replay) to [replay a Function execution locally](https://shopify.dev/docs/apps/build/functions/test-debug-functions#execute-the-function-locally-using-shopify-cli). This lets you debug your Function without triggering it again on Shopify. ## Terminal ```terminal shopify app function replay ``` 3. Select the Function execution from the top of the list. Press `q` to quit when you are finished debugging. In the Function run logs, you'll see: * The entered discount codes that were queried * The rejection operations returned with the custom message ## 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. ## extensions/discount-function/shopify.extension.toml ```toml api_version = "unstable" [[extensions]] name = "t:name" handle = "discount-function-rs" uid = "a1a9900e-8776-2411-a444-bf3a2aff01a7c360e941" type = "function" description = "t:description" [[extensions.targeting]] target = "cart.lines.discounts.generate.run" input_query = "src/cart_lines_discounts_generate_run.graphql" export = "cart_lines_discounts_generate_run" [extensions.build] command = "cargo build --target=wasm32-wasip1 --release" path = "target/wasm32-wasip1/release/discount-function-rs.wasm" watch = [ "src/**/*.rs" ] ``` ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.graphql ```graphql query Input { enteredDiscountCodes { code rejectable } } ``` ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.rs ```rust use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[shopify_function] fn cart_lines_discounts_generate_run( input: schema::cart_lines_discounts_generate_run::Input, ) -> Result { let all_influencer_codes: Vec<_> = input .entered_discount_codes() .iter() .filter(|code| code.code().to_lowercase().starts_with("inf-")) .collect(); let rejectable_influencer_codes: Vec = all_influencer_codes .iter() .filter(|code| *code.rejectable()) .map(|code| code.code().to_string()) .collect(); let non_rejectable_exists = all_influencer_codes.iter().any(|code| !*code.rejectable()); let codes_to_reject = if non_rejectable_exists { rejectable_influencer_codes } else { // Take all except the last one rejectable_influencer_codes .split_last() .map(|(_, all_but_last)| all_but_last.to_vec()) .unwrap_or_default() }; if codes_to_reject.is_empty() { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![schema::CartOperation::EnteredDiscountCodesReject( schema::EnteredDiscountCodesRejectOperation { codes: codes_to_reject .iter() .map(|code| schema::RejectedDiscountCode { code: code.to_string(), }) .collect(), message: format!( "Only one influencer code allowed. Rejected: {}", codes_to_reject.join(", ") ), }, )], }) } ``` ## discountAutomaticAppCreate.graphql ```graphql mutation { discountAutomaticAppCreate( automaticAppDiscount: { title: "Discount Rejection" functionHandle: "YOUR_FUNCTION_HANDLE_HERE" discountClasses: [PRODUCT, ORDER] startsAt: "2025-01-01T00:00:00" } ) { automaticAppDiscount { discountId } userErrors { field message } } } ``` ## Tutorial complete! You've successfully created a Discount Function that can reject invalid discount codes and display a custom error message. This gives customers clear explanations for why their discount codes don't work. *** ### Next Steps [Build a UI extension to configure your discount Function\ \ 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)[Build a React Router app to configure your discount Function\ \ 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)[Add network access to your discount Function\ \ Learn how to add network access to your discount Function to query an external system for discount code validation.](https://shopify.dev/docs/apps/build/discounts/network-access)[Review the UX guidelines\ \ Review the UX guidelines to learn how to implement discounts in user interfaces.](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts)[Consult the Shopify Functions API references\ \ Consult the API references for Shopify Functions](https://shopify.dev/docs/api/functions)[Learn more about deploying app versions\ \ Learn more about deploying app versions to Shopify](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions)