Reconciler Customers and data

Test and Bogus Gateway orders in live data

Someone tested checkout on the live theme. A developer ran through the flow with the Bogus Gateway during a project. A staff member placed a practice order to see what the customer gets. None of that money is real, but every one of those orders sits in the same Orders list as your paying customers, in the same export, and in the same revenue number, unless something separates them out.

Python and Node.js Admin GraphQL API Safe by default (dry run)
A close up of a printed circuit board
Photo by Vishnu Mohanan on Unsplash
The short answer

Shopify marks every order with a test field that is true when the order came from test mode, a development store, or the Bogus Gateway. Run a small Python or Node.js script that lists recent orders, reads test and the transaction gateway name with a pure decision function, and tags the ones that are test data with a review tag such as test-order using tagsAdd. Reports and automations can then filter that tag out and never touch a real order. Full code, tests, and a dry run guard are below.

The problem in plain words

Shopify does not hide test orders from you. It puts a small "Test order" label on the order page in the admin, and it sets a test field to true on the order object. But almost nothing downstream checks that field on its own.

A CSV export of orders includes them. A dashboard built on the Admin API that just counts orders and sums totals includes them. A Zapier flow that fires on every new order fires on them too. So a handful of checkout tests from a developer, or a QA pass before launch, or someone poking at a new discount code, quietly inflates your order count and your revenue, and nobody notices until the numbers do not match what actually landed in the bank.

Test checkout test mode or Bogus Gateway Order created test: true on the order nobody checks the flag Reporting tool reads all orders Live revenue is wrong
The order is fake, but nothing told the reporting tool to skip it, so it counts toward live sales anyway.

Why it happens

Shopify sets the test field at the moment the order is created, and it never changes after that. A few common ways stores end up with test data mixed into live reports:

This is a common source of confusion during a launch week or right after a big app install, when test traffic is highest. The orders are not hidden, Shopify shows the test label right on the order page, but a script or a report reading the API has to ask for the field on purpose. See the citations at the end for the exact docs.

The key insight

You do not need to delete test orders to fix this. Deleting is risky and Shopify does not make it easy on purpose. The safe move is to detect them and label them, so every report and automation downstream can ask "is this tagged as test" and skip it, the same way you would filter out a spam row in a spreadsheet rather than delete the whole file.

The fix, as a flow

We do not touch the live checkout or delete anything. We add a job that lists recent orders, checks two signals with a single pure function, the order's own test field and whether any of its transactions ran through the Bogus Gateway, and tags the matches with a review tag. Anything already tagged is left alone so the job never repeats work.

Scheduled job runs on a timer List recent orders last 30 days Read test and gateway test, transactions, tags Test data and untagged? yes no, skip tagsAdd order tagged test-order
The script only tags orders that are already test data by Shopify's own signal, or by gateway name. Everything else is left untouched.

Build it step by step

1

Get an Admin API access token

Create a custom app in your Shopify admin under Settings, Apps and sales channels, Develop apps. Give it the read_orders and write_orders scopes, since tagging an order needs write access, and install it to get an Admin API access token that starts with shpat_. Keep the token and the shop domain in environment variables, never in the file.

setup (shell)
pip install requests

export SHOPIFY_SHOP="yourstore.myshopify.com"
export SHOPIFY_ACCESS_TOKEN="shpat_..."
export TEST_ORDER_TAG="test-order"
export DRY_RUN="true"   # start safe, change to false to write
setup (shell)
// Node 18+ has fetch built in, no dependencies needed

export SHOPIFY_SHOP="yourstore.myshopify.com"
export SHOPIFY_ACCESS_TOKEN="shpat_..."
export TEST_ORDER_TAG="test-order"
export DRY_RUN="true"   // start safe, change to false to write
2

Talk to the Admin GraphQL API

Every call goes to one GraphQL endpoint with your token in the X-Shopify-Access-Token header. A small helper sends a query and returns the data, and raises if Shopify reports an error. We use this same helper to read orders and to run the tag mutation.

step2.py
import os, requests

