Build a location rule function
You can use location rules to rank the possible locations for a line item during checkout. In this tutorial, you'll use Shopify Functions to create a function that prioritizes locations in a particular country.
Location rules is a new feature that's only available by request. Reach out to Shopify Plus Support to know more about your eligibility and the requirements for the beta program.
Anchor to What you'll learnWhat you'll learn
In this tutorial, you'll learn how to do the following tasks:
- Generate starter code for Shopify Functions.
- Use GraphQL to define the input of your function.
- Deploy functions to the Shopify platform.
- Use the order routing settings to create a location rule in a Shopify store.
- Review logs for your function.
Anchor to RequirementsRequirements
Refer to Build location rules.
Anchor to Step 1: Create the location rule functionStep 1: Create the location rule function
To create your location rule function, you can use Shopify CLI to generate a starter function, specify the inputs for your function using an input query, and implement your function logic using Rust.
-
Navigate to your app directory:
Terminal
cd <directory> -
Run the following command to create a new location rule extension:
Terminal
shopify app generate extension --template order_routing_location_rule --name location-rule
-
Choose the language that you want to use. For this tutorial, you should select either Rust or JavaScript.
Shopify defaults to Rust as the most performant and recommended language choice to stay within the platform limits. For more information, refer to language considerations.
Terminal
? What would you like to work in?> (1) Rust(2) JavaScript(3) TypeScript(4) Wasm
Shopify Functions support any language that compiles to WebAssembly (Wasm), such as Rust, AssemblyScript, or TinyGo. You specify the Wasm template option when you're using a language other than Rust and can conform to the Wasm API. Learn more about the Wasm API.
-
Navigate to
extensions/location-rule
:Terminal
cd extensions/location-rule -
Replace the contents of
src/run.graphql
file with the following code:run.graphql
defines the input for the function. You need the fulfillment groups, with the available locations for the items in the group.The query differs slightly in Rust and JavaScript due to code generation requirements.
run.graphql
src/run.graphql
query Input {fulfillmentGroups {handleinventoryLocationHandles}locations {handleaddress {countryCode}}}query RunInput {fulfillmentGroups {handleinventoryLocationHandles}locations {handleaddress {countryCode}}}query Input { fulfillmentGroups { handle inventoryLocationHandles } locations { handle address { countryCode } } }
query RunInput { fulfillmentGroups { handle inventoryLocationHandles } locations { handle address { countryCode } } }
TipIf a store has many locations, then running this query can result in a big input to parse. To optimize performance, consider loading the location in the input and querying the inventory locations by ID.
-
If you're using JavaScript, then run the following command to regenerate types based on your input query:
Terminal
shopify app function typegen -
Replace the
src/run.rs
orsrc/run.js
file with the following code.The function logic sorts the locations in your store, putting locations in Canada first.
File
src/run.rs
use super::schema;use shopify_function::prelude::*;use shopify_function::Result;use schema::FulfillmentGroupRankedLocations;use crate::schema::Handle;fn run(input: schema::run::Input) -> Result<schema::CartFulfillmentGroupsLocationRankingsGenerateRunResult> {// Load the fulfillment groups and generate the rank operations for each onelet operations = input.fulfillment_groups().into_iter().map(|fulfillment_group| schema::Operation {fulfillment_group_location_ranking_add: build_rank_operation(fulfillment_group, &input.locations()),}).collect();// Return the operationsOk(schema::CartFulfillmentGroupsLocationRankingsGenerateRunResult { operations })}fn build_rank_operation(input: &schema::run::input::FulfillmentGroups,locations: &[schema::run::input::Locations],) -> FulfillmentGroupLocationRankingAddOperation {FulfillmentGroupLocationRankingAddOperation {fulfillment_group_handle: input.handle().to_string(),rankings: prioritize_canadian_locations(input.inventory_location_handles().to_vec(), locations),}}fn prioritize_canadian_locations(handles: Vec<Handle>,locations: &[schema::run::input::Locations],) -> Vec<schema::RankedLocation> {// Load the inventory locations for the fulfillment grouphandles.into_iter().map(|location_handle| {let location = locations.iter().find(|&loc| *loc.handle() == location_handle);// Rank the location as 0 if the country code is CA, otherwise rank it as 1schema::RankedLocation {location_handle,rank: match location {Some(location) => {if location.address().country_code() == Some("CA".to_string()).as_ref() {0} else {1}}None => 1,},}}).collect()}// @ts-check/*** @typedef {import("../generated/api").RunInput} RunInput* @typedef {import("../generated/api").RunInput["fulfillmentGroups"][0]} FulfillmentGroup* @typedef {import("../generated/api").RunInput["locations"][0]} Location* @typedef {import("../generated/api").CartFulfillmentGroupsLocationRankingsGenerateRunResult} CartFulfillmentGroupsLocationRankingsGenerateRunResult* @typedef {import("../generated/api").Operation} Operation* @typedef {import("../generated/api").FulfillmentGroupLocationRankingAddOperation} FulfillmentGroupLocationRankingAddOperation* @typedef {import("../generated/api").RankedLocation} RankedLocation*//*** @param {RunInput} input* @returns {CartFulfillmentGroupsLocationRankingsGenerateRunResult}*/export function run(input) {// Load the fulfillment groups and generate the rank operations for each onelet operations = input.fulfillmentGroups.map(fulfillmentGroup => /** @type {Operation} */({fulfillment_group_location_ranking_add: buildRankOperation(fulfillmentGroup, input.locations)}));// Return the operationsreturn { operations: operations };};/*** @param {FulfillmentGroup} fulfillmentGroup* @param {Location[]} locations* @returns {FulfillmentGroupLocationRankingAddOperation}*/function buildRankOperation(fulfillmentGroup, locations) {return {fulfillmentGroupHandle: fulfillmentGroup.handle,rankings: prioritizeCanadianLocations(fulfillmentGroup.inventoryLocationHandles, locations),};};/*** @param {string[]} locationHandles* @param {Location[]} locations* @returns {RankedLocation[]}*/function prioritizeCanadianLocations(locationHandles, locations) {// Load the inventory locations for the fulfillment groupreturn locationHandles.map(locationHandle => {const location = locations.find((loc) => loc.handle == locationHandle);return {locationHandle,// Rank the location as 0 if the country code is CA, otherwise rank it as 1fulfillment_group_location_ranking_add: location?.address.countryCode === "CA" ? 0 : 1,}});};use super::schema; use shopify_function::prelude::*; use shopify_function::Result; use schema::FulfillmentGroupRankedLocations; use crate::schema::Handle; #[shopify_function] fn run(input: schema::run::Input) -> Result<schema::CartFulfillmentGroupsLocationRankingsGenerateRunResult> { // Load the fulfillment groups and generate the rank operations for each one let operations = input .fulfillment_groups() .into_iter() .map(|fulfillment_group| schema::Operation { fulfillment_group_location_ranking_add: build_rank_operation(fulfillment_group, &input.locations()), }) .collect(); // Return the operations Ok(schema::CartFulfillmentGroupsLocationRankingsGenerateRunResult { operations }) } fn build_rank_operation( input: &schema::run::input::FulfillmentGroups, locations: &[schema::run::input::Locations], ) -> FulfillmentGroupLocationRankingAddOperation { FulfillmentGroupLocationRankingAddOperation { fulfillment_group_handle: input.handle().to_string(), rankings: prioritize_canadian_locations(input.inventory_location_handles().to_vec(), locations), } } fn prioritize_canadian_locations( handles: Vec<Handle>, locations: &[schema::run::input::Locations], ) -> Vec<schema::RankedLocation> { // Load the inventory locations for the fulfillment group handles .into_iter() .map(|location_handle| { let location = locations.iter().find(|&loc| *loc.handle() == location_handle); // Rank the location as 0 if the country code is CA, otherwise rank it as 1 schema::RankedLocation { location_handle, rank: match location { Some(location) => { if location.address().country_code() == Some("CA".to_string()).as_ref() { 0 } else { 1 } } None => 1, }, } }) .collect() }
// @ts-check /** * @typedef {import("../generated/api").RunInput} RunInput * @typedef {import("../generated/api").RunInput["fulfillmentGroups"][0]} FulfillmentGroup * @typedef {import("../generated/api").RunInput["locations"][0]} Location * @typedef {import("../generated/api").CartFulfillmentGroupsLocationRankingsGenerateRunResult} CartFulfillmentGroupsLocationRankingsGenerateRunResult * @typedef {import("../generated/api").Operation} Operation * @typedef {import("../generated/api").FulfillmentGroupLocationRankingAddOperation} FulfillmentGroupLocationRankingAddOperation * @typedef {import("../generated/api").RankedLocation} RankedLocation */ /** * @param {RunInput} input * @returns {CartFulfillmentGroupsLocationRankingsGenerateRunResult} */ export function run(input) { // Load the fulfillment groups and generate the rank operations for each one let operations = input.fulfillmentGroups .map(fulfillmentGroup => /** @type {Operation} */( { fulfillment_group_location_ranking_add: buildRankOperation(fulfillmentGroup, input.locations) } )); // Return the operations return { operations: operations }; }; /** * @param {FulfillmentGroup} fulfillmentGroup * @param {Location[]} locations * @returns {FulfillmentGroupLocationRankingAddOperation} */ function buildRankOperation(fulfillmentGroup, locations) { return { fulfillmentGroupHandle: fulfillmentGroup.handle, rankings: prioritizeCanadianLocations(fulfillmentGroup.inventoryLocationHandles, locations), }; }; /** * @param {string[]} locationHandles * @param {Location[]} locations * @returns {RankedLocation[]} */ function prioritizeCanadianLocations(locationHandles, locations) { // Load the inventory locations for the fulfillment group return locationHandles.map(locationHandle => { const location = locations.find((loc) => loc.handle == locationHandle); return { locationHandle, // Rank the location as 0 if the country code is CA, otherwise rank it as 1 fulfillment_group_location_ranking_add: location?.address.countryCode === "CA" ? 0 : 1, } }); };
-
If you're using Rust, then build the function's Wasm module:
Terminal
cargo build --target=wasm32-wasip1 --releaseIf you encounter any errors, then ensure that you've installed Rust and the
wasm32-wasip1
target.
Anchor to Step 2: Preview the function on a development storeStep 2: Preview the function on a development store
To test your function, you need to make it available to your development store.
- If you're developing a function in a language other than JavaScript or TypeScript, ensure you have configured
build.watch
in your function extension configuration.
-
Navigate back to your app root:
Terminal
cd ../..
-
Use the Shopify CLI
dev
command to start app preview:Terminal
shopify app devYou can keep the preview running as you work on your function. When you make changes to a watched file, Shopify CLI rebuilds your function and updates the function extension's drafts, so you can immediately test your changes.
-
Follow the CLI prompts to preview your app, and install it on your development store.
Anchor to Step 3: Test the location ruleStep 3: Test the location rule
You can test your location rule to ensure it's working as expected, and review logs for your function.
Before you test the location rule, make sure that you have the following:
- Two locations in your store in different countries, one of them in Canada.
- One product that is stocked on multiple locations.
-
From the Shopify admin, go to Settings > Shipping and delivery.
-
In the Order routing section, click Manage.
-
Click Add rule.
Your new rule should be an available option.
-
Add your new rule and drag it to the top to make it your highest priority location rule.
-
Open your development store.
-
Add products to your cart. At least one product should be stocked at multiple locations, including your Canadian location.
-
Proceed through checkout.
-
Open your development store's admin, and find your new order. This should be assigned to your Canadian location.
-
Open your terminal where
shopify app dev
is running, and review your function executions.When testing functions on development stores, the output of
dev
includes executions of your functions, any debug logging you have added to them, and a link to a local file with the full function execution details. -
In a new terminal window, use the Shopify CLI
app function replay
command to replay a function execution locally, and debug your function without the need to re-trigger the function execution on Shopify.
Terminal
- Select the function execution from the top of the list. Press
q
to quit when you are finished debugging.
Anchor to Next stepsNext steps
- Add configuration to your location rules using metafields.