---
title: Build a Discount Function that has network access
description: Learn how to build a Discount Function that can make network calls.
source_url:
  html: 'https://shopify.dev/docs/apps/build/discounts/network-access?extension=rust'
  md: >-
    https://shopify.dev/docs/apps/build/discounts/network-access.md?extension=rust
api_type: functions
---

# Build a Discount Function that has network access

In this tutorial, you'll build a Shopify Function that validates discount codes against an external system then applies valid discounts to a buyer's cart. Using network access, you'll configure HTTP requests to fetch data from external services and provide that data as input to your run Function. To learn more, see [network access for Shopify Functions](https://shopify.dev/docs/apps/build/functions/input-output/network-access).

**Shopify for enterprise:**

Network access for [Shopify Functions](https://shopify.dev/docs/apps/build/functions) is limited to [Shopify for enterprise](https://www.shopify.com/enterprise) plans. For more information, contact [our enterprise sales team](https://www.shopify.com/solutions/shop-pay/enterprise#contact-sales).

**Discounts in custom storefronts:**

To apply discounts powered by functions with network access in cart, you must use `@defer` in your GraphQL calls. For more information, see [discounts with network access in custom storefronts](https://shopify.dev/docs/apps/build/discounts/network-access-in-cart).

## What you'll learn

In this tutorial, you'll learn how to do the following tasks:

* Add a network request to your Function.
* Respond to the network request with a mock HTTP server.
* Create a tunnel using a service like Ngrok to expose your mock HTTP server to the internet.
* Deploy your Function to Shopify.
* Review logs for your Function.
* Test your Function in checkout.

## Requirements

[Create a Partner account](https://www.shopify.com/partners)

[Create a dev store](https://shopify.dev/docs/apps/build/stores/development-stores#create-a-dev-store)

[Scaffold an app with a Discount Function](https://shopify.dev/docs/apps/build/discounts/build-discount-function)

For example, follow the [Build a Discount Function](https://shopify.dev/docs/apps/build/discounts/build-discount-function) tutorial, or build your own app and Discount Function using the [Discount API](https://shopify.dev/docs/apps/build/discounts).

## Project

[View on GitHub](https://github.com/Shopify/discounts-reference-app/tree/main/examples/rust/network-access)

Using the app you already have, or the one you created by completing the [Build a Discount Function](https://shopify.dev/docs/apps/build/discounts/build-discount-function) tutorial, you can add network access to your Function.

## Update your Function to access the network

In this step, you'll update your Function to access the network and use the `fetchResult` in the `run` target's [input query](https://shopify.dev/docs/api/functions/reference/discount/graphql/input).

### Add network access targets

To access a network from your Shopify Function, add the `cart.lines.discounts.generate.fetch` and `cart.delivery-options.discounts.generate.fetch` targets to your `shopify.extension.toml` file.

The `[[extensions.targeting]]` sections define the Function targets. Each section includes a `target`, `input_query`, and `export` property.

The `target` property defines where the Function is applied. There are two network access targets:

* `cart.lines.discounts.generate.fetch`: This target runs before the `cart.lines.discounts.generate.run` target.
* `cart.delivery-options.discounts.generate.fetch`: This target runs before the `cart.delivery-options.discounts.generate.run` target.

The `input_query` property is the path to the Function's input query file. The input query file defines the Function's input parameters.

The `export` property specifies the entry point for your Function's discount calculation logic.

## extensions/discount-function/shopify.extension.toml

```toml
api_version = "2025-10"


[[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.targeting]]
  target = "cart.delivery-options.discounts.generate.run"
  input_query = "src/cart_delivery_options_discounts_generate_run.graphql"
  export = "cart_delivery_options_discounts_generate_run"


  [[extensions.targeting]]
  target = "cart.lines.discounts.generate.fetch"
  input_query = "src/cart_lines_discounts_generate_fetch.graphql"
  export = "cart_lines_discounts_generate_fetch"


  [[extensions.targeting]]
  target = "cart.delivery-options.discounts.generate.fetch"
  input_query = "src/cart_delivery_options_discounts_generate_fetch.graphql"
  export = "cart_delivery_options_discounts_generate_fetch"


  [extensions.build]
  command = "cargo build --target=wasm32-unknown-unknown --release"
  path = "target/wasm32-unknown-unknown/release/discount-function-rs.wasm"
  watch = [ "src/**/*.rs" ]
```

### Query the `enteredDiscountCodes` for the cart fetch target

Create a `cart_lines_discounts_generate_fetch.graphql` [input query](https://shopify.dev/docs/api/functions/reference/discount/graphql/input) file that provides data to your Function. This allows you to conditionally build an HTTP request that is then executed by Shopify. For example, you can query [`enteredDiscountCodes`](https://shopify.dev/docs/api/functions/reference/discount/graphql/input), which allow you to perform validations against your external service.

## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.graphql

```graphql
query Input {
  enteredDiscountCodes {
    code
  }
  cart {
    buyerIdentity {
      email
    }
  }
}
```

### Query the `enteredDiscountCodes` for the delivery fetch target

Create a `cart_delivery_options_discounts_generate_fetch.graphql` [input query](https://shopify.dev/docs/api/functions/reference/discount/graphql/input) file that provides data to your Function. This allows you to conditionally build an HTTP request that is then executed by Shopify. For example, you can query [`enteredDiscountCodes`](https://shopify.dev/docs/api/functions/reference/discount/graphql/input), which allow you to perform validations against your external service.

## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.graphql

```graphql
query Input {
  enteredDiscountCodes {
    code
  }
  cart {
    buyerIdentity {
      email
    }
  }
}
```

### Query the `fetchResult` for the cart run target

Update the `cart_lines_discounts_generate_run.graphql` file. The `fetchResult` from the fetch request will be the input to your `cart.lines.discounts.generate.run` target. Refer to [Discount API](https://shopify.dev/docs/api/functions/reference/discount) for more information about the input data available to your Function.

## /examples/rust/network-access/extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.graphql

```graphql
query Input {
  fetchResult {
    jsonBody
  }
  cart {
    buyerIdentity {
      email
    }
    lines {
      id
      cost {
        subtotalAmount {
          amount
        }
      }
    }
  }
  discount {
    discountClasses
  }
}
```

### Query the `fetchResult` for the delivery run target

Update the `cart_delivery_options_discounts_generate_run.graphql` file. The `fetchResult` from the fetch request will be the input to your `cart.delivery-options.discounts.generate.run` Function. Refer to [Discount API](https://shopify.dev/docs/api/functions/reference/discount) for more information about the input data available to your Function.

## /examples/rust/network-access/extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.graphql

```graphql
query Input {
  fetchResult {
    jsonBody
  }
  cart {
    buyerIdentity {
      email
    }
    deliveryGroups {
      id
    }
  }
  discount {
    discountClasses
  }
}
```

### Add an export for the cart fetch target

Shopify uses the `cart.lines.discounts.generate.fetch` target to build an HTTP request, which it executes and provides to your run target as input.

## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.rs

```rust
use super::schema;
use shopify_function;
use shopify_function::prelude::*;
use std::collections::BTreeMap;


#[shopify_function]
fn cart_lines_discounts_generate_fetch(
    input: schema::cart_lines_discounts_generate_fetch::Input,
) -> shopify_function::Result<schema::CartLinesDiscountsGenerateFetchResult> {
    let entered_discount_codes = &input.entered_discount_codes();
    let json_body = JsonValue::Object(BTreeMap::from([(
        "enteredDiscountCodes".to_string(),
        JsonValue::Array(
            entered_discount_codes
                .iter()
                .map(|code| JsonValue::String(code.code().to_string()))
                .collect(),
        ),
    )]));


    let request = schema::HttpRequest {
        headers: vec![
            schema::HttpRequestHeader {
                name: "accept".to_string(),
                value: "application/json".to_string(),
            },
            schema::HttpRequestHeader {
                name: "Content-Type".to_string(),
                value: "application/json".to_string(),
            },
        ],
        method: schema::HttpRequestMethod::Post,
        policy: schema::HttpRequestPolicy {
            read_timeout_ms: 2000,
        },
        url: "<external-server-url>/api".to_string(),
        json_body: Some(json_body.clone()),
        body: None,
    };


    Ok(schema::CartLinesDiscountsGenerateFetchResult {
        request: Some(request),
    })
}


#[cfg(test)]
mod tests {
    use super::*;
    use shopify_function::run_function_with_input;


    #[test]
    fn adds_entered_discount_codes_to_json_body_for_cart() -> shopify_function::Result<()> {
        let input = r#"{
            "enteredDiscountCodes": [{"code": "SUMMER10"}, {"code": "WELCOME20"}],
            "cart": {
                "lines": []
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_fetch, input)?;
        let json_body = JsonValue::Object(BTreeMap::from([(
            "enteredDiscountCodes".to_string(),
            JsonValue::Array(vec![
                JsonValue::String("SUMMER10".to_string()),
                JsonValue::String("WELCOME20".to_string()),
            ]),
        )]));
        let expected = schema::CartLinesDiscountsGenerateFetchResult {
            request: Some(schema::HttpRequest {
                headers: vec![
                    schema::HttpRequestHeader {
                        name: "accept".to_string(),
                        value: "application/json".to_string(),
                    },
                    schema::HttpRequestHeader {
                        name: "Content-Type".to_string(),
                        value: "application/json".to_string(),
                    },
                ],
                method: schema::HttpRequestMethod::Post,
                policy: schema::HttpRequestPolicy {
                    read_timeout_ms: 2000,
                },
                url: "<external-server-url>/api".to_string(),
                json_body: Some(json_body),
                body: None,
            }),
        };


        assert_eq!(result, expected);
        Ok(())
    }
}
```

### Add an export for the delivery fetch target

Shopify uses the `cart.delivery-options.discounts.generate.fetch` target to build an HTTP request, which it executes and provides to your run target as input.

## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.rs

```rust
use super::schema;
use shopify_function;
use shopify_function::prelude::*;
use std::collections::BTreeMap;
#[shopify_function]
fn cart_delivery_options_discounts_generate_fetch(
    input: schema::cart_delivery_options_discounts_generate_fetch::Input,
) -> shopify_function::Result<schema::CartDeliveryOptionsDiscountsGenerateFetchResult> {
    let entered_discount_codes = &input.entered_discount_codes();
    let json_body = JsonValue::Object(BTreeMap::from([(
        "enteredDiscountCodes".to_string(),
        JsonValue::Array(
            entered_discount_codes
                .iter()
                .map(|code| JsonValue::String(code.code().to_string()))
                .collect(),
        ),
    )]));


    let request = schema::HttpRequest {
        headers: vec![
            schema::HttpRequestHeader {
                name: "accept".to_string(),
                value: "application/json".to_string(),
            },
            schema::HttpRequestHeader {
                name: "Content-Type".to_string(),
                value: "application/json".to_string(),
            },
        ],
        method: schema::HttpRequestMethod::Post,
        policy: schema::HttpRequestPolicy {
            read_timeout_ms: 2000,
        },
        url: "<external-server-url>/api".to_string(),
        body: None,
        json_body: Some(json_body.clone()),
    };


    Ok(schema::CartDeliveryOptionsDiscountsGenerateFetchResult {
        request: Some(request),
    })
}


#[cfg(test)]
mod tests {
    use super::*;
    use shopify_function::run_function_with_input;


    #[test]
    fn adds_entered_discount_codes_to_json_body_for_delivery() -> shopify_function::Result<()> {
        let input = r#"{
            "enteredDiscountCodes": [{"code": "FREESHIP"}],
            "cart": {
                "lines": []
            }
        }"#;


        let result =
            run_function_with_input(cart_delivery_options_discounts_generate_fetch, input)?;
        let json_body = JsonValue::Object(BTreeMap::from([(
            "enteredDiscountCodes".to_string(),
            JsonValue::Array(vec![JsonValue::String("FREESHIP".to_string())]),
        )]));
        let expected = schema::CartDeliveryOptionsDiscountsGenerateFetchResult {
            request: Some(schema::HttpRequest {
                headers: vec![
                    schema::HttpRequestHeader {
                        name: "accept".to_string(),
                        value: "application/json".to_string(),
                    },
                    schema::HttpRequestHeader {
                        name: "Content-Type".to_string(),
                        value: "application/json".to_string(),
                    },
                ],
                method: schema::HttpRequestMethod::Post,
                policy: schema::HttpRequestPolicy {
                    read_timeout_ms: 2000,
                },
                url: "<external-server-url>/api".to_string(),
                json_body: Some(json_body),
                body: None,
            }),
        };


        assert_eq!(result, expected);
        Ok(())
    }
}
```

### Parse the result from the network call in the cart run target

Update the `cart.lines.discounts.generate.run` target to handle the response from the network call. This target parses the `fetchResult` from the network call and uses it to apply a discount to the cart lines.

## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.rs

```rust
use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;


#[derive(Deserialize)]
#[shopify_function(rename_all = "camelCase")]
pub struct OperationItem {
    product_discounts_add: Option<schema::ProductDiscountsAddOperation>,
    order_discounts_add: Option<schema::OrderDiscountsAddOperation>,
    entered_discount_codes_accept: Option<schema::EnteredDiscountCodesAcceptOperation>,
}
pub type JsonBody = Vec<OperationItem>;


#[shopify_function]
fn cart_lines_discounts_generate_run(
    input: schema::cart_lines_discounts_generate_run::Input,
) -> Result<schema::CartLinesDiscountsGenerateRunResult> {
    let fetch_result = input.fetch_result().ok_or("Missing fetch result")?;
    let discount_classes = &input.discount().discount_classes();


    // Check if relevant discount classes are set
    let has_order_discount_class = discount_classes.contains(&schema::DiscountClass::Order);
    let has_product_discount_class = discount_classes.contains(&schema::DiscountClass::Product);


    // If no relevant discount class is set, return empty operations
    if !has_order_discount_class && !has_product_discount_class {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }


    // Use jsonBody which is the only available property
    let operation_items = fetch_result
        .json_body()
        .ok_or("Missing json_body in response")?;


    // Convert the response into operations
    let mut operations = Vec::new();


    // Process each operation item
    for item in operation_items {
        // Always include discount code operations
        if let Some(validations) = &item.entered_discount_codes_accept {
            operations.push(schema::CartOperation::EnteredDiscountCodesAccept(
                validations.clone(),
            ));
        }


        // Include product discounts only if that class is set
        if has_product_discount_class {
            if let Some(product_discounts_add_operation) = &item.product_discounts_add {
                operations.push(schema::CartOperation::ProductDiscountsAdd(
                    product_discounts_add_operation.clone(),
                ));
            }
        }


        // Include order discounts only if that class is set
        if has_order_discount_class {
            if let Some(order_discounts_add_operation) = &item.order_discounts_add {
                operations.push(schema::CartOperation::OrderDiscountsAdd(
                    order_discounts_add_operation.clone(),
                ));
            }
        }
        // Ignore shipping discounts for cart operations
    }


    Ok(schema::CartLinesDiscountsGenerateRunResult { operations })
}


#[cfg(test)]
mod tests {
    use super::*;
    use shopify_function::run_function_with_input;


    #[test]
    fn processes_discount_codes_with_product_and_order_classes() -> Result<()> {
        let input = r#"{
            "cart": {
                "lines": []
            },
            "discount": {
                "discountClasses": ["PRODUCT", "ORDER"]
            },
            "fetchResult": {
                "status": 200,
                "jsonBody": [
                    {
                        "enteredDiscountCodesAccept": {
                            "codes": [
                                {"code": "SUMMER10"}
                            ]
                        }
                    },
                    {
                        "productDiscountsAdd": {
                            "selectionStrategy": "FIRST",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.1"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "cartLine": {
                                                "id": "gid://shopify/CartLine/123"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    },
                    {
                        "orderDiscountsAdd": {
                            "selectionStrategy": "MAXIMUM",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.15"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "orderSubtotal": {
                                                "excludedCartLineIds": []
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_run, input)?;


        // We should have 3 operations: discount codes, product discounts, and order discounts
        assert_eq!(result.operations.len(), 3);


        // First operation should be EnteredDiscountCodesAccept
        match &result.operations[0] {
            schema::CartOperation::EnteredDiscountCodesAccept(op) => {
                assert_eq!(op.codes.len(), 1);
                assert_eq!(op.codes[0].code, "SUMMER10");
            }
            _ => panic!("Expected EnteredDiscountCodesAccept operation"),
        }


        // Second operation should be ProductDiscountsAdd
        match &result.operations[1] {
            schema::CartOperation::ProductDiscountsAdd(op) => {
                assert_eq!(
                    op.selection_strategy,
                    schema::ProductDiscountSelectionStrategy::First
                );
                assert_eq!(op.candidates.len(), 1);


                let candidate = &op.candidates[0];
                match &candidate.value {
                    schema::ProductDiscountCandidateValue::Percentage(pct) => {
                        assert_eq!(pct.value, Decimal::from(0.1));
                    }
                    _ => panic!("Expected Percentage value"),
                }


                assert_eq!(candidate.targets.len(), 1);
                match &candidate.targets[0] {
                    schema::ProductDiscountCandidateTarget::CartLine(target) => {
                        assert_eq!(target.id, "gid://shopify/CartLine/123");
                    }
                }
            }
            _ => panic!("Expected ProductDiscountsAdd operation"),
        }


        // Third operation should be OrderDiscountsAdd
        match &result.operations[2] {
            schema::CartOperation::OrderDiscountsAdd(op) => {
                assert_eq!(
                    op.selection_strategy,
                    schema::OrderDiscountSelectionStrategy::Maximum
                );
                assert_eq!(op.candidates.len(), 1);


                let candidate = &op.candidates[0];
                match &candidate.value {
                    schema::OrderDiscountCandidateValue::Percentage(pct) => {
                        assert_eq!(pct.value, Decimal::from(0.15));
                    }
                    _ => panic!("Expected Percentage value"),
                }


                assert_eq!(candidate.targets.len(), 1);
                match &candidate.targets[0] {
                    schema::OrderDiscountCandidateTarget::OrderSubtotal(target) => {
                        assert_eq!(target.excluded_cart_line_ids.len(), 0);
                    }
                }
            }
            _ => panic!("Expected OrderDiscountsAdd operation"),
        }


        Ok(())
    }


    #[test]
    fn filters_operations_based_on_discount_classes() -> Result<()> {
        // Only PRODUCT class is enabled, so ORDER discounts should be ignored
        let input = r#"{
            "cart": {
                "lines": []
            },
            "discount": {
                "discountClasses": ["PRODUCT"]
            },
            "fetchResult": {
                "status": 200,
                "jsonBody": [
                    {
                        "productDiscountsAdd": {
                            "selectionStrategy": "FIRST",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.1"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "cartLine": {
                                                "id": "gid://shopify/CartLine/123"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    },
                    {
                        "orderDiscountsAdd": {
                            "selectionStrategy": "MAXIMUM",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.15"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "orderSubtotal": {
                                                "excludedCartLineIds": []
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_run, input)?;


        // Should only include product discounts and filter out order discounts
        assert_eq!(
            result.operations.len(),
            1,
            "Should only have one operation (product discounts)"
        );


        // The operation should be ProductDiscountsAdd
        match &result.operations[0] {
            schema::CartOperation::ProductDiscountsAdd(op) => {
                assert_eq!(
                    op.selection_strategy,
                    schema::ProductDiscountSelectionStrategy::First
                );
                assert_eq!(op.candidates.len(), 1);


                let candidate = &op.candidates[0];
                match &candidate.value {
                    schema::ProductDiscountCandidateValue::Percentage(pct) => {
                        assert_eq!(pct.value, Decimal::from(0.1));
                    }
                    _ => panic!("Expected Percentage value"),
                }


                assert_eq!(candidate.targets.len(), 1);
                match &candidate.targets[0] {
                    schema::ProductDiscountCandidateTarget::CartLine(target) => {
                        assert_eq!(target.id, "gid://shopify/CartLine/123");
                    }
                }
            }
            _ => panic!("Expected ProductDiscountsAdd operation"),
        }


        Ok(())
    }


    #[test]
    fn returns_empty_operations_with_no_relevant_discount_classes() -> Result<()> {
        // Only SHIPPING class is set, which isn't relevant for cart operations
        let input = r#"{
            "cart": {
                "lines": []
            },
            "discount": {
                "discountClasses": ["SHIPPING"]
            },
            "fetchResult": {
                "status": 200,
                "jsonBody": [
                    {
                        "productDiscountsAdd": {
                            "selectionStrategy": "FIRST",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.1"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "cartLine": {
                                                "id": "gid://shopify/CartLine/123"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_run, input)?;


        // Should return empty operations since no relevant discount class is set
        assert_eq!(
            result.operations.len(),
            0,
            "Should have no operations when no relevant discount classes are set"
        );


        Ok(())
    }


    #[test]
    fn always_includes_discount_code_operations() -> Result<()> {
        // Only ORDER class is set, but discount code operations should still be included
        let input = r#"{
            "cart": {
                "lines": []
            },
            "discount": {
                "discountClasses": ["ORDER"]
            },
            "fetchResult": {
                "status": 200,
                "jsonBody": [
                    {
                        "enteredDiscountCodesAccept": {
                            "codes": [
                                {"code": "SAVE20"}
                            ]
                        }
                    },
                    {
                        "productDiscountsAdd": {
                            "selectionStrategy": "FIRST",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.1"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "cartLine": {
                                                "id": "gid://shopify/CartLine/123"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_run, input)?;


        // Should only include discount codes and filter out product discounts
        assert_eq!(
            result.operations.len(),
            1,
            "Should only have one operation (discount codes)"
        );


        // The operation should be EnteredDiscountCodesAccept
        match &result.operations[0] {
            schema::CartOperation::EnteredDiscountCodesAccept(op) => {
                assert_eq!(op.codes.len(), 1);
                assert_eq!(op.codes[0].code, "SAVE20");
            }
            _ => panic!("Expected EnteredDiscountCodesAccept operation"),
        }


        // Make sure we don't have any product discount operations
        assert!(
            !result
                .operations
                .iter()
                .any(|op| matches!(op, schema::CartOperation::ProductDiscountsAdd(_))),
            "Should not have ProductDiscountsAdd operations when PRODUCT class is not set"
        );


        Ok(())
    }
}
```

### Parse the result from the network call in the delivery run target

Update the `cart.delivery-options.discounts.generate.run` target to handle the response from the network call. This target parses the `fetchResult` from the network call and uses it to apply a discount to the delivery options.

## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.rs

```rust
use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;
#[derive(Deserialize)]
#[shopify_function(rename_all = "camelCase")]
pub struct OperationItem {
    delivery_discounts_add: Option<schema::DeliveryDiscountsAddOperation>,
    entered_discount_codes_accept: Option<schema::EnteredDiscountCodesAcceptOperation>,
}
pub type JsonBody = Vec<OperationItem>;


#[shopify_function]
fn cart_delivery_options_discounts_generate_run(
    input: schema::cart_delivery_options_discounts_generate_run::Input,
) -> Result<schema::CartDeliveryOptionsDiscountsGenerateRunResult> {
    let fetch_result = input.fetch_result().ok_or("Missing fetch result")?;
    let discount_classes = &input.discount().discount_classes();


    // Check if shipping discount class is set
    let has_shipping_discount_class = discount_classes.contains(&schema::DiscountClass::Shipping);


    // If shipping discount class is not set, return empty operations
    if !has_shipping_discount_class {
        return Ok(schema::CartDeliveryOptionsDiscountsGenerateRunResult { operations: vec![] });
    }


    // Use jsonBody which is the only available property
    let operation_items = fetch_result
        .json_body()
        .ok_or("Missing json_body in response")?;


    // Convert the response into operations
    let mut operations = Vec::new();


    // Process each operation item
    for item in operation_items {
        // Always include discount code operations
        if let Some(validations) = &item.entered_discount_codes_accept {
            operations.push(schema::DeliveryOperation::EnteredDiscountCodesAccept(
                validations.clone(),
            ));
        }


        // Include delivery discounts (shipping discount class is already verified)
        if let Some(delivery_discounts_add_operation) = &item.delivery_discounts_add {
            operations.push(schema::DeliveryOperation::DeliveryDiscountsAdd(
                delivery_discounts_add_operation.clone(),
            ));
        }
        // Ignore cart/order discounts for delivery operations
    }


    Ok(schema::CartDeliveryOptionsDiscountsGenerateRunResult { operations })
}
```

### Export the fetch request targets in your main file

Add the `cart_lines_discounts_generate_fetch` and `cart_delivery_options_discounts_generate_fetch` exports to the `main.rs` file. The `main.rs` file is the entry point for your Shopify Function.

## extensions/discount-function/src/main.rs

```rust
use std::process;
pub mod cart_delivery_options_discounts_generate_run;
pub mod cart_lines_discounts_generate_run;
pub mod cart_delivery_options_discounts_generate_fetch;
pub mod cart_lines_discounts_generate_fetch;
use shopify_function::typegen;


#[typegen("schema.graphql")]
pub mod schema {
    #[query("src/cart_delivery_options_discounts_generate_fetch.graphql")]
    pub mod cart_delivery_options_discounts_generate_fetch {}


    #[query("src/cart_lines_discounts_generate_fetch.graphql")]
    pub mod cart_lines_discounts_generate_fetch {}


    #[query(
        "src/cart_delivery_options_discounts_generate_run.graphql",
        custom_scalar_overrides = {
            "Input.fetchResult.jsonBody" => super::cart_delivery_options_discounts_generate_run::JsonBody
        }
    )]
    pub mod cart_delivery_options_discounts_generate_run {}


    #[query(
        "src/cart_lines_discounts_generate_run.graphql", 
        custom_scalar_overrides = {
            "Input.fetchResult.jsonBody" => super::cart_lines_discounts_generate_run::JsonBody
        }
    )]
    pub mod cart_lines_discounts_generate_run {}
}


fn main() {
    eprintln!("Please invoke a named export.");
    process::exit(1);
}
```

### Ensure the name of the function package, matches the handle of your Function

In the `Cargo.toml` file, ensure the name of the function package matches the handle of your Function. The handle is the name you provided when you created your Function and it can be found in the `shopify.extension.toml` file.

## extensions/discount-function/Cargo.toml

```toml
[package]
name = "discount-function-rs"
version = "1.0.0"
edition = "2021"
rust-version = "1.87"


[dependencies]
shopify_function = "=2.0.2"
```

### Re-generate the schema

You must regenerate the new Discount API schema to support the new properties you added to your Function Input and Output.

## Terminal

shopify app function schema

## Set up a web service to respond to your Function's network requests

To provide network access to your Function, set up an external service that can respond to the HTTP requests that Shopify will make on behalf of your Function.

When your Shopify Function runs, it cannot make network requests directly. Instead, Shopify makes the requests on behalf of your Function to URLs you specify. In a production environment, this would typically be an API endpoint within your existing infrastructure. For this tutorial, we'll create a simple mock HTTP server.

**Caution:**

When you test this server in development, there is no URL validation against the value found in the JSON web token. Since the URL of your tunnel can change often, you can ignore validating the URL. In production, you should validate the URL to ensure it matches the value found in the JSON web token.

### Set up a mock HTTP server with React Router

**Info:**

This is a separate app from your Shopify App. Create it in a new folder outside your Shopify app.

1. Create a new React Router app from the React Router template:

In a separate folder from your Shopify app, run the following command to create a new React Router app. This will create a new directory called `mock-http-server` with the React Router app inside it.

## Terminal

```bash
npx create-react-router@latest mock-http-server --template remix-run/react-router-templates/javascript
```

1. Navigate to the mock HTTP server directory

## Terminal

```bash
cd mock-http-server
```

1. Install the `jsonwebtoken` package:

## Terminal

```bash
npm i jsonwebtoken
```

### Create an API route in the routes directory

Create a file named `api.js` in the `/routes` directory of the `mock-http-server` app. The code authenticates the request by verifying the JWT, and then executes the the associated business logic, which returns discount operations.

### Review the response from the mock HTTP server

The mock HTTP server responds to your Function's HTTP requests, which are made by Shopify with an object similar to this sample code. This object can then be queried as the `fetchResult` for the input of your `cart.lines.discounts.generate.run` and `cart.delivery-options.discounts.generate.run` targets.

## app/routes/api.js

```javascript
import {subtle} from "crypto";
import {TextEncoder} from "util";
import jwt from "jsonwebtoken";


const selectionStrategy = {
  All: "ALL",
  First: "FIRST",
  Maximum: "MAXIMUM",
};


const PRODUCT_DISCOUNT_CODE = "10OFFPRODUCT";
const ORDER_DISCOUNT_CODE = "20OFFORDER";
const SHIPPING_DISCOUNT_CODE = "FREESHIPPING";


export const action = async ({request}) => {
  if (request.method.toUpperCase() !== "POST") {
    return {
      error: "Invalid request method. Only POST requests are allowed.",
    };
  }


  let body;


  try {
    body = await authenticate(request);
  } catch (err) {
    return {error: err.message};
  }
  return handle(body);
};


const authenticate = async request => {
  const requestJwtHeader = request.headers.get("x-shopify-request-jwt");
  const requestIdHeader = request.headers.get("x-shopify-request-id");


  const secretKey = process.env.APP_CLIENT_SECRET;
  // Include the headers explicitly specified in the HttpRequest of the fetch target.
  const includedVerificationHeaders = process.env.JWT_HEADERS.split(",");
  const shopId = parseInt(process.env.JWT_SHOP_ID);


  // Validate the JWT signature and ensure it hasn't expired.
  const decoded = jwt.verify(requestJwtHeader, secretKey);


  // Validate the JWT claims. The following checks are optional, but they enhance the authenticity of the request.


  // Validate the method
  const method = request.method;
  if (decoded.method !== method) {
    throw new Error("JWT invalid method.");
  }


  // Validate the URL
  const fullUrl = request.url;
  const url_sha256 = await hashWithSHA256(fullUrl);
  if (
    process.env.NODE_ENV !== "development" &&
    decoded.url_sha256 !== url_sha256
  ) {
    throw new Error("JWT invalid url.");
  }


  // Validate the headers
  const headers = Array.from(request.headers);
  const canonicalHeaders = headers
    .filter(([k]) => includedVerificationHeaders.includes(k.toLowerCase()))
    .map(([k, v]) => `${k.toLowerCase()}:${v}`)
    .sort()
    .join(",");
  const headersSha256 = await hashWithSHA256(canonicalHeaders);
  if (decoded.headers_sha256 !== headersSha256) {
    throw new Error("JWT invalid headers.");
  }


  // Validate the body
  const body = await request.text();
  if (body) {
    const body_sha256 = await hashWithSHA256(body);
    if (decoded.body_sha256 !== body_sha256) {
      throw new Error("JWT invalid body.");
    }
  }


  // Validate the issuer Shop
  if (decoded.iss !== shopId) {
    throw new Error("JWT invalid issuer shop.");
  }


  // Validate the request ID. Each request ID is unique and can be used as a measure to prevent replay attacks.
  if (decoded.x_shopify_request_id !== requestIdHeader) {
    throw new Error("JWT invalid x_shopify_request_id.");
  }


  return body;
};


const hashWithSHA256 = async input => {
  const encoder = new TextEncoder();
  const data = encoder.encode(input);
  const hashBuffer = await subtle.digest("SHA-256", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
  return hashHex;
};


const handle = body => {
  const {enteredDiscountCodes} = JSON.parse(body);
  const validDiscountCodes = enteredDiscountCodes.filter(code =>
    [
      PRODUCT_DISCOUNT_CODE,
      ORDER_DISCOUNT_CODE,
      SHIPPING_DISCOUNT_CODE,
    ].includes(code),
  );


  const cartOperations = [];
  const deliveryOperations = [];
  const validationOperations = [];


  if (validDiscountCodes && validDiscountCodes.length > 0) {
    // Add valid discount codes to cart operations
    validationOperations.push({
      enteredDiscountCodesAccept: {
        codes: validDiscountCodes.map(code => ({code})),
      },
    });


    if (validDiscountCodes.includes(PRODUCT_DISCOUNT_CODE)) {
      cartOperations.push({
        productDiscountsAdd: {
          selectionStrategy: selectionStrategy.First,
          candidates: [
            {
              associatedDiscountCode: {code: PRODUCT_DISCOUNT_CODE},
              targets: [
                {
                  cartLine: {
                    id: "gid://shopify/CartLine/0",
                  },
                },
              ],
              value: {
                percentage: {
                  value: "10",
                },
              },
            },
          ],
        },
      });
    }


    if (validDiscountCodes.includes(ORDER_DISCOUNT_CODE)) {
      cartOperations.push({
        orderDiscountsAdd: {
          selectionStrategy: selectionStrategy.First,
          candidates: [
            {
              associatedDiscountCode: {code: ORDER_DISCOUNT_CODE},
              targets: [
                {
                  orderSubtotal: {
                    excludedCartLineIds: [],
                  },
                },
              ],
              value: {
                percentage: {
                  value: "20",
                },
              },
            },
          ],
        },
      });
    }


    if (validDiscountCodes.includes(SHIPPING_DISCOUNT_CODE)) {
      deliveryOperations.push({
        deliveryDiscountsAdd: {
          selectionStrategy: selectionStrategy.All,
          candidates: [
            {
              associatedDiscountCode: {code: SHIPPING_DISCOUNT_CODE},
              value: {
                percentage: {
                  value: "100",
                },
              },
              targets: [
                {
                  deliveryGroup: {
                    id: "gid://shopify/DeliveryGroup/0",
                  },
                },
              ],
            },
          ],
        },
      });
    }
  }
  return [...validationOperations, ...cartOperations, ...deliveryOperations];
};
```

### Create an environment file in the root of your app

This file contains the environment variables for your application:

* `APP_CLIENT_SECRET`: Find this on your App's settings page in the [Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard)
* `JWT_SHOP_ID`: Find this by adding `/shop.json` to the end of your store's Admin URL
  * For example, `https://admin.shopify.com/store/<your-store-name>/shop.json`

### Run your mock HTTP server locally

Run your mock HTTP server locally using the following command:

## Terminal

```bash
npm run dev
```

### Use a service like Ngrok to expose your local server to the internet.

To setup Ngrok, follow [these steps](https://shopify.dev/docs/apps/build/cli-for-apps/use-ngrok-tunneling#set-up-ngrok). Then you can create a tunnel to your local server.

In a separate terminal, start the Ngrok tunnel:

## Terminal

```bash
ngrok http 5173
```

### Update the `url` in your cart fetch target

In your `cart_lines_discounts_generate_fetch.rs` file, replace the `url` with that of the Ngrok tunnel you created in the previous step.

## Terminal

```bash
https://<ngrok-url>/api
```

## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.rs

```rust
use super::schema;
use shopify_function;
use shopify_function::prelude::*;
use std::collections::BTreeMap;


#[shopify_function]
fn cart_lines_discounts_generate_fetch(
    input: schema::cart_lines_discounts_generate_fetch::Input,
) -> shopify_function::Result<schema::CartLinesDiscountsGenerateFetchResult> {
    let entered_discount_codes = &input.entered_discount_codes();
    let json_body = JsonValue::Object(BTreeMap::from([(
        "enteredDiscountCodes".to_string(),
        JsonValue::Array(
            entered_discount_codes
                .iter()
                .map(|code| JsonValue::String(code.code().to_string()))
                .collect(),
        ),
    )]));


    let request = schema::HttpRequest {
        headers: vec![
            schema::HttpRequestHeader {
                name: "accept".to_string(),
                value: "application/json".to_string(),
            },
            schema::HttpRequestHeader {
                name: "Content-Type".to_string(),
                value: "application/json".to_string(),
            },
        ],
        method: schema::HttpRequestMethod::Post,
        policy: schema::HttpRequestPolicy {
            read_timeout_ms: 2000,
        },
        url: "<external-server-url>/api".to_string(),
        json_body: Some(json_body.clone()),
        body: None,
    };


    Ok(schema::CartLinesDiscountsGenerateFetchResult {
        request: Some(request),
    })
}


#[cfg(test)]
mod tests {
    use super::*;
    use shopify_function::run_function_with_input;


    #[test]
    fn adds_entered_discount_codes_to_json_body_for_cart() -> shopify_function::Result<()> {
        let input = r#"{
            "enteredDiscountCodes": [{"code": "SUMMER10"}, {"code": "WELCOME20"}],
            "cart": {
                "lines": []
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_fetch, input)?;
        let json_body = JsonValue::Object(BTreeMap::from([(
            "enteredDiscountCodes".to_string(),
            JsonValue::Array(vec![
                JsonValue::String("SUMMER10".to_string()),
                JsonValue::String("WELCOME20".to_string()),
            ]),
        )]));
        let expected = schema::CartLinesDiscountsGenerateFetchResult {
            request: Some(schema::HttpRequest {
                headers: vec![
                    schema::HttpRequestHeader {
                        name: "accept".to_string(),
                        value: "application/json".to_string(),
                    },
                    schema::HttpRequestHeader {
                        name: "Content-Type".to_string(),
                        value: "application/json".to_string(),
                    },
                ],
                method: schema::HttpRequestMethod::Post,
                policy: schema::HttpRequestPolicy {
                    read_timeout_ms: 2000,
                },
                url: "<external-server-url>/api".to_string(),
                json_body: Some(json_body),
                body: None,
            }),
        };


        assert_eq!(result, expected);
        Ok(())
    }
}
```

### Update the `url` in your delivery fetch target

In your `cart_delivery_options_discounts_generate_fetch.rs` file, replace the `url` with that of the Ngrok tunnel you created in the previous step.

## Terminal

```bash
https://<ngrok-url>/api
```

## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.rs

```rust
use super::schema;
use shopify_function;
use shopify_function::prelude::*;
use std::collections::BTreeMap;
#[shopify_function]
fn cart_delivery_options_discounts_generate_fetch(
    input: schema::cart_delivery_options_discounts_generate_fetch::Input,
) -> shopify_function::Result<schema::CartDeliveryOptionsDiscountsGenerateFetchResult> {
    let entered_discount_codes = &input.entered_discount_codes();
    let json_body = JsonValue::Object(BTreeMap::from([(
        "enteredDiscountCodes".to_string(),
        JsonValue::Array(
            entered_discount_codes
                .iter()
                .map(|code| JsonValue::String(code.code().to_string()))
                .collect(),
        ),
    )]));


    let request = schema::HttpRequest {
        headers: vec![
            schema::HttpRequestHeader {
                name: "accept".to_string(),
                value: "application/json".to_string(),
            },
            schema::HttpRequestHeader {
                name: "Content-Type".to_string(),
                value: "application/json".to_string(),
            },
        ],
        method: schema::HttpRequestMethod::Post,
        policy: schema::HttpRequestPolicy {
            read_timeout_ms: 2000,
        },
        url: "<external-server-url>/api".to_string(),
        body: None,
        json_body: Some(json_body.clone()),
    };


    Ok(schema::CartDeliveryOptionsDiscountsGenerateFetchResult {
        request: Some(request),
    })
}


#[cfg(test)]
mod tests {
    use super::*;
    use shopify_function::run_function_with_input;


    #[test]
    fn adds_entered_discount_codes_to_json_body_for_delivery() -> shopify_function::Result<()> {
        let input = r#"{
            "enteredDiscountCodes": [{"code": "FREESHIP"}],
            "cart": {
                "lines": []
            }
        }"#;


        let result =
            run_function_with_input(cart_delivery_options_discounts_generate_fetch, input)?;
        let json_body = JsonValue::Object(BTreeMap::from([(
            "enteredDiscountCodes".to_string(),
            JsonValue::Array(vec![JsonValue::String("FREESHIP".to_string())]),
        )]));
        let expected = schema::CartDeliveryOptionsDiscountsGenerateFetchResult {
            request: Some(schema::HttpRequest {
                headers: vec![
                    schema::HttpRequestHeader {
                        name: "accept".to_string(),
                        value: "application/json".to_string(),
                    },
                    schema::HttpRequestHeader {
                        name: "Content-Type".to_string(),
                        value: "application/json".to_string(),
                    },
                ],
                method: schema::HttpRequestMethod::Post,
                policy: schema::HttpRequestPolicy {
                    read_timeout_ms: 2000,
                },
                url: "<external-server-url>/api".to_string(),
                json_body: Some(json_body),
                body: None,
            }),
        };


        assert_eq!(result, expected);
        Ok(())
    }
}
```

### Add your tunnel URL to the list of allowed hosts in your mock-http-server's Vite config

To allow your tunnel to proxy traffic to localhost, add the tunnel URL to the list of approved hosts in your Vite config. This is necessary because Vite uses a proxy to forward requests to your local server. **Don't include the `https://` or `http://` prefix in the URL**.

## vite.config.js

```javascript
import {reactRouter} from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import {defineConfig} from "vite";


export default defineConfig({
  plugins: [reactRouter(), tailwindcss()],
  server: {
    allowedHosts: ["<ngrok-url>"],
  },
});
```

### 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.

## Test the discount with network access

### Trigger the discount on your dev store

1. From your Shopify admin, go to [**Discounts**](https://www.shopify.com/admin/discounts). You should now see the **Cart line, order and shipping** that you created.

   ![An image showing a list of all active discounts for the store.](https://shopify.dev/assets/assets/apps/discounts/functions-discount-list-multi-class-bMqqn_8b.png)
2. Open your dev store and build a cart with a single item in it.
3. Add `10OFFPRODUCT`, `20OFFORDER`, and `FREESHIPPING` codes in the **Discount codes or gift card** field.
4. After you navigate to the checkout page, shipping discounts display on the checkout page after you provide your shipping address.

### 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.

## 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 = "2025-10"


[[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.targeting]]
  target = "cart.delivery-options.discounts.generate.run"
  input_query = "src/cart_delivery_options_discounts_generate_run.graphql"
  export = "cart_delivery_options_discounts_generate_run"


  [[extensions.targeting]]
  target = "cart.lines.discounts.generate.fetch"
  input_query = "src/cart_lines_discounts_generate_fetch.graphql"
  export = "cart_lines_discounts_generate_fetch"


  [[extensions.targeting]]
  target = "cart.delivery-options.discounts.generate.fetch"
  input_query = "src/cart_delivery_options_discounts_generate_fetch.graphql"
  export = "cart_delivery_options_discounts_generate_fetch"


  [extensions.build]
  command = "cargo build --target=wasm32-unknown-unknown --release"
  path = "target/wasm32-unknown-unknown/release/discount-function-rs.wasm"
  watch = [ "src/**/*.rs" ]
```

## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.graphql

```graphql
query Input {
  enteredDiscountCodes {
    code
  }
  cart {
    buyerIdentity {
      email
    }
  }
}
```

## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.graphql

```graphql
query Input {
  enteredDiscountCodes {
    code
  }
  cart {
    buyerIdentity {
      email
    }
  }
}
```

## /examples/rust/network-access/extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.graphql

```graphql
query Input {
  fetchResult {
    jsonBody
  }
  cart {
    buyerIdentity {
      email
    }
    lines {
      id
      cost {
        subtotalAmount {
          amount
        }
      }
    }
  }
  discount {
    discountClasses
  }
}
```

## /examples/rust/network-access/extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.graphql

```graphql
query Input {
  fetchResult {
    jsonBody
  }
  cart {
    buyerIdentity {
      email
    }
    deliveryGroups {
      id
    }
  }
  discount {
    discountClasses
  }
}
```

## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.rs

```rust
use super::schema;
use shopify_function;
use shopify_function::prelude::*;
use std::collections::BTreeMap;


#[shopify_function]
fn cart_lines_discounts_generate_fetch(
    input: schema::cart_lines_discounts_generate_fetch::Input,
) -> shopify_function::Result<schema::CartLinesDiscountsGenerateFetchResult> {
    let entered_discount_codes = &input.entered_discount_codes();
    let json_body = JsonValue::Object(BTreeMap::from([(
        "enteredDiscountCodes".to_string(),
        JsonValue::Array(
            entered_discount_codes
                .iter()
                .map(|code| JsonValue::String(code.code().to_string()))
                .collect(),
        ),
    )]));


    let request = schema::HttpRequest {
        headers: vec![
            schema::HttpRequestHeader {
                name: "accept".to_string(),
                value: "application/json".to_string(),
            },
            schema::HttpRequestHeader {
                name: "Content-Type".to_string(),
                value: "application/json".to_string(),
            },
        ],
        method: schema::HttpRequestMethod::Post,
        policy: schema::HttpRequestPolicy {
            read_timeout_ms: 2000,
        },
        url: "<external-server-url>/api".to_string(),
        json_body: Some(json_body.clone()),
        body: None,
    };


    Ok(schema::CartLinesDiscountsGenerateFetchResult {
        request: Some(request),
    })
}


#[cfg(test)]
mod tests {
    use super::*;
    use shopify_function::run_function_with_input;


    #[test]
    fn adds_entered_discount_codes_to_json_body_for_cart() -> shopify_function::Result<()> {
        let input = r#"{
            "enteredDiscountCodes": [{"code": "SUMMER10"}, {"code": "WELCOME20"}],
            "cart": {
                "lines": []
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_fetch, input)?;
        let json_body = JsonValue::Object(BTreeMap::from([(
            "enteredDiscountCodes".to_string(),
            JsonValue::Array(vec![
                JsonValue::String("SUMMER10".to_string()),
                JsonValue::String("WELCOME20".to_string()),
            ]),
        )]));
        let expected = schema::CartLinesDiscountsGenerateFetchResult {
            request: Some(schema::HttpRequest {
                headers: vec![
                    schema::HttpRequestHeader {
                        name: "accept".to_string(),
                        value: "application/json".to_string(),
                    },
                    schema::HttpRequestHeader {
                        name: "Content-Type".to_string(),
                        value: "application/json".to_string(),
                    },
                ],
                method: schema::HttpRequestMethod::Post,
                policy: schema::HttpRequestPolicy {
                    read_timeout_ms: 2000,
                },
                url: "<external-server-url>/api".to_string(),
                json_body: Some(json_body),
                body: None,
            }),
        };


        assert_eq!(result, expected);
        Ok(())
    }
}
```

## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.rs

```rust
use super::schema;
use shopify_function;
use shopify_function::prelude::*;
use std::collections::BTreeMap;
#[shopify_function]
fn cart_delivery_options_discounts_generate_fetch(
    input: schema::cart_delivery_options_discounts_generate_fetch::Input,
) -> shopify_function::Result<schema::CartDeliveryOptionsDiscountsGenerateFetchResult> {
    let entered_discount_codes = &input.entered_discount_codes();
    let json_body = JsonValue::Object(BTreeMap::from([(
        "enteredDiscountCodes".to_string(),
        JsonValue::Array(
            entered_discount_codes
                .iter()
                .map(|code| JsonValue::String(code.code().to_string()))
                .collect(),
        ),
    )]));


    let request = schema::HttpRequest {
        headers: vec![
            schema::HttpRequestHeader {
                name: "accept".to_string(),
                value: "application/json".to_string(),
            },
            schema::HttpRequestHeader {
                name: "Content-Type".to_string(),
                value: "application/json".to_string(),
            },
        ],
        method: schema::HttpRequestMethod::Post,
        policy: schema::HttpRequestPolicy {
            read_timeout_ms: 2000,
        },
        url: "<external-server-url>/api".to_string(),
        body: None,
        json_body: Some(json_body.clone()),
    };


    Ok(schema::CartDeliveryOptionsDiscountsGenerateFetchResult {
        request: Some(request),
    })
}


#[cfg(test)]
mod tests {
    use super::*;
    use shopify_function::run_function_with_input;


    #[test]
    fn adds_entered_discount_codes_to_json_body_for_delivery() -> shopify_function::Result<()> {
        let input = r#"{
            "enteredDiscountCodes": [{"code": "FREESHIP"}],
            "cart": {
                "lines": []
            }
        }"#;


        let result =
            run_function_with_input(cart_delivery_options_discounts_generate_fetch, input)?;
        let json_body = JsonValue::Object(BTreeMap::from([(
            "enteredDiscountCodes".to_string(),
            JsonValue::Array(vec![JsonValue::String("FREESHIP".to_string())]),
        )]));
        let expected = schema::CartDeliveryOptionsDiscountsGenerateFetchResult {
            request: Some(schema::HttpRequest {
                headers: vec![
                    schema::HttpRequestHeader {
                        name: "accept".to_string(),
                        value: "application/json".to_string(),
                    },
                    schema::HttpRequestHeader {
                        name: "Content-Type".to_string(),
                        value: "application/json".to_string(),
                    },
                ],
                method: schema::HttpRequestMethod::Post,
                policy: schema::HttpRequestPolicy {
                    read_timeout_ms: 2000,
                },
                url: "<external-server-url>/api".to_string(),
                json_body: Some(json_body),
                body: None,
            }),
        };


        assert_eq!(result, expected);
        Ok(())
    }
}
```

## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.rs

```rust
use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;


#[derive(Deserialize)]
#[shopify_function(rename_all = "camelCase")]
pub struct OperationItem {
    product_discounts_add: Option<schema::ProductDiscountsAddOperation>,
    order_discounts_add: Option<schema::OrderDiscountsAddOperation>,
    entered_discount_codes_accept: Option<schema::EnteredDiscountCodesAcceptOperation>,
}
pub type JsonBody = Vec<OperationItem>;


#[shopify_function]
fn cart_lines_discounts_generate_run(
    input: schema::cart_lines_discounts_generate_run::Input,
) -> Result<schema::CartLinesDiscountsGenerateRunResult> {
    let fetch_result = input.fetch_result().ok_or("Missing fetch result")?;
    let discount_classes = &input.discount().discount_classes();


    // Check if relevant discount classes are set
    let has_order_discount_class = discount_classes.contains(&schema::DiscountClass::Order);
    let has_product_discount_class = discount_classes.contains(&schema::DiscountClass::Product);


    // If no relevant discount class is set, return empty operations
    if !has_order_discount_class && !has_product_discount_class {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }


    // Use jsonBody which is the only available property
    let operation_items = fetch_result
        .json_body()
        .ok_or("Missing json_body in response")?;


    // Convert the response into operations
    let mut operations = Vec::new();


    // Process each operation item
    for item in operation_items {
        // Always include discount code operations
        if let Some(validations) = &item.entered_discount_codes_accept {
            operations.push(schema::CartOperation::EnteredDiscountCodesAccept(
                validations.clone(),
            ));
        }


        // Include product discounts only if that class is set
        if has_product_discount_class {
            if let Some(product_discounts_add_operation) = &item.product_discounts_add {
                operations.push(schema::CartOperation::ProductDiscountsAdd(
                    product_discounts_add_operation.clone(),
                ));
            }
        }


        // Include order discounts only if that class is set
        if has_order_discount_class {
            if let Some(order_discounts_add_operation) = &item.order_discounts_add {
                operations.push(schema::CartOperation::OrderDiscountsAdd(
                    order_discounts_add_operation.clone(),
                ));
            }
        }
        // Ignore shipping discounts for cart operations
    }


    Ok(schema::CartLinesDiscountsGenerateRunResult { operations })
}


#[cfg(test)]
mod tests {
    use super::*;
    use shopify_function::run_function_with_input;


    #[test]
    fn processes_discount_codes_with_product_and_order_classes() -> Result<()> {
        let input = r#"{
            "cart": {
                "lines": []
            },
            "discount": {
                "discountClasses": ["PRODUCT", "ORDER"]
            },
            "fetchResult": {
                "status": 200,
                "jsonBody": [
                    {
                        "enteredDiscountCodesAccept": {
                            "codes": [
                                {"code": "SUMMER10"}
                            ]
                        }
                    },
                    {
                        "productDiscountsAdd": {
                            "selectionStrategy": "FIRST",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.1"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "cartLine": {
                                                "id": "gid://shopify/CartLine/123"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    },
                    {
                        "orderDiscountsAdd": {
                            "selectionStrategy": "MAXIMUM",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.15"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "orderSubtotal": {
                                                "excludedCartLineIds": []
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_run, input)?;


        // We should have 3 operations: discount codes, product discounts, and order discounts
        assert_eq!(result.operations.len(), 3);


        // First operation should be EnteredDiscountCodesAccept
        match &result.operations[0] {
            schema::CartOperation::EnteredDiscountCodesAccept(op) => {
                assert_eq!(op.codes.len(), 1);
                assert_eq!(op.codes[0].code, "SUMMER10");
            }
            _ => panic!("Expected EnteredDiscountCodesAccept operation"),
        }


        // Second operation should be ProductDiscountsAdd
        match &result.operations[1] {
            schema::CartOperation::ProductDiscountsAdd(op) => {
                assert_eq!(
                    op.selection_strategy,
                    schema::ProductDiscountSelectionStrategy::First
                );
                assert_eq!(op.candidates.len(), 1);


                let candidate = &op.candidates[0];
                match &candidate.value {
                    schema::ProductDiscountCandidateValue::Percentage(pct) => {
                        assert_eq!(pct.value, Decimal::from(0.1));
                    }
                    _ => panic!("Expected Percentage value"),
                }


                assert_eq!(candidate.targets.len(), 1);
                match &candidate.targets[0] {
                    schema::ProductDiscountCandidateTarget::CartLine(target) => {
                        assert_eq!(target.id, "gid://shopify/CartLine/123");
                    }
                }
            }
            _ => panic!("Expected ProductDiscountsAdd operation"),
        }


        // Third operation should be OrderDiscountsAdd
        match &result.operations[2] {
            schema::CartOperation::OrderDiscountsAdd(op) => {
                assert_eq!(
                    op.selection_strategy,
                    schema::OrderDiscountSelectionStrategy::Maximum
                );
                assert_eq!(op.candidates.len(), 1);


                let candidate = &op.candidates[0];
                match &candidate.value {
                    schema::OrderDiscountCandidateValue::Percentage(pct) => {
                        assert_eq!(pct.value, Decimal::from(0.15));
                    }
                    _ => panic!("Expected Percentage value"),
                }


                assert_eq!(candidate.targets.len(), 1);
                match &candidate.targets[0] {
                    schema::OrderDiscountCandidateTarget::OrderSubtotal(target) => {
                        assert_eq!(target.excluded_cart_line_ids.len(), 0);
                    }
                }
            }
            _ => panic!("Expected OrderDiscountsAdd operation"),
        }


        Ok(())
    }


    #[test]
    fn filters_operations_based_on_discount_classes() -> Result<()> {
        // Only PRODUCT class is enabled, so ORDER discounts should be ignored
        let input = r#"{
            "cart": {
                "lines": []
            },
            "discount": {
                "discountClasses": ["PRODUCT"]
            },
            "fetchResult": {
                "status": 200,
                "jsonBody": [
                    {
                        "productDiscountsAdd": {
                            "selectionStrategy": "FIRST",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.1"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "cartLine": {
                                                "id": "gid://shopify/CartLine/123"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    },
                    {
                        "orderDiscountsAdd": {
                            "selectionStrategy": "MAXIMUM",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.15"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "orderSubtotal": {
                                                "excludedCartLineIds": []
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_run, input)?;


        // Should only include product discounts and filter out order discounts
        assert_eq!(
            result.operations.len(),
            1,
            "Should only have one operation (product discounts)"
        );


        // The operation should be ProductDiscountsAdd
        match &result.operations[0] {
            schema::CartOperation::ProductDiscountsAdd(op) => {
                assert_eq!(
                    op.selection_strategy,
                    schema::ProductDiscountSelectionStrategy::First
                );
                assert_eq!(op.candidates.len(), 1);


                let candidate = &op.candidates[0];
                match &candidate.value {
                    schema::ProductDiscountCandidateValue::Percentage(pct) => {
                        assert_eq!(pct.value, Decimal::from(0.1));
                    }
                    _ => panic!("Expected Percentage value"),
                }


                assert_eq!(candidate.targets.len(), 1);
                match &candidate.targets[0] {
                    schema::ProductDiscountCandidateTarget::CartLine(target) => {
                        assert_eq!(target.id, "gid://shopify/CartLine/123");
                    }
                }
            }
            _ => panic!("Expected ProductDiscountsAdd operation"),
        }


        Ok(())
    }


    #[test]
    fn returns_empty_operations_with_no_relevant_discount_classes() -> Result<()> {
        // Only SHIPPING class is set, which isn't relevant for cart operations
        let input = r#"{
            "cart": {
                "lines": []
            },
            "discount": {
                "discountClasses": ["SHIPPING"]
            },
            "fetchResult": {
                "status": 200,
                "jsonBody": [
                    {
                        "productDiscountsAdd": {
                            "selectionStrategy": "FIRST",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.1"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "cartLine": {
                                                "id": "gid://shopify/CartLine/123"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_run, input)?;


        // Should return empty operations since no relevant discount class is set
        assert_eq!(
            result.operations.len(),
            0,
            "Should have no operations when no relevant discount classes are set"
        );


        Ok(())
    }


    #[test]
    fn always_includes_discount_code_operations() -> Result<()> {
        // Only ORDER class is set, but discount code operations should still be included
        let input = r#"{
            "cart": {
                "lines": []
            },
            "discount": {
                "discountClasses": ["ORDER"]
            },
            "fetchResult": {
                "status": 200,
                "jsonBody": [
                    {
                        "enteredDiscountCodesAccept": {
                            "codes": [
                                {"code": "SAVE20"}
                            ]
                        }
                    },
                    {
                        "productDiscountsAdd": {
                            "selectionStrategy": "FIRST",
                            "candidates": [
                                {
                                    "value": {
                                        "percentage": {
                                            "value": "0.1"
                                        }
                                    },
                                    "targets": [
                                        {
                                            "cartLine": {
                                                "id": "gid://shopify/CartLine/123"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        }"#;


        let result = run_function_with_input(cart_lines_discounts_generate_run, input)?;


        // Should only include discount codes and filter out product discounts
        assert_eq!(
            result.operations.len(),
            1,
            "Should only have one operation (discount codes)"
        );


        // The operation should be EnteredDiscountCodesAccept
        match &result.operations[0] {
            schema::CartOperation::EnteredDiscountCodesAccept(op) => {
                assert_eq!(op.codes.len(), 1);
                assert_eq!(op.codes[0].code, "SAVE20");
            }
            _ => panic!("Expected EnteredDiscountCodesAccept operation"),
        }


        // Make sure we don't have any product discount operations
        assert!(
            !result
                .operations
                .iter()
                .any(|op| matches!(op, schema::CartOperation::ProductDiscountsAdd(_))),
            "Should not have ProductDiscountsAdd operations when PRODUCT class is not set"
        );


        Ok(())
    }
}
```

## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.rs

```rust
use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;
#[derive(Deserialize)]
#[shopify_function(rename_all = "camelCase")]
pub struct OperationItem {
    delivery_discounts_add: Option<schema::DeliveryDiscountsAddOperation>,
    entered_discount_codes_accept: Option<schema::EnteredDiscountCodesAcceptOperation>,
}
pub type JsonBody = Vec<OperationItem>;


#[shopify_function]
fn cart_delivery_options_discounts_generate_run(
    input: schema::cart_delivery_options_discounts_generate_run::Input,
) -> Result<schema::CartDeliveryOptionsDiscountsGenerateRunResult> {
    let fetch_result = input.fetch_result().ok_or("Missing fetch result")?;
    let discount_classes = &input.discount().discount_classes();


    // Check if shipping discount class is set
    let has_shipping_discount_class = discount_classes.contains(&schema::DiscountClass::Shipping);


    // If shipping discount class is not set, return empty operations
    if !has_shipping_discount_class {
        return Ok(schema::CartDeliveryOptionsDiscountsGenerateRunResult { operations: vec![] });
    }


    // Use jsonBody which is the only available property
    let operation_items = fetch_result
        .json_body()
        .ok_or("Missing json_body in response")?;


    // Convert the response into operations
    let mut operations = Vec::new();


    // Process each operation item
    for item in operation_items {
        // Always include discount code operations
        if let Some(validations) = &item.entered_discount_codes_accept {
            operations.push(schema::DeliveryOperation::EnteredDiscountCodesAccept(
                validations.clone(),
            ));
        }


        // Include delivery discounts (shipping discount class is already verified)
        if let Some(delivery_discounts_add_operation) = &item.delivery_discounts_add {
            operations.push(schema::DeliveryOperation::DeliveryDiscountsAdd(
                delivery_discounts_add_operation.clone(),
            ));
        }
        // Ignore cart/order discounts for delivery operations
    }


    Ok(schema::CartDeliveryOptionsDiscountsGenerateRunResult { operations })
}
```

## extensions/discount-function/src/main.rs

```rust
use std::process;
pub mod cart_delivery_options_discounts_generate_run;
pub mod cart_lines_discounts_generate_run;
pub mod cart_delivery_options_discounts_generate_fetch;
pub mod cart_lines_discounts_generate_fetch;
use shopify_function::typegen;


#[typegen("schema.graphql")]
pub mod schema {
    #[query("src/cart_delivery_options_discounts_generate_fetch.graphql")]
    pub mod cart_delivery_options_discounts_generate_fetch {}


    #[query("src/cart_lines_discounts_generate_fetch.graphql")]
    pub mod cart_lines_discounts_generate_fetch {}


    #[query(
        "src/cart_delivery_options_discounts_generate_run.graphql",
        custom_scalar_overrides = {
            "Input.fetchResult.jsonBody" => super::cart_delivery_options_discounts_generate_run::JsonBody
        }
    )]
    pub mod cart_delivery_options_discounts_generate_run {}


    #[query(
        "src/cart_lines_discounts_generate_run.graphql", 
        custom_scalar_overrides = {
            "Input.fetchResult.jsonBody" => super::cart_lines_discounts_generate_run::JsonBody
        }
    )]
    pub mod cart_lines_discounts_generate_run {}
}


fn main() {
    eprintln!("Please invoke a named export.");
    process::exit(1);
}
```

## extensions/discount-function/Cargo.toml

```toml
[package]
name = "discount-function-rs"
version = "1.0.0"
edition = "2021"
rust-version = "1.87"


[dependencies]
shopify_function = "=2.0.2"
```

## app/routes/api.js

```javascript
import {subtle} from "crypto";
import {TextEncoder} from "util";
import jwt from "jsonwebtoken";


const selectionStrategy = {
  All: "ALL",
  First: "FIRST",
  Maximum: "MAXIMUM",
};


const PRODUCT_DISCOUNT_CODE = "10OFFPRODUCT";
const ORDER_DISCOUNT_CODE = "20OFFORDER";
const SHIPPING_DISCOUNT_CODE = "FREESHIPPING";


export const action = async ({request}) => {
  if (request.method.toUpperCase() !== "POST") {
    return {
      error: "Invalid request method. Only POST requests are allowed.",
    };
  }


  let body;


  try {
    body = await authenticate(request);
  } catch (err) {
    return {error: err.message};
  }
  return handle(body);
};


const authenticate = async request => {
  const requestJwtHeader = request.headers.get("x-shopify-request-jwt");
  const requestIdHeader = request.headers.get("x-shopify-request-id");


  const secretKey = process.env.APP_CLIENT_SECRET;
  // Include the headers explicitly specified in the HttpRequest of the fetch target.
  const includedVerificationHeaders = process.env.JWT_HEADERS.split(",");
  const shopId = parseInt(process.env.JWT_SHOP_ID);


  // Validate the JWT signature and ensure it hasn't expired.
  const decoded = jwt.verify(requestJwtHeader, secretKey);


  // Validate the JWT claims. The following checks are optional, but they enhance the authenticity of the request.


  // Validate the method
  const method = request.method;
  if (decoded.method !== method) {
    throw new Error("JWT invalid method.");
  }


  // Validate the URL
  const fullUrl = request.url;
  const url_sha256 = await hashWithSHA256(fullUrl);
  if (
    process.env.NODE_ENV !== "development" &&
    decoded.url_sha256 !== url_sha256
  ) {
    throw new Error("JWT invalid url.");
  }


  // Validate the headers
  const headers = Array.from(request.headers);
  const canonicalHeaders = headers
    .filter(([k]) => includedVerificationHeaders.includes(k.toLowerCase()))
    .map(([k, v]) => `${k.toLowerCase()}:${v}`)
    .sort()
    .join(",");
  const headersSha256 = await hashWithSHA256(canonicalHeaders);
  if (decoded.headers_sha256 !== headersSha256) {
    throw new Error("JWT invalid headers.");
  }


  // Validate the body
  const body = await request.text();
  if (body) {
    const body_sha256 = await hashWithSHA256(body);
    if (decoded.body_sha256 !== body_sha256) {
      throw new Error("JWT invalid body.");
    }
  }


  // Validate the issuer Shop
  if (decoded.iss !== shopId) {
    throw new Error("JWT invalid issuer shop.");
  }


  // Validate the request ID. Each request ID is unique and can be used as a measure to prevent replay attacks.
  if (decoded.x_shopify_request_id !== requestIdHeader) {
    throw new Error("JWT invalid x_shopify_request_id.");
  }


  return body;
};


const hashWithSHA256 = async input => {
  const encoder = new TextEncoder();
  const data = encoder.encode(input);
  const hashBuffer = await subtle.digest("SHA-256", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
  return hashHex;
};


const handle = body => {
  const {enteredDiscountCodes} = JSON.parse(body);
  const validDiscountCodes = enteredDiscountCodes.filter(code =>
    [
      PRODUCT_DISCOUNT_CODE,
      ORDER_DISCOUNT_CODE,
      SHIPPING_DISCOUNT_CODE,
    ].includes(code),
  );


  const cartOperations = [];
  const deliveryOperations = [];
  const validationOperations = [];


  if (validDiscountCodes && validDiscountCodes.length > 0) {
    // Add valid discount codes to cart operations
    validationOperations.push({
      enteredDiscountCodesAccept: {
        codes: validDiscountCodes.map(code => ({code})),
      },
    });


    if (validDiscountCodes.includes(PRODUCT_DISCOUNT_CODE)) {
      cartOperations.push({
        productDiscountsAdd: {
          selectionStrategy: selectionStrategy.First,
          candidates: [
            {
              associatedDiscountCode: {code: PRODUCT_DISCOUNT_CODE},
              targets: [
                {
                  cartLine: {
                    id: "gid://shopify/CartLine/0",
                  },
                },
              ],
              value: {
                percentage: {
                  value: "10",
                },
              },
            },
          ],
        },
      });
    }


    if (validDiscountCodes.includes(ORDER_DISCOUNT_CODE)) {
      cartOperations.push({
        orderDiscountsAdd: {
          selectionStrategy: selectionStrategy.First,
          candidates: [
            {
              associatedDiscountCode: {code: ORDER_DISCOUNT_CODE},
              targets: [
                {
                  orderSubtotal: {
                    excludedCartLineIds: [],
                  },
                },
              ],
              value: {
                percentage: {
                  value: "20",
                },
              },
            },
          ],
        },
      });
    }


    if (validDiscountCodes.includes(SHIPPING_DISCOUNT_CODE)) {
      deliveryOperations.push({
        deliveryDiscountsAdd: {
          selectionStrategy: selectionStrategy.All,
          candidates: [
            {
              associatedDiscountCode: {code: SHIPPING_DISCOUNT_CODE},
              value: {
                percentage: {
                  value: "100",
                },
              },
              targets: [
                {
                  deliveryGroup: {
                    id: "gid://shopify/DeliveryGroup/0",
                  },
                },
              ],
            },
          ],
        },
      });
    }
  }
  return [...validationOperations, ...cartOperations, ...deliveryOperations];
};
```

## .env

```bash
# Found in the Partner Dashboard
APP_CLIENT_SECRET=123456789asdfg


# JWT headers for verification
JWT_HEADERS=accept,content-type


# JWT shop ID
JWT_SHOP_ID=123456
```

## vite.config.js

```javascript
import {reactRouter} from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import {defineConfig} from "vite";


export default defineConfig({
  plugins: [reactRouter(), tailwindcss()],
  server: {
    allowedHosts: ["<ngrok-url>"],
  },
});
```

## Tutorial complete!

You've successfully created a Discount Function with network access. You can now use this Function to apply discounts that target cart lines, order subtotals, and shipping rates, while validating discount codes against an external system.

***

### 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)[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)[Learn more about Shopify Functions\
\
Learn more about how Shopify Functions work and the benefits of using Shopify Functions.](https://shopify.dev/docs/apps/build/functions)[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)[Apply discounts in cart\
\
Learn more about applying discounts from functions with network access in cart.](https://shopify.dev/docs/apps/build/discounts/network-access-in-cart)