SHOP = os.environ["SHOPIFY_SHOP"]
TOKEN = os.environ["SHOPIFY_ACCESS_TOKEN"]
API_VERSION = os.environ.get("SHOPIFY_API_VERSION", "2025-01")
ENDPOINT = f"https://{SHOP}/admin/api/{API_VERSION}/graphql.json"

def gql(query, variables=None):
    r = requests.post(
        ENDPOINT,
        json={"query": query, "variables": variables or {}},
        headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"},
        timeout=30,
    )
    r.raise_for_status()
    body = r.json()
    if body.get("errors"):
        raise RuntimeError(body["errors"])
    return body["data"]
step2.js
const SHOP = process.env.SHOPIFY_SHOP;
const TOKEN = process.env.SHOPIFY_ACCESS_TOKEN;
const API_VERSION = process.env.SHOPIFY_API_VERSION || "2025-01";
const ENDPOINT = `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`;

async function gql(query, variables = {}) {
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) throw new Error(`Shopify ${res.status}`);
  const body = await res.json();
  if (body.errors) throw new Error(JSON.stringify(body.errors));
  return body.data;
}
3

List recent orders with the fields the decision needs

Ask for orders from a recent window, and read back the test field, the current total in shop money so we can see how much test revenue is in the mix, the transaction gateways, and the tags. We page through with a cursor so the job handles a busy store.

step3.py
ORDERS_QUERY = """
query($cursor: String) {
  orders(first: 50, after: $cursor, query: "created_at:>-30d") {
    pageInfo { hasNextPage endCursor }
    nodes {
      id name test tags
      currentTotalPriceSet { shopMoney { amount currencyCode } }
      transactions(first: 10) { gateway }
    }
  }
}"""

def recent_orders():
    cursor = None
    while True:
        data = gql(ORDERS_QUERY, {"cursor": cursor})["orders"]
        for node in data["nodes"]:
            yield node
        if not data["pageInfo"]["hasNextPage"]:
            return
        cursor = data["pageInfo"]["endCursor"]
step3.js
const ORDERS_QUERY = `
query($cursor: String) {
  orders(first: 50, after: $cursor, query: "created_at:>-30d") {
    pageInfo { hasNextPage endCursor }
    nodes {
      id name test tags
      currentTotalPriceSet { shopMoney { amount currencyCode } }
      transactions(first: 10) { gateway }
    }
  }
}`;

async function* recentOrders() {
  let cursor = null;
  while (true) {
    const data = (await gql(ORDERS_QUERY, { cursor })).orders;
    for (const node of data.nodes) yield node;
    if (!data.pageInfo.hasNextPage) return;
    cursor = data.pageInfo.endCursor;
  }
}
4

Decide, with one pure function

Keep the decision in its own function that takes an order and returns true or false. A pure function like this is easy to read and easy to test, which we do later. The rule checks two things: Shopify's own test field, and whether any transaction on the order ran through a Bogus Gateway variant, since a store can have test false on an order that still went through Bogus Gateway during certain flows. Either signal is enough to call it test data.

decide.py
BOGUS_GATEWAYS = {"bogus", "bogus_gateway"}

def uses_bogus_gateway(transactions):
    for t in transactions or []:
        gateway = (t.get("gateway") or "").lower()
        if gateway in BOGUS_GATEWAYS:
            return True
    return False

def is_test_order(order):
    """True when an order should be treated as test data, not a live sale."""
    if order.get("test"):
        return True
    return uses_bogus_gateway(order.get("transactions"))

def needs_tag(order, review_tag):
    if not is_test_order(order):
        return False
    return review_tag not in (order.get("tags") or [])
decide.js
const BOGUS_GATEWAYS = new Set(["bogus", "bogus_gateway"]);

export function usesBogusGateway(transactions) {
  for (const t of transactions || []) {
    const gateway = (t.gateway || "").toLowerCase();
    if (BOGUS_GATEWAYS.has(gateway)) return true;
  }
  return false;
}

export function isTestOrder(order) {
  if (order.test) return true;
  return usesBogusGateway(order.transactions);
}

export function needsTag(order, reviewTag) {
  if (!isTestOrder(order)) return false;
  return !(order.tags || []).includes(reviewTag);
}
5

Tag the order, never delete it

When an order needs the tag, call tagsAdd with the order id and the review tag. This is the only write the script makes. It does not cancel the order, does not refund anything, and does not change fulfillment. Always read back userErrors. If Shopify refuses, the error tells you why, and the script should stop on it rather than pretend it worked.

apply.py
TAGS_ADD = """
mutation($id: ID!, $tags: [String!]!) {
  tagsAdd(id: $id, tags: $tags) { node { id } userErrors { field message } }
}"""

def tag_as_test(order_id, review_tag):
    result = gql(TAGS_ADD, {"id": order_id, "tags": [review_tag]})["tagsAdd"]
    if result["userErrors"]:
        raise RuntimeError(result["userErrors"])
apply.js
const TAGS_ADD = `
mutation($id: ID!, $tags: [String!]!) {
  tagsAdd(id: $id, tags: $tags) { node { id } userErrors { field message } }
}`;

async function tagAsTest(orderId, reviewTag) {
  const result = (await gql(TAGS_ADD, { id: orderId, tags: [reviewTag] })).tagsAdd;
  if (result.userErrors.length) throw new Error(JSON.stringify(result.userErrors));
}
6

Wire it together with a dry run guard

The loop ties every piece together. Notice the dry run guard. On the first few runs, leave DRY_RUN on so the script only reports which orders it would tag and how much test money it saw in the window. Read the output, agree with it, then switch it off to let it write. Run it on a schedule, for example once a day, so new test orders never sit unlabeled for long.

Run it safe

Always start with DRY_RUN=true. The script only ever adds a tag, it never deletes or cancels an order, so even a mistaken match just leaves an extra label you can remove, not a lost order.

The full code

Here is the complete script in one file for each language. It reads settings from the environment, logs what it does, respects the dry run flag, and is safe to run again and again because it only tags orders that are already test data by Shopify's own signal, and skips anything already tagged.

View this code on GitHub Full runnable folder with tests in the shopify-fixes repo.

flag_test_orders.py
"""Flag Shopify test and Bogus Gateway orders that leaked into live reporting.

Every order placed with Shopify's own test mode, or paid through the Bogus
Gateway used in development stores and checkout QA, carries a `test` flag set
to true. Those orders are not real sales, but they still show up in the
Orders list, in exports, and in anything that reads the Admin API without
checking that flag. This walks recent orders, decides which ones are test or
bogus-gateway orders with a pure function, and tags the ones that are not
already tagged so reports and automations can filter them out. It never
deletes or cancels an order. Read and tag only. Run on a schedule. Safe to
run again and again.
"""
import os
import logging
import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("flag_test_orders")

SHOP = os.environ["SHOPIFY_SHOP"]
TOKEN = os.environ["SHOPIFY_ACCESS_TOKEN"]
API_VERSION = os.environ.get("SHOPIFY_API_VERSION", "2025-01")
ENDPOINT = f"https://{SHOP}/admin/api/{API_VERSION}/graphql.json"
REVIEW_TAG = os.environ.get("TEST_ORDER_TAG", "test-order")
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"

BOGUS_GATEWAYS = {"bogus", "bogus_gateway"}

ORDERS_QUERY = """
query($cursor: String) {
  orders(first: 50, after: $cursor, query: "created_at:>-30d") {
    pageInfo { hasNextPage endCursor }
    nodes {
      id name test tags
      currentTotalPriceSet { shopMoney { amount currencyCode } }
      transactions(first: 10) { gateway }
    }
  }
}"""

TAGS_ADD = """
mutation($id: ID!, $tags: [String!]!) {
  tagsAdd(id: $id, tags: $tags) { node { id } userErrors { field message } }
}"""


def gql(query, variables=None):
    r = requests.post(
        ENDPOINT,
        json={"query": query, "variables": variables or {}},
        headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"},
        timeout=30,
    )
    r.raise_for_status()
    body = r.json()
    if body.get("errors"):
        raise RuntimeError(body["errors"])
    return body["data"]


def to_cents(amount):
    return round(float(amount) * 100)


def uses_bogus_gateway(transactions):
    for t in transactions or []:
        gateway = (t.get("gateway") or "").lower()
        if gateway in BOGUS_GATEWAYS:
            return True
    return False


def is_test_order(order):
    """True when an order should be treated as test data, not a live sale."""
    if order.get("test"):
        return True
    return uses_bogus_gateway(order.get("transactions"))


def needs_tag(order, review_tag):
    if not is_test_order(order):
        return False
    return review_tag not in (order.get("tags") or [])


def tag_as_test(order_id, review_tag):
    result = gql(TAGS_ADD, {"id": order_id, "tags": [review_tag]})["tagsAdd"]
    if result["userErrors"]:
        raise RuntimeError(result["userErrors"])


def recent_orders():
    cursor = None
    while True:
        data = gql(ORDERS_QUERY, {"cursor": cursor})["orders"]
        for node in data["nodes"]:
            yield node
        if not data["pageInfo"]["hasNextPage"]:
            return
        cursor = data["pageInfo"]["endCursor"]


def run():
    flagged = 0
    leaked_cents = 0
    for order in recent_orders():
        if not is_test_order(order):
            continue
        amount = (order.get("currentTotalPriceSet") or {}).get("shopMoney", {}).get("amount", "0")
        leaked_cents += to_cents(amount)
        if not needs_tag(order, REVIEW_TAG):
            continue
        log.warning("Order %s is test or bogus-gateway data. %s",
                    order["name"], "would tag" if DRY_RUN else "tagging")
        if not DRY_RUN:
            tag_as_test(order["id"], REVIEW_TAG)
        flagged += 1
    log.info("Done. %d order(s) %s. %.2f in test money seen in the window.",
              flagged, "to tag" if DRY_RUN else "tagged", leaked_cents / 100)


if __name__ == "__main__":
    run()
flag-test-orders.js
/**
 * Flag Shopify test and Bogus Gateway orders that leaked into live reporting.
 *
 * Every order placed with Shopify's own test mode, or paid through the Bogus
 * Gateway used in development stores and checkout QA, carries a `test` flag
 * set to true. Those orders are not real sales, but they still show up in
 * the Orders list, in exports, and in anything that reads the Admin API
 * without checking that flag. This walks recent orders, decides which ones
 * are test or bogus-gateway orders with a pure function, and tags the ones
 * that are not already tagged so reports and automations can filter them
 * out. It never deletes or cancels an order. Read and tag only. Run on a
 * schedule.
 *
 * Guide: https://www.allanninal.dev/shopify/test-and-bogus-gateway-orders-in-live-data/
 */
import { pathToFileURL } from "node:url";

const SHOP = process.env.SHOPIFY_SHOP || "example.myshopify.com";
const TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || "shpat_dummy";
const API_VERSION = process.env.SHOPIFY_API_VERSION || "2025-01";
const ENDPOINT = `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`;
const REVIEW_TAG = process.env.TEST_ORDER_TAG || "test-order";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

const BOGUS_GATEWAYS = new Set(["bogus", "bogus_gateway"]);

export function toCents(amount) {
  return Math.round(parseFloat(amount) * 100);
}

export function usesBogusGateway(transactions) {
  for (const t of transactions || []) {
    const gateway = (t.gateway || "").toLowerCase();
    if (BOGUS_GATEWAYS.has(gateway)) return true;
  }
  return false;
}

export function isTestOrder(order) {
  if (order.test) return true;
  return usesBogusGateway(order.transactions);
}

export function needsTag(order, reviewTag) {
  if (!isTestOrder(order)) return false;
  return !(order.tags || []).includes(reviewTag);
}

async function gql(query, variables = {}) {
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) throw new Error(`Shopify ${res.status}`);
  const body = await res.json();
  if (body.errors) throw new Error(JSON.stringify(body.errors));
  return body.data;
}

const ORDERS_QUERY = `
query($cursor: String) {
  orders(first: 50, after: $cursor, query: "created_at:>-30d") {
    pageInfo { hasNextPage endCursor }
    nodes {
      id name test tags
      currentTotalPriceSet { shopMoney { amount currencyCode } }
      transactions(first: 10) { gateway }
    }
  }
}`;

const TAGS_ADD = `
mutation($id: ID!, $tags: [String!]!) {
  tagsAdd(id: $id, tags: $tags) { node { id } userErrors { field message } }
}`;

async function* recentOrders() {
  let cursor = null;
  while (true) {
    const data = (await gql(ORDERS_QUERY, { cursor })).orders;
    for (const node of data.nodes) yield node;
    if (!data.pageInfo.hasNextPage) return;
    cursor = data.pageInfo.endCursor;
  }
}

async function tagAsTest(orderId, reviewTag) {
  const result = (await gql(TAGS_ADD, { id: orderId, tags: [reviewTag] })).tagsAdd;
  if (result.userErrors.length) throw new Error(JSON.stringify(result.userErrors));
}

export async function run() {
  let flagged = 0;
  let leakedCents = 0;
  for await (const order of recentOrders()) {
    if (!isTestOrder(order)) continue;
    const amount = order.currentTotalPriceSet?.shopMoney?.amount ?? "0";
    leakedCents += toCents(amount);
    if (!needsTag(order, REVIEW_TAG)) continue;
    console.warn(`Order ${order.name} is test or bogus-gateway data. ${DRY_RUN ? "would tag" : "tagging"}`);
    if (!DRY_RUN) await tagAsTest(order.id, REVIEW_TAG);
    flagged++;
  }
  console.log(`Done. ${flagged} order(s) ${DRY_RUN ? "to tag" : "tagged"}. ${(leakedCents / 100).toFixed(2)} in test money seen in the window.`);
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  run().catch((err) => { console.error(err); process.exit(1); });
}

Add a test

The decision rule is the part most worth testing, because it decides whether a real order or a test order gets the tag. Because we kept is_test_order and needs_tag pure, the tests need no network and no Shopify account. They just feed in plain objects and check the answer.

test_bogus_gateway_detection.py
from flag_test_orders import is_test_order, needs_tag, uses_bogus_gateway, to_cents


def order(**over):
    base = {
        "test": False,
        "tags": [],
        "transactions": [{"gateway": "shopify_payments"}],
        "currentTotalPriceSet": {"shopMoney": {"amount": "50.00", "currencyCode": "USD"}},
    }
    base.update(over)
    return base


def test_to_cents_rounds():
    assert to_cents("50.00") == 5000
    assert to_cents("9.99") == 999


def test_uses_bogus_gateway_true():
    assert uses_bogus_gateway([{"gateway": "bogus"}]) is True


def test_uses_bogus_gateway_case_insensitive():
    assert uses_bogus_gateway([{"gateway": "Bogus_Gateway"}]) is True


def test_uses_bogus_gateway_false_for_real_gateway():
    assert uses_bogus_gateway([{"gateway": "shopify_payments"}]) is False


def test_is_test_order_true_when_test_flag_set():
    assert is_test_order(order(test=True)) is True


def test_is_test_order_true_when_bogus_gateway_used():
    assert is_test_order(order(transactions=[{"gateway": "bogus"}])) is True


def test_is_test_order_false_for_real_order():
    assert is_test_order(order()) is False


def test_needs_tag_true_for_untagged_test_order():
    assert needs_tag(order(test=True), "test-order") is True


def test_needs_tag_false_when_already_tagged():
    assert needs_tag(order(test=True, tags=["test-order"]), "test-order") is False
flag-test-orders.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { isTestOrder, needsTag, usesBogusGateway, toCents } from "./flag-test-orders.js";

const order = (over = {}) => ({
  test: false,
  tags: [],
  transactions: [{ gateway: "shopify_payments" }],
  currentTotalPriceSet: { shopMoney: { amount: "50.00", currencyCode: "USD" } },
  ...over,
});

test("toCents rounds", () => {
  assert.equal(toCents("50.00"), 5000);
  assert.equal(toCents("9.99"), 999);
});

test("usesBogusGateway true for bogus gateway", () => {
  assert.equal(usesBogusGateway([{ gateway: "bogus" }]), true);
});

test("isTestOrder true when test flag is set", () => {
  assert.equal(isTestOrder(order({ test: true })), true);
});

test("isTestOrder true when bogus gateway used", () => {
  assert.equal(isTestOrder(order({ transactions: [{ gateway: "bogus" }] })), true);
});

test("isTestOrder false for a real order", () => {
  assert.equal(isTestOrder(order()), false);
});

test("needsTag true for an untagged test order", () => {
  assert.equal(needsTag(order({ test: true }), "test-order"), true);
});

test("needsTag false when already tagged", () => {
  assert.equal(needsTag(order({ test: true, tags: ["test-order"] }), "test-order"), false);
});

Case studies

Pre-launch QA

The agency that left forty test orders behind

An agency rebuilt a store's checkout and ran dozens of test purchases with the Bogus Gateway to check every discount code and shipping rule. Launch day arrived, the store went live, and the first week's revenue report included every one of those test orders because nobody had thought to filter them.

Running the script in dry run first showed the exact forty orders and how much fake revenue they added. A day later, every one was tagged test-order, the dashboard filter was updated to exclude the tag, and the numbers matched the bank again.

Ongoing QA

The store that tests discounts in production

A mid-size store tests new discount codes directly on the live theme using test mode, a few times a month, because staging never quite matches production behavior. Each test order sat untagged, and a quarterly revenue reconciliation kept turning up small unexplained gaps.

The team scheduled the script to run nightly. Now every test order gets tagged the same day it is created, the finance export excludes the tag by default, and the small gaps stopped appearing in the reconciliation.

What good looks like

After this runs on a schedule, every test mode or Bogus Gateway order carries a clear tag within a day of being created. Reports, dashboards, and automations can exclude that tag once and trust the rest of the data. Nothing is deleted, nothing is cancelled, so the tag can always be double-checked or removed by hand if a real order is ever mistagged.

FAQ

How do I know if a Shopify order is a test order?

Every order object has a test field. It is true when the order was placed while the store was in test mode, or through a development store, or paid with the Bogus Gateway. That single field is the reliable signal, more reliable than guessing from the customer name or the order total.

What is the Bogus Gateway in Shopify?

Bogus Gateway is the fake payment processor Shopify provides for development stores and checkout testing. It accepts made up card numbers and completes a checkout without moving real money, but the order it creates looks exactly like a normal order unless you check the test flag or the transaction gateway name.

Is it safe to tag test orders automatically with a script?

Yes, when the script only tags and never deletes, cancels, or refunds anything. Tagging is reversible and does not change money or fulfillment, so a script that reads the test flag and the transaction gateway, then calls tagsAdd, is safe to run on a schedule and safe to run again and again.

Related field notes

Citations

On the problem:

  1. Shopify Help Center: test orders and how the test order label appears in the admin. help.shopify.com/en/manual/checkout-settings/test-orders
  2. Shopify Help Center: the Bogus Gateway for testing payments on a development store. help.shopify.com/en/manual/payments/shopify-payments/testing-shopify-payments
  3. Shopify Community: reports and exports counting test orders alongside live sales. community.shopify.com shopify discussions

On the solution:

  1. Shopify Admin GraphQL: the Order object, including the test field and currentTotalPriceSet. shopify.dev/docs/api/admin-graphql/latest/objects/Order
  2. Shopify Admin GraphQL: the tagsAdd mutation. shopify.dev/docs/api/admin-graphql/latest/mutations/tagsAdd
  3. Shopify Admin GraphQL: the orders query and its search syntax. shopify.dev/docs/api/admin-graphql/latest/queries/orders

Stuck on a tricky one?

If you have a problem in Shopify orders, payments, subscriptions, inventory, or fulfillment that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.

Contact me on LinkedIn

Did this clean up your reports?

If this saved you from a wrong revenue number or a pile of stray test orders, you can buy me a coffee. It is the best way to keep these field notes free and growing.

Buy me a coffee on Ko-fi

Back to all Shopify field notes