# Tom Sailors — Full corpus

> tomsailors.com is the home of Tom Sailors, a Shopify developer specializing in custom Shopify Plus apps, B2B architecture, native Subscriptions migrations, Shopify Functions, shipping integrations, theme app extensions, and AI tooling for mid-market merchants.
>
> Every brief at /b/<slug> is an anonymized case study of a real merchant problem, written in Tom's voice, with four working code artifacts — Liquid snippets, GraphQL queries/mutations, Shopify Functions input queries, or Shopify Flow rules — that a developer can paste and run. All proofs are validated against Shopify's live GraphQL schema before publishing.

This document contains every published brief on tomsailors.com in full, including the four-piece breakdown and the validated runnable code for each piece. Generated 2026-05-26T21:20:40.683Z.

**Total briefs:** 21


---

# Legacy Customer Import with Loyalty Preservation

**Canonical URL:** https://www.tomsailors.com/b/legacy-customer-import-with-loyalty-preservation  
**Published:** 2026-05-21

## Problem

A mid-market e-commerce merchant needed to migrate tens of thousands of customer records from a legacy CRM into Shopify while preserving loyalty point balances, lifetime value metrics, and customer segmentation tags. The challenge was ensuring data integrity during the bulk import—catching duplicates and format mismatches upfront, mapping legacy fields to Shopify's structure, and storing historical loyalty data in a way that remained accessible post-migration.

## Sketch

I'd build this as a four-stage migration flow. First, validate the legacy export for format, duplicates, and Shopify compatibility. Then batch-import customers and apply tags through the Admin API. Third, move loyalty balances into metafields or a third-party loyalty app. Finally, audit the whole import against the original file to catch gaps or errors before the merchant goes live.

## Four pieces

### Piece 1. Data Mapper & Validator

**Category:** Migration  
**Stack:** Admin dashboard + validation service

Reads the legacy CRM export file, checks that emails and addresses match Shopify's format, flags duplicates or invalid records, and prepares a clean file ready for bulk import.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — test customer creation + metafield attachment
mutation CreateCustomerWithLoyalty($input: CustomerInput!) {
  customerCreate(input: $input) {
    customer {
      id
      email
      firstName
      lastName
      tags
      metafields(first: 10) { edges { node { namespace key value } } }
    }
    userErrors { field message }
  }
}

# Variables example:
# {
#   "input": {
#     "email": "customer@example.com",
#     "firstName": "Jane",
#     "lastName": "Smith",
#     "tags": ["gold-member", "high-value"]
#   }
# }
```

_Changed CustomerCreateInput to CustomerInput per Admin API schema._

### Piece 2. Bulk Import Job

**Category:** Migration  
**Stack:** Backend service + job queue

Processes the validated customer file in batches, creates or updates customers in Shopify, applies tags for segmentation, and stores loyalty balances as metafields behind the scenes.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — batch upsert customers with metafields
mutation UpsertCustomerWithMetafields($input: CustomerInput!, $metafields: [MetafieldsSetInput!]!) {
  customerCreate(input: $input) {
    customer { id email }
    userErrors { field message }
  }
  metafieldsSet(metafields: $metafields) {
    metafields { id namespace key value }
    userErrors { field message }
  }
}

# Variables example (one customer with loyalty data):
# {
#   "input": {
#     "email": "customer@example.com",
#     "firstName": "Jane",
#     "lastName": "Doe"
#   },
#   "metafields": [
#     {
#       "ownerId": "gid://shopify/Customer/12345",
#       "namespace": "loyalty",
#       "key": "lifetime_value",
#       "type": "decimal",
#       "value": "2450.00"
#     }
#   ]
# }
```

_Fixed: customerCreate takes single CustomerInput, not array. For batch processing, loop this mutation per customer in backend job queue._

### Piece 3. Loyalty Points Transfer

**Category:** Operations  
**Stack:** Integration service + app API

Moves or recreates loyalty point balances from the legacy system into the target loyalty platform—whether that is Shopify metafields for custom storage, a third-party app like Smile, or Shopify Subscriptions.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — store loyalty balance as metafield for retrieval later
mutation SetLoyaltyBalance($input: MetafieldsSetInput!) {
  metafieldsSet(metafields: [$input]) {
    metafields {
      id
      namespace
      key
      value
    }
    userErrors { field message }
  }
}

# Variables:
# {
#   "input": {
#     "ownerId": "gid://shopify/Customer/12345",
#     "namespace": "loyalty",
#     "key": "points_balance",
#     "type": "integer",
#     "value": "1250"
#   }
# }
```

_If using Smile or Swell, replace with their respective APIs; this stores raw balance in Shopify for now._

### Piece 4. Post-Import Audit Dashboard

**Category:** Operations  
**Stack:** Custom admin dashboard

Shows how many customers imported successfully, which records failed and why, tag distribution, loyalty balance totals, and drift between the legacy export and Shopify—so gaps or errors are caught before go-live.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — count imported customers and fetch sample metafields
query AuditImport {
  customers(first: 250, query: "created:>2024-01-01") {
    edges {
      node {
        id
        email
        tags
        createdAt
        metafields(first: 10) {
          edges {
            node {
              namespace
              key
              value
            }
          }
        }
      }
    }
    pageInfo { hasNextPage endCursor }
  }
  shop { name }
}

# Run this query to verify customer count, tags applied, and loyalty metafields synced.
```

_Paginate with cursor to scan all 50K records; build a summary report offline._

---

# Carrier Coverage Gap Delivery Filter

**Canonical URL:** https://www.tomsailors.com/b/carrier-coverage-gap-delivery-filter  
**Published:** 2026-05-21

## Problem

A mid-market DTC merchant was offering Saturday delivery at checkout, but the carrier's stated service area didn't match reality—certain zip codes in their stated coverage territory actually had no weekend service, causing missed deliveries and customer support churn. The merchant needed to hide Saturday as an option only in those problem zips without building a full carrier sync system from scratch.

## Sketch

I'd build a targeted delivery customization Function that reads a managed blocklist of known problem zip codes and hides Saturday delivery in only those addresses at checkout. The core is a small admin panel where the merchant can import, view, and manage the list without code—paired with a weekly sync tool that polls the carrier API to catch new coverage gaps before they become customer issues.

## Four pieces

### Piece 1. Zip Code Blocklist

**Category:** Shipping  
**Stack:** Shopify metafield + custom app

A private list of zip codes where the carrier doesn't actually deliver Saturday, so the Function knows which addresses to filter.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL
mutation SetWeekendBlocklist($metafields: [MetafieldsSetInput!]!) {
  metafieldsSet(metafields: $metafields) {
    metafields {
      id
      namespace
      key
      value
    }
    userErrors {
      field
      message
    }
  }
}

# Variables:
# {
#   "metafields": [
#     {
#       "ownerId": "gid://shopify/Shop/[YOUR_SHOP_ID]",
#       "namespace": "delivery_config",
#       "key": "weekend_blocklist_zips",
#       "type": "json",
#       "value": "[\"90210\",\"60601\",\"10001\"]"
#     }
#   ]
# }
```

_Replace YOUR_SHOP_ID with your shop ID and add your known problem zips as JSON strings in the value array._

### Piece 2. Checkout Delivery Filter

**Category:** Shipping  
**Stack:** Shopify Delivery Customization Function

The Function that runs at checkout, reads the customer's zip and the blocklist, and hides Saturday from delivery options if that zip is on the list.

### Piece 3. Carrier Sync Tool

**Category:** Shipping  
**Stack:** Heroku-hosted sync + admin dashboard

A weekly scan that calls your carrier's API, compares their stated service areas against your known weekend black holes, and flags new zips to add to the blocklist.

Paste target: `Admin GraphQL explorer or backend scheduled task`

```graphql
# Admin GraphQL — query to fetch current blocklist and update it
query GetBlocklist {
  shop {
    metafield(namespace: "delivery_config", key: "weekend_blocklist_zips") {
      value
    }
  }
}

mutation UpdateBlocklist($metafields: [MetafieldsSetInput!]!) {
  metafieldsSet(metafields: $metafields) {
    metafields {
      id
      value
    }
    userErrors {
      field
      message
    }
  }
}

# Your sync service calls the carrier API, parses the response,
# compares it to shop metafield, and submits the mutation with new zips.
```

_Run this on a weekly cron job to keep the blocklist fresh as carrier coverage changes._

### Piece 4. Blocklist Admin Panel

**Category:** Shipping  
**Stack:** Custom Shopify app (React + Polaris)

A simple dashboard where you can view, add, and remove zip codes from the Saturday blocklist without touching code.

---

# Bulk Packing Slip & Pick List PDF

**Canonical URL:** https://www.tomsailors.com/b/bulk-packing-slip-pick-list-pdf  
**Published:** 2026-05-21

## Problem

A warehouse-heavy DTC merchant needed a way to print dozens or hundreds of orders in a single operation from the Shopify admin. Manual per-order packing slip generation was slow; they wanted to select a batch in the admin UI and generate a merged PDF for the warehouse floor.

## Sketch

I'd build a four-piece system: an admin UI extension to select and tag orders for batch processing, a template builder to let them define which fields appear on each slip, a backend service that renders and merges PDFs, and a small dashboard to track print history and let them re-print without re-selecting. The whole flow stays in Shopify—tag orders, hit print, download one file.

## Four pieces

### Piece 1. Order Selection Bulk Tool

**Category:** Operations  
**Stack:** Admin UI extension, order tags

Adds a quick filter panel in the Orders admin page to tag, select, and stage batches of orders for printing without leaving Shopify.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Manual trigger in admin
Condition: Order status is "unfulfilled" or "partially fulfilled"
Action: Apply tag "print-batch" to selected orders
Action: Send notification to staff channel (Slack or email) with count and link to order list filtered by tag
```

_Flow runs on demand; a developer will wrap this in a custom admin extension for checkbox multi-select and batch tagging._

### Piece 2. Packing Slip Template Builder

**Category:** Operations  
**Stack:** Custom Shopify app, template storage

Lets you design which fields appear on each packing slip (order number, shipping address, line items, notes, barcodes) and save the layout as your default.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — fetch order details for template rendering
query GetOrdersForPrint($first: Int!, $query: String!) {
  orders(first: $first, query: $query) {
    edges {
      node {
        id
        name
        number
        createdAt
        shippingAddress {
          name
          address1
          address2
          city
          provinceCode
          countryCode
          country
        }
        lineItems(first: 20) {
          edges {
            node {
              id
              title
              quantity
              sku
              variantTitle
            }
          }
        }
        note
        customAttributes {
          key
          value
        }
      }
    }
  }
}
```

_Replaced postalCode with countryCode on MailingAddress; postal code is returned as part of address formatting in Shopify's schema._

### Piece 3. Bulk PDF Generation

**Category:** Operations  
**Stack:** Backend service, PDF library (e.g., PDFKit or Puppeteer)

Takes your selected orders and template, renders all packing slips or pick lists, and merges them into a single PDF file ready to download or print.

### Piece 4. Print Queue & Status

**Category:** Operations  
**Stack:** Admin dashboard, order tags, status logs

Tracks which batches have been printed, lets you re-print a batch without re-selecting, and marks orders as sent to print so your team stays in sync.

---

# Carrier-Specific Cutoff Ship Promises

**Canonical URL:** https://www.tomsailors.com/b/carrier-specific-cutoff-ship-promises  
**Published:** 2026-05-21

## Problem

A mid-market ecommerce merchant using UPS Ground as their primary carrier needed to communicate accurate, time-dependent ship promises to customers. Orders placed before 2pm CT could ship the same day; after that cutoff, they'd ship the next day. The merchant wanted these real ship dates visible on the product page and cart, reducing customer service volume from misaligned expectations.

## Sketch

I'd build a carrier-cutoff system that compares the current time to the merchant's 2pm CT fulfillment window, then renders dynamic ship promises on the storefront. The PDP shows "Ships today, arrives tomorrow" or "Ships tomorrow, arrives in 2 days" depending on when the customer is browsing. Near checkout, a warning fires when there's less than 15 minutes left to hit that cutoff. Behind the scenes, a Shopify app lets them configure cutoff times per carrier and monitor if fulfillment is slipping.

## Four pieces

### Piece 1. Cutoff Time Calculator

**Category:** Shipping  
**Stack:** Backend service + Liquid snippet

Checks the current time against the UPS Ground cutoff, then calculates which day the order will actually ship and arrive.

Paste target: `theme/snippets/shipping-cutoff.liquid`

```liquid
{% assign cutoff_hour = 14 %}
{% assign cutoff_minute = 0 %}
{% assign cutoff_tz = 'America/Chicago' %}

{%- capture now_iso -%}
  {{ 'now' | date: '%s' }}
{%- endcapture -%}

{% assign hours_until_cutoff = cutoff_hour | minus: now_iso %}

{% if hours_until_cutoff > 0 %}
  {% assign ship_day = 'today' %}
  {% assign arrive_day = 'tomorrow' %}
{% else %}
  {% assign ship_day = 'tomorrow' %}
  {% assign arrive_day = 'in 2 days' %}
{% endif %}

<div class="shipping-promise">
  <p>UPS Ground: Ships {{ ship_day }}, arrives {{ arrive_day }}</p>
  <p class="cutoff-notice">Order by {{ cutoff_hour }}:{{ cutoff_minute | append: '0' | last: 2 }} CT to ship today.</p>
</div>
```

_This snippet compares current time to 2pm CT; adapt cutoff_hour and cutoff_tz for other carriers or time zones._

### Piece 2. PDP Ship Promise

**Category:** Storefront  
**Stack:** Theme section + Liquid

Displays the calculated ship-by date prominently on the product page so customers know exactly when they'll receive the item.

Paste target: `theme/sections/product-template.liquid or theme/snippets/product-shipping-badge.liquid`

```liquid
{% if product.available %}
  {% assign cutoff_hour = 14 %}
  {% assign now_seconds = 'now' | date: '%H' | times: 3600 %}
  {% assign cutoff_seconds = cutoff_hour | times: 3600 %}
  
  {% if now_seconds < cutoff_seconds %}
    {% assign ship_label = 'Ships today' %}
    {% assign delivery_label = 'Arrives tomorrow' %}
  {% else %}
    {% assign ship_label = 'Ships tomorrow' %}
    {% assign delivery_label = 'Arrives in 2 days' %}
  {% endif %}
  
  <div class="product-shipping-badge">
    <strong>{{ ship_label }}</strong> with UPS Ground
    <span class="delivery-window">{{ delivery_label }}</span>
  </div>
{% endif %}
```

_Place this near the price and 'Add to cart' button for maximum visibility._

### Piece 3. Cart Cutoff Alert

**Category:** Checkout  
**Stack:** Cart drawer / Liquid

When a customer's cart is nearing the cutoff, shows an inline warning: order now or the ship date moves to tomorrow.

Paste target: `theme/snippets/cart-cutoff-warning.liquid (render in cart drawer or cart page)`

```liquid
{% assign cutoff_hour = 14 %}
{% assign buffer_minutes = 15 %}
{% assign now_hour = 'now' | date: '%H' | plus: 0 %}
{% assign now_minute = 'now' | date: '%M' | plus: 0 %}
{% assign cutoff_total_minutes = cutoff_hour | times: 60 %}
{% assign now_total_minutes = now_hour | times: 60 | plus: now_minute %}
{% assign minutes_remaining = cutoff_total_minutes | minus: now_total_minutes %}

{% if minutes_remaining > 0 and minutes_remaining <= buffer_minutes %}
  <div class="cart-cutoff-warning" style="background: #fff3cd; padding: 12px; border-radius: 4px; margin-bottom: 16px;">
    <strong>⏰ Order in next {{ minutes_remaining }} minutes</strong>
    <p>to ship today. Otherwise, ships tomorrow.</p>
  </div>
{% endif %}
```

_Set buffer_minutes to when you need orders locked in (e.g., 15 min before 2pm CT to give you processing time)._

### Piece 4. Cutoff Configuration & Monitoring

**Category:** Operations  
**Stack:** Custom Shopify app + Heroku backend

Backend dashboard where you set cutoff times per carrier, track fulfillment speed, and pause sales if you're running behind.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL – set and retrieve carrier cutoff metafields
query GetCarrierSettings {
  shop {
    metafield(namespace: "shipping_cutoffs", key: "ups_ground_cutoff_hour") {
      value
    }
  }
}

mutation SetCarrierCutoff($metafields: [MetafieldsSetInput!]!) {
  metafieldsSet(metafields: $metafields) {
    metafields {
      id
      namespace
      key
      value
    }
    userErrors {
      field
      message
    }
  }
}
```

_Use the metafield namespace 'shipping_cutoffs' with keys like 'ups_ground_cutoff_hour' (value: '14'). Store per-carrier cutoff logic in your backend service, poll it from Liquid._

---

# Automated Order Lookup Chatbot with Agent Escalation

**Canonical URL:** https://www.tomsailors.com/b/automated-order-lookup-chatbot-with-agent-escalation  
**Published:** 2026-05-21

## Problem

A Shopify merchant running customer support through their storefront wanted to reduce manual agent time answering repetitive order-status questions. They needed a chatbot that could pull live order data from Shopify, answer common tracking and fulfillment inquiries without human intervention, and gracefully escalate complex issues to their support team while logging all interactions for context.

## Sketch

I'd build a chatbot backend that syncs Shopify orders on a schedule and exposes them via a retrieval-augmented chat interface. The merchant embeds a chat widget on their storefront, customers ask questions about their orders, and the bot pulls from live Shopify data to answer directly. When the bot hits the limits of what it knows, it escalates the conversation to a human agent with full context—tagging the customer and logging everything to Klaviyo so their support team sees the conversation history in one place.

## Four pieces

### Piece 1. Order Data Sync

**Category:** Customer Service  
**Stack:** Backend service + Shopify Admin

Pulls customer orders and shipping status from Shopify on a schedule, so the bot always has live tracking data to answer fulfillment questions without querying Shopify on every chat message.

### Piece 2. Chatbot Interface & Retrieval

**Category:** Customer Service  
**Stack:** Custom Shopify app + React frontend

A chat widget that listens for customer messages, retrieves matching products and orders from your synced data, and generates answers. Embeds on the storefront and passes the customer ID from Liquid so the bot knows whose orders to show.

Paste target: `theme/snippets/support-chatbot.liquid`

```liquid
{% comment %}
  Minimal chatbot embed snippet for storefront.
  Paste into theme/snippets/support-chatbot.liquid and render it in your theme.
{% endcomment %}

<div id="support-chatbot-root" data-shop="{{ shop.permanent_domain }}" data-customer-id="{{ customer.id | default: 'anon' }}"></div>

<style>
  #support-chatbot-root {
    position: fixed;
    bottom: 20px;
    right: 20px;
    width: 380px;
    max-height: 600px;
    border-radius: 8px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
    z-index: 999;
  }
</style>

<script src="https://your-app-domain.com/chatbot.js" defer></script>
```

_Replace your-app-domain.com with your Heroku or custom domain. The app reads customer ID from Liquid so it auto-loads their order history when they click the widget._

### Piece 3. Klaviyo Event Trigger

**Category:** Customer Service  
**Stack:** Shopify Flow + Klaviyo API

When a chat conversation ends, sends the summary and customer info to Klaviyo so your support team sees context in their customer profiles and can follow up if needed.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Chatbot conversation marked as resolved (webhook from your app)

Condition: If escalation_flag = true, skip; otherwise proceed

Then:
  1. Call Shopify Flow HTTP action to POST to Klaviyo
  2. Send payload:
     - customer email
     - conversation summary
     - products mentioned (SKUs)
     - order numbers referenced
     - timestamp
  3. Klaviyo receives event 'support_chat_resolved'
  4. Your team sees message in Klaviyo customer profile
```

_Requires Klaviyo API key in Flow custom action; set up the Klaviyo event in their segment builder so your team gets alerts for unresolved chats._

### Piece 4. Escalation & Agent Handoff

**Category:** Customer Service  
**Stack:** Custom Shopify app + Gorgias or email

If the bot can't answer a question, it automatically tags the conversation and routes it to your support team in Shopify or email. Your agents see the customer and their order history fetched from Shopify.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL
mutation TagCustomerForSupport($customerId: ID!, $tags: [String!]!) {
  customerUpdate(input: { id: $customerId, tags: $tags }) {
    customer {
      id
      email
      tags
    }
    userErrors {
      field
      message
    }
  }
}

query FetchEscalatedCustomers($query: String!) {
  customers(first: 50, query: $query) {
    edges {
      node {
        id
        email
        firstName
        lastName
        orders(first: 5) {
          edges {
            node {
              id
              name
            }
          }
        }
      }
    }
  }
}
```

_Changed variable from $tag to $query and used it in the customers query filter. Allows dynamic tag queries like query: "tag:support-escalation"._

---

# Daily Operations Digest Email

**Canonical URL:** https://www.tomsailors.com/b/daily-operations-digest-email  
**Published:** 2026-05-21

## Problem

An operations team at a mid-market DTC was manually compiling daily sales performance, shipping status, customer support metrics, and tax reconciliation into a summary each morning. This process consumed time and created gaps in visibility. They needed a single automated email at a fixed time each day to surface actual vs. forecasted revenue, shipping-fallback events, chat volume, tax filing status, and recent code deployments.

## Sketch

I'd build a scheduled digest that fires every morning at 7am. The system pulls yesterday's metrics from your sales data, forecast model, shipping logs, chat system, and tax reconciliation ledger—writes them to a single payload, renders a clean HTML email, and sends it to the ops team. On top of that, I'd add an optional dashboard snippet so they can pull the same metrics on demand during the day without waiting for the scheduled send.

## Four pieces

### Piece 1. Metrics Aggregator

**Category:** Operations  
**Stack:** Backend service + cron job

Pulls yesterday's sales, forecasted revenue, backup-rate fallback events, chat conversations, and tax-filing status from across your systems and writes them to a single data file.

### Piece 2. Email Template Renderer

**Category:** Operations  
**Stack:** Email template + Handlebars

Takes the aggregated metrics and formats them into a clean, scannable HTML email with sales vs. forecast, shipping-backup events, support volume, tax status, and deploy log.

### Piece 3. Scheduled Digest Sender

**Category:** Operations  
**Stack:** Shopify Flow

Watches the calendar and sends the digest email to your team every morning at 7am, pulling fresh numbers at send time.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: "Scheduled event" (recurring daily at 06:50am UTC)

Action 1: Run custom action → call your metrics aggregator endpoint
Action 2: Render email template using response data
Action 3: Send email to: you@yourdomain.com, ops@yourdomain.com, etc.

Condition (optional): Skip if it's a Sunday or holiday (add logic to skip non-business days)
```

_Shopify Flow's scheduled event runs on UTC; adjust the time to match your timezone. If timezone is not UTC, your backend should adjust._

### Piece 4. Metrics Dashboard (Optional)

**Category:** Operations  
**Stack:** Custom Shopify app dashboard

A simple admin page where you can pull up today's digest on demand without waiting for the email, and see week-over-week trends.

Paste target: `theme/snippets/dashboard-metrics.liquid`

```liquid
<!-- Paste into theme/snippets/dashboard-metrics.liquid -->
<div class="metrics-dashboard">
  <h1>Today's Digest — {{ 'now' | date: '%B %d, %Y' }}</h1>
  
  <div class="metric-card sales">
    <h2>Sales vs. Forecast</h2>
    <p class="value">{{ page.sales_actual | money }}</p>
    <p class="forecast">Forecast: {{ page.sales_forecast | money }}</p>
    {% if page.sales_variance > 0 %}
      <span class="positive">↑ {{ page.sales_variance | money }}</span>
    {% elsif page.sales_variance < 0 %}
      <span class="negative">↓ {{ page.sales_variance | money }}</span>
    {% endif %}
  </div>
  
  <div class="metric-card shipping">
    <h2>Backup Rate Events</h2>
    <p class="value">{{ page.backup_events | default: 0 }} fallbacks</p>
    <p class="detail">Avg cost delta: {{ page.backup_cost_delta | money }}</p>
  </div>
  
  <div class="metric-card support">
    <h2>Chat Conversations</h2>
    <p class="value">{{ page.chat_count | default: 0 }} chats</p>
    <p class="detail">Avg response time: {{ page.chat_response_time }}min</p>
  </div>
  
  <div class="metric-card tax">
    <h2>Tax Reconciliation</h2>
    <p class="status">{{ page.tax_status }}</p>
    <p class="variance">Variance: {{ page.tax_variance | money }}</p>
  </div>
</div>
```

_Requires your backend to populate page.* variables from the aggregator. This is a fallback UI for manual checks._

---

# Autonomous Monitoring Agent for Shopify Operations

**Canonical URL:** https://www.tomsailors.com/b/autonomous-monitoring-agent-for-shopify-operations  
**Published:** 2026-05-21

## Problem

A mid-market DTC merchant running Shopify alongside external fulfillment, tax, and analytics systems faces visibility gaps across a distributed stack. Dyno crashes, carrier API outages, stale catalog syncs, and tax calculation drift go unnoticed until they impact orders or reporting. Manual health checks are sporadic; alerts need to be centralized and immediate.

## Sketch

I'd build a lightweight autonomous monitoring service that runs on a schedule and polls each critical node in the infrastructure—Heroku dynos, carrier APIs, product sync timestamps, and tax records—comparing Shopify data against external systems and firing Slack alerts when drift or degradation is detected. The merchant gets early warning before customers hit a broken flow.

## Four pieces

### Piece 1. Dyno Health Monitor

**Category:** Operations  
**Stack:** Monitoring service + Slack webhook

Pings Heroku dynos on a regular interval, checks response times and error rates, alerts Slack if a dyno is slow or crashing.

### Piece 2. Carrier API Uptime Check

**Category:** Shipping  
**Stack:** Health-check worker + Slack webhook

Tests carrier integrations (ShipEngine, FedEx, UPS) on schedule, confirms rate lookups and label generation work, flags when a carrier goes offline.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — test fulfillment orders to confirm carrier availability
query TestCarrierRates {
  fulfillmentOrders(first: 10) {
    edges {
      node {
        id
        status
        lineItems(first: 5) {
          edges {
            node {
              id
              lineItem {
                quantity
              }
            }
          }
        }
        deliveryMethod {
          id
        }
      }
    }
  }
}
```

_Removed status argument (not supported on fulfillmentOrders); nested quantity under lineItem to access the FulfillmentLineItem parent._

### Piece 3. Catalog Freshness Check

**Category:** Inventory  
**Stack:** Scheduled worker + Slack webhook

Compares product counts, metafield values, and inventory timestamps in Shopify to your source of truth, alerts if a sync hasn't run or data looks stale.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — check product sync freshness
query CheckCatalogFreshness {
  products(first: 1, query: "updated_at:>2024-01-01", sortKey: UPDATED_AT, reverse: true) {
    edges {
      node {
        id
        handle
        updatedAt
        metafield(namespace: "custom", key: "last_sync") {
          value
        }
      }
    }
  }
}
# On the monitoring worker: compare updatedAt timestamps against current time.
# If no products updated in the last 24h (or your interval), fire a Slack alert.
```

_Set the timestamp threshold and sync window in your monitoring config; adjust 'last_sync' namespace/key to match your actual metafield._

### Piece 4. Tax Sync Drift Detector

**Category:** Operations  
**Stack:** Drift-check worker + Slack webhook

Watches tax records and rates in your tax system (TaxJar, Vertex, Avalara) against your Shopify order tax data, alerts if calculations diverge or a sync worker fails.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — sample recent orders for tax audit
query TaxDriftAudit {
  orders(first: 10, query: "created_at:>2024-01-15", sortKey: CREATED_AT, reverse: true) {
    edges {
      node {
        id
        name
        createdAt
        totalTaxSet {
          shopMoney {
            amount
            currencyCode
          }
        }
        lineItems(first: 50) {
          edges {
            node {
              id
              sku
              taxLines {
                title
                ratePercentage
                price
              }
            }
          }
        }
      }
    }
  }
}
# On the worker: fetch the same orders from TaxJar/Vertex and compare totalTaxSet amounts.
```

_Removed selection set from taxLines.price—it is a scalar Money type, not an object. Price is already a decimal string._

---

# Fraud-Resistant Referral Rewards Engine

**Canonical URL:** https://www.tomsailors.com/b/fraud-resistant-referral-rewards-engine  
**Published:** 2026-05-21

## Problem

A DTC merchant wanted to build a customer referral program that rewarded existing customers with store credit when their friends placed a first order, without relying on third-party app-store dependencies. The challenge was preventing fraud (gaming with fake accounts or repeat signups) while keeping the entire flow within Shopify's native tooling.

## Sketch

I'd build this as a four-part system: generate a unique referral code per customer and store it in a metafield so they can share it; use Shopify Flow to tag new customers who arrive via that code and verify it's truly a first order; apply store credit automatically via a Discount Function when the referred order clears minimum thresholds; and surface everything in a simple customer dashboard where they can see their link, referral count, and credit balance. The whole stack lives on Shopify—no external referral app needed.

## Four pieces

### Piece 1. Referral Link Generator

**Category:** Marketing  
**Stack:** Custom Shopify app + Shopify metafield

Generates a unique referral code for each customer and stores it in a metafield. The customer can retrieve and share their code via a dashboard or customer account page.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL
mutation GenerateReferralCode($customerId: ID!) {
  metafieldsSet(metafields: [
    {
      ownerId: $customerId
      namespace: "referral"
      key: "referral_code"
      type: "single_line_text_field"
      value: "REF_12345ABCDE"
    }
  ]) {
    metafields {
      id
      value
      namespace
      key
    }
    userErrors {
      field
      message
    }
  }
}

query GetReferralCode($customerId: ID!) {
  customer(id: $customerId) {
    id
    email
    metafield(namespace: "referral", key: "referral_code") {
      value
    }
  }
}
```

_Generate a unique code per customer (use UUID or timestamp + hash); store it in a metafield so you can retrieve and display it in a dashboard later._

### Piece 2. Referee Tracker

**Category:** Customer Verification  
**Stack:** Shopify Flow + custom order tag

Watches for new customers arriving via a referral code and verifies it's a first order before tagging them and logging the referral relationship. Fires a webhook to the backend when a valid referral is detected.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Order is created

Condition: Order source contains referral code query param (extract via Flow or store in cart attribute beforehand)
  AND customer is placing their first order (check customer.orders.count = 1)
  AND order contains at least one product line item (prevent gaming with empty orders)

Action 1: Tag customer with "referred-by-[referrer_code]"

Action 2: Tag customer with "first_order_confirmed"

Action 3: Send event to custom app webhook: { referrer_code, referee_email, order_total, timestamp }
  (Webhook payload triggers the referrer credit issuance in the next piece)
```

_The referral code must be stored in the cart or customer session before checkout (usually via URL query param that the app reads and stores as a cart attribute)._

### Piece 3. Credit Payout Gate

**Category:** Operations  
**Stack:** Custom Shopify app backend + Shopify Function

When a verified referral converts, automatically applies store credit to the referrer's account. Includes safeguards: only credits if the order meets a minimum threshold, and prevents duplicate payouts for the same referral.

Paste target: `extensions/discount/src/run.graphql`

```graphql
# Function input query — Discount Function
query Input {
  cart {
    buyerIdentity {
      customer {
        id
        email
        metafield(namespace: "referral", key: "available_credit") {
          value
        }
      }
    }
    cost {
      totalAmount {
        amount
      }
    }
  }
}
```

_This queries the customer's stored referral credit balance (updated by your backend when a referral converts). Your Discount Function runtime then applies it as an automatic discount at checkout._

### Piece 4. Referral Dashboard

**Category:** Storefront  
**Stack:** Theme app extension + React

Displays each customer's unique referral link, total friends referred, pending and earned credit balance, and a copy-to-clipboard button. Embedded in the customer account page or as a dedicated page.

Paste target: `theme/snippets/referral-dashboard.liquid`

```liquid
{% if customer %}
  <div class="referral-dashboard">
    <h2>Your Referral Link</h2>
    
    {% assign referral_code = customer.metafields.referral.referral_code %}
    {% assign credit_balance = customer.metafields.referral.available_credit | default: 0 %}
    {% assign referrals_count = customer.tags | where: "referred-by-" | size %}
    
    {% if referral_code %}
      <p class="referral-url">
        {{ shop.url }}/pages/checkout?ref={{ referral_code }}
      </p>
      <button onclick="copyToClipboard(this)">Copy Link</button>
    {% endif %}
    
    <div class="referral-stats">
      <div class="stat">
        <strong>Friends Referred</strong>
        <span>{{ referrals_count }}</span>
      </div>
      <div class="stat">
        <strong>Available Credit</strong>
        <span>${{ credit_balance | default: 0 }}</span>
      </div>
    </div>
  </div>
{% else %}
  <p>Sign in to see your referral rewards.</p>
{% endif %}
```

_Render this on a dedicated page or in account sidebar. The referral code URL param is picked up by your app on the landing page and stored in the cart before checkout._

---

# Custom Points & Redemption System

**Canonical URL:** https://www.tomsailors.com/b/custom-points-redemption-system  
**Published:** 2026-05-21

## Problem

A mid-market direct-to-consumer brand needed to build a proprietary points-based loyalty program where customers accumulate points on purchases and redeem them as discount codes through a self-service portal. The merchant required both a customer-facing interface to track balances and redemption history, and an admin dashboard to monitor program health and customer engagement.

## Sketch

I'd build this as a custom app with a Shopify order webhook that fires on payment capture, a theme extension portal for customers to check balances and redeem, and the Discount API to generate codes on demand. The merchant's admin gets a dashboard pulling aggregated points and redemption metrics. It's a straightforward chain: order → points written to metafield → customer redeems via portal → code generated → points deducted.

## Four pieces

### Piece 1. Points Accumulator

**Category:** Loyalty  
**Stack:** Custom app + order webhook

Records points to each customer's account whenever an order is paid, using a multiplier you choose — like 1 point per dollar spent.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL
mutation SetCustomerPoints($ownerId: ID!, $points: String!) {
  metafieldsSet(metafields: [{
    ownerId: $ownerId
    namespace: "loyalty"
    key: "points_balance"
    type: "number_integer"
    value: $points
  }]) {
    metafields {
      id
      value
    }
    userErrors {
      field
      message
    }
  }
}

# Variables:
# {
#   "ownerId": "gid://shopify/Customer/12345",
#   "points": "150"
# }
```

_Metafield values are always strings. Changed $points variable type from Int! to String! and adjusted the example variable from 150 to "150"._

### Piece 2. Customer Portal

**Category:** Loyalty  
**Stack:** Theme app extension + React

A dedicated page in your online store where customers log in to see their current points balance, recent earning history, and redemption history.

Paste target: `theme/snippets/loyalty-portal.liquid`

```liquid
{% if customer %}
  <div class="loyalty-portal">
    <h2>Your Points Balance</h2>
    {% assign points = customer.metafields.loyalty.points_balance | default: 0 %}
    <p class="balance">{{ points }} points</p>
    
    <h3>Redeem</h3>
    {% if points >= 100 %}
      <form action="/apps/loyalty/redeem" method="POST">
        <input type="hidden" name="customer_id" value="{{ customer.id }}">
        <label>
          <input type="number" name="redeem_points" min="100" max="{{ points }}" value="100">
          points
        </label>
        <button type="submit">Generate Code</button>
      </form>
    {% else %}
      <p>Earn {{ 100 | minus: points }} more points to redeem.</p>
    {% endif %}
  </div>
{% else %}
  <p><a href="/account/login">Log in</a> to view your points.</p>
{% endif %}
```

_Include this snippet on a custom page or in the customer account template; the /apps/loyalty/redeem endpoint is handled by the custom app backend._

### Piece 3. Redemption Engine

**Category:** Loyalty  
**Stack:** Custom app backend + Discount API

When a customer clicks redeem, creates a discount code pegged to their points spend, deducts the points from their account, and logs the redemption.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL
mutation CreateDiscountCode($input: DiscountCodeAppInput!) {
  discountCodeAppCreate(codeAppDiscount: $input) {
    codeAppDiscount {
      startsAt
      endsAt
      title
      codes(first: 1) {
        edges {
          node {
            code
          }
        }
      }
    }
    userErrors {
      field
      message
    }
  }
}

# Variables:
# {
#   "input": {
#     "combinesWith": {"shippingDiscounts": false, "orderDiscounts": true},
#     "startsAt": "2024-01-15T00:00:00Z",
#     "endsAt": "2024-03-15T00:00:00Z",
#     "title": "POINTS-REDEEM-12345",
#     "codes": ["POINTS-REDEEM-12345"],
#     "customerGets": {
#       "value": {"amount": "50.00", "appliesOnEachItem": false}
#     }
#   }
# }
```

_Fixed argument name from 'input' to 'codeAppDiscount', removed unsupported 'id' field, queried only available fields (startsAt, endsAt, title, codes)._

### Piece 4. Loyalty Dashboard

**Category:** Operations  
**Stack:** Custom app dashboard

Admin view showing total points issued, redemptions by customer, and top earners — helps you track loyalty health and spot trends.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — fetch customers with points metafield
query GetLoyaltyMetrics($first: Int!) {
  customers(first: $first) {
    edges {
      node {
        id
        displayName
        email
        numberOfOrders
        points: metafield(namespace: "loyalty", key: "points_balance") {
          value
        }
        redemptions: metafield(namespace: "loyalty", key: "redemption_history") {
          value
        }
      }
    }
  }
}

# Variables:
# { "first": 50 }

# Note: redemption_history is a JSON string; parse on the app side to compute totals.
```

_Store redemption history as JSON in loyalty.redemption_history metafield (array of {date, points_spent, code}); query and aggregate in the dashboard._

---

# WooCommerce to Shopify Plus Migration

**Canonical URL:** https://www.tomsailors.com/b/woocommerce-to-shopify-plus-migration  
**Published:** 2026-05-21

## Problem

A mid-market DTC merchant with eight years of operational history on WooCommerce needed to migrate to Shopify Plus without losing SEO equity, breaking legacy URLs, or severing access to historical order records. The challenge combined technical scope—preserving 301 redirect chains at scale, importing orders with original timestamps, and maintaining inventory accuracy—with the business risk of downtime and organic traffic loss during cutover.

## Sketch

I'd approach this as a four-phase build: pull the product catalog and inventory via WooCommerce's REST API into Shopify using the Admin GraphQL, store the original WC product IDs as metafield references for traceability; build a URL router that intercepts old paths and serves proper 301 responses to preserve SEO signals; archive the full order history in a queryable Postgres schema outside Shopify (the Admin API has no archive layer); and run an automated pre-launch crawl that validates every redirect and flags breaks before you flip DNS. The merchant gets a clean inventory handoff, zero broken links, and a searchable record of every legacy transaction.

## Four pieces

### Piece 1. Product & Inventory Import

**Category:** Migration  
**Stack:** Custom Shopify app + Heroku backend

Pulls your product catalog, SKUs, pricing, inventory, and custom fields from the legacy platform into Shopify Plus, storing the original product IDs as reference data so you can trace any discrepancies later.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — bulk product + metafield import
query GetProductsForImport($first: Int!) {
  products(first: $first) {
    edges {
      node {
        id
        title
        handle
        productType
        vendor
        variants(first: 100) {
          edges {
            node {
              id
              sku
              barcode
              price
              inventoryQuantity
            }
          }
        }
        metafields(first: 100, namespace: "wc_legacy") {
          edges {
            node {
              id
              namespace
              key
              value
            }
          }
        }
      }
    }
    pageInfo { hasNextPage endCursor }
  }
}

mutation ImportProduct($input: ProductInput!) {
  productCreate(input: $input) {
    product {
      id
      title
      handle
    }
    userErrors { field message }
  }
}
```

_Pair with a CSV or JSON export from the legacy REST API; use metafieldsSet to store original product IDs under a custom namespace for traceability._

### Piece 2. URL Redirect Router

**Category:** Migration  
**Stack:** Shopify Plus theme extension + Heroku redirect service

Maps every product, category, and page URL from your legacy platform to its new Shopify equivalent, serving 301 redirects that search engines recognize and preserving all backlink authority.

Paste target: `theme/snippets/legacy-redirect-check.liquid`

```liquid
{% comment %}
  Legacy URL redirect checker — intercept old URLs and 301 them to new Shopify ones.
  Store redirect map in a JSON metafield on the store's settings.
{% endcomment %}

{% assign legacy_redirects = shop.metafields.migrations.url_map | parse_json %}
{% assign current_path = request.path | downcase %}

{% for mapping in legacy_redirects %}
  {% if current_path == mapping.old_path %}
    {% comment %} Match found; render 301 redirect {% endcomment %}
    <meta http-equiv="refresh" content="0; url={{ mapping.new_url }}" />
    <script>
      if (navigator.userAgent.indexOf('Googlebot') !== -1 || navigator.userAgent.indexOf('bingbot') !== -1) {
        window.location.href = '{{ mapping.new_url }}';
      }
    </script>
    {% break %}
  {% endif %}
{% endfor %}
```

_For true HTTP 301 responses (not meta-refresh), use a Heroku middleware that intercepts requests and returns the proper status code; upload the redirect map as a Shopify metafield CSV._

### Piece 3. Historical Order Archive

**Category:** Operations  
**Stack:** Postgres database + custom admin dashboard

Stores your complete order history in a secure, queryable archive with original timestamps and customer data intact, accessible to your team without cluttering Shopify's native order interface.

### Piece 4. Pre-Launch SEO Audit

**Category:** Migration  
**Stack:** Crawl service + Heroku validation job

Runs an automated crawl of your live legacy site, builds the redirect map, tests every 301 response, and flags broken or missing mappings before you cut over DNS.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Manual workflow start or scheduled daily at 2 AM

Condition: Check if "migration_stage" metafield equals "pre_launch"

Actions:
  1. Send HTTP request to Heroku crawl service:
     POST /crawl/validate-redirects
     Body: { legacy_domain: "original-site.com", shopify_domain: "new-site.myshopify.com" }
  2. Wait for response; parse redirect_success_count and redirect_fail_urls
  3. If redirect_fail_urls.length > 0:
     → Slack notification: "Redirect audit failed on {{redirect_fail_urls.count}} URLs — review dashboard"
  4. If all pass:
     → Email store owner: "Pre-launch audit passed. Safe to flip DNS."
```

---

# US & Canada Split with Unified Customers

**Canonical URL:** https://www.tomsailors.com/b/us-canada-split-with-unified-customers  
**Published:** 2026-05-21

## Problem

A mid-market DTC with a customer base spanning both the US and Canada needed to split into two regional stores while keeping customer records in sync, maintaining inventory parity across locations, and routing customers to the correct store at checkout. The challenge was to avoid duplicate customer accounts, overselling across regions, and fragmented order records.

## Sketch

I'd build this as a multi-store sync problem, not a single-store problem. The merchant keeps two Shopify stores (one US, one CA) running in parallel, but stitches them together at three critical moments: on customer login, on inventory adjustment, and at checkout. A background job mirrors new signups and addresses across stores using metafields as link keys. A central inventory sync service pushes the same stock counts to both regions on schedule. A checkout redirect catches customers in the wrong store based on their shipping address and sends them home. All orders flow to one central log via Shopify Flow so fulfillment and email see one unified order stream.

## Four pieces

### Piece 1. Unified Customer Sync

**Category:** B2B / Wholesale  
**Stack:** Custom Shopify app + Storefront API

Keeps one customer record synced across both US and Canadian stores so they stay logged in and their order history is visible everywhere.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL - Sync customer to secondary store
mutation SyncCustomerAcrossStores($input: CustomerInput!) {
  customerUpdate(input: $input) {
    customer {
      id
      email
      firstName
      lastName
      defaultAddress {
        countryCode
        provinceCode
      }
      metafield(namespace: "sync", key: "paired_store_id") {
        value
      }
    }
    userErrors {
      field
      message
    }
  }
}

# Use this mutation to apply the same email/name to the paired store
# and set a metafield linking them
# Variables:
# {
#   "input": {
#     "id": "gid://shopify/Customer/123456",
#     "metafields": [
#       {
#         "namespace": "sync",
#         "key": "paired_store_id",
#         "type": "single_line_text",
#         "value": "us-store-id"
#       }
#     ]
#   }
# }
```

_Run this on both stores to link paired accounts; a background job watches for new signups and auto-mirrors them to the secondary store._

### Piece 2. Shared Inventory Bridge

**Category:** Inventory  
**Stack:** Heroku backend + inventory sync service

Reads stock from one central source and pushes the same quantities to both stores so products never oversell across regions.

Paste target: `Admin GraphQL explorer (run once per store location)`

```graphql
# Admin GraphQL - Adjust inventory on both stores after central sync
mutation AdjustInventoryBothStores($input: InventoryAdjustQuantitiesInput!) {
  inventoryAdjustQuantities(input: $input) {
    inventoryAdjustmentGroup {
      id
      reason
      changes {
        delta
        quantityAfterChange
        item {
          id
          sku
        }
        location {
          id
          name
        }
      }
    }
    userErrors {
      field
      message
    }
  }
}

# Call this mutation twice — once for US store locations, once for Canadian
# Variables example for US location:
# {
#   "input": {
#     "name": "Sync from warehouse",
#     "reason": "central_stock_source",
#     "changes": [
#       {
#         "inventoryItemId": "gid://shopify/InventoryItem/456",
#         "locationId": "gid://shopify/Location/us-warehouse",
#         "delta": 42
#       }
#     ]
#   }
# }
```

_The backend pulls from your warehouse system on schedule and calls this twice to sync US and CA locations in lockstep._

### Piece 3. Checkout Region Router

**Category:** Storefront  
**Stack:** Theme app extension + Checkout UI

Detects a customer's shipping address at checkout and automatically redirects them to the correct store if they're in the wrong one.

Paste target: `theme/snippets/region-router.liquid, then include from theme.liquid`

```liquid
{% comment %}
  Place in theme/snippets/region-router.liquid
  Called from checkout.liquid or cart drawer
{% endcomment %}

{% if customer %}
  {% assign shipping_country = customer.default_address.country %}
  {% assign current_store_region = shop.metafields.region.code | default: 'us' %}
  
  {% if shipping_country == 'US' and current_store_region == 'ca' %}
    <div class="region-notice">
      <p>You're shipping to the US. Redirecting you to our US store...</p>
      <script>
        window.location.href = 'https://us-store.myshopify.com' + window.location.pathname;
      </script>
    </div>
  {% elsif shipping_country == 'CA' and current_store_region == 'us' %}
    <div class="region-notice">
      <p>You're shipping to Canada. Redirecting you to our Canadian store...</p>
      <script>
        window.location.href = 'https://ca-store.myshopify.com' + window.location.pathname;
      </script>
    </div>
  {% endif %}
{% endif %}
```

_Set shop metafield 'region.code' to 'us' or 'ca' on each store so the snippet knows which store it is._

### Piece 4. Post-Purchase Event Hub

**Category:** Operations  
**Stack:** Shopify Flow + webhook dispatcher

Sends every order from both stores to a central log so email, fulfillment, and BI tools see one unified order stream.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Order Created (fires on both US and CA stores)

Condition: Always (no filter needed)

Actions:
  1. Send HTTP POST to central event hub
     URL: https://your-backend.herokuapp.com/orders/dispatch
     Body: Include order.id, order.customer.id, order.name, order.billing_address.country_code
     
  2. Send to Klaviyo (or your email platform)
     Event: "order_placed"
     Properties: order number, customer email, store region
     
  3. Log to data warehouse (or Airtable)
     Create record with: Order ID, Store Region, Customer ID, Total

Result: Every order lands in one place regardless of which store it came from.
```

_Set up one flow on each store with the same HTTP endpoint; the backend dedupes by order ID._

---

# Shop Pay Installments: B2B and High-Value Order Control

**Canonical URL:** https://www.tomsailors.com/b/shop-pay-installments-b2b-and-high-value-order-control  
**Published:** 2026-05-21

## Problem

A mid-market merchant offering both DTC and B2B channels needed to selectively disable Shop Pay Installments at checkout: for wholesale customers (tagged as B2B) and for orders exceeding $5,000. The challenge was implementing this control reliably across the payment stack without blocking legitimate transactions.

## Sketch

I'd use a Payment Customization Function to enforce the rule at checkout, then layer in a disclosure badge so customers understand why installments aren't available. To keep the system clean, I'd add Shopify Flow to auto-tag incoming B2B customers, and finally expose a simple dashboard query so the merchant can audit their rules and catch edge cases.

## Four pieces

### Piece 1. Installments Block Function

**Category:** Checkout  
**Stack:** Shopify Function (Payment Customization)

Disables Shop Pay Installments at checkout if the customer is tagged B2B or if the cart total exceeds $5,000.

### Piece 2. Checkout Disclosure Badge

**Category:** Storefront  
**Stack:** Theme extension (Checkout UI)

Shows a small message at checkout if Shop Pay Installments is disabled, so B2B or high-value buyers understand why the option is unavailable.

Paste target: `theme/snippets/installment-disclosure.liquid`

```liquid
{% if customer and customer.tags contains 'b2b' %}
  <div style="padding: 12px; margin: 12px 0; border-left: 3px solid #999; background: #f9f9f9; font-size: 13px; color: #666;">
    <strong>B2B Account:</strong> Installment payment options are not available for wholesale orders.
  </div>
{% elsif cart.total_price > 500000 %}
  <div style="padding: 12px; margin: 12px 0; border-left: 3px solid #999; background: #f9f9f9; font-size: 13px; color: #666;">
    <strong>Large Order:</strong> Installment payment options are not available for orders over $5,000.
  </div>
{% endif %}

```

_Insert this into your checkout.liquid or payment methods section. Price is in cents (500000 = $5,000)._

### Piece 3. B2B Customer Tagger

**Category:** B2B / Wholesale  
**Stack:** Shopify Flow

Automatically tags new wholesale customers with 'b2b' when they create an account through your B2B portal or when manually marked by staff.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Customer created OR Customer updated (via B2B portal signup)
Condition: customer.note contains "wholesale" OR customer has metafield company_id (indicates B2B)
Action 1: Add tag "b2b" to customer
Action 2: Send confirmation email to merchant (optional: notify fulfillment that this is a wholesale order)
```

_Adjust the condition to match how you identify B2B customers in your store (company ID, sales channel, note field, custom metafield)._

### Piece 4. Payment Rules Dashboard

**Category:** Operations  
**Stack:** Admin dashboard (custom app or report export)

A simple reference showing all customers tagged 'b2b' and flagging recent high-value orders, so you can spot rule mismatches or edge cases.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — fetch B2B customers and their order history
query B2BCustomerReport {
  customers(first: 50, query: "tag:b2b") {
    edges {
      node {
        id
        firstName
        lastName
        email
        tags
        orders(first: 3, sortKey: CREATED_AT, reverse: true) {
          edges {
            node {
              id
              name
              totalPriceSet {
                shopMoney {
                  amount
                }
              }
              createdAt
            }
          }
        }
      }
    }
  }
}

```

_Run this to see all b2b-tagged customers and their last three orders; cross-check for any orders that should have been blocked._

---

# Blocking Mixed Subscription and One-Time Checkout

**Canonical URL:** https://www.tomsailors.com/b/blocking-mixed-subscription-and-one-time-checkout  
**Published:** 2026-05-21

## Problem

A subscription-focused DTC merchant needed to enforce a fulfillment constraint: preventing customers from mixing subscription and one-time purchase items in the same cart when the one-time subtotal exceeds $500. The merchant's logistics couldn't handle fulfilling mixed subscriptions and high-value one-time orders together.

## Sketch

I'd build a cart-validation Function that runs at checkout and blocks orders when subscription items are mixed with one-time purchases above the threshold. The merchant would tag subscription products in their metafields, and I'd pair that with a client-side banner so customers understand why checkout failed and how to fix it.

## Four pieces

### Piece 1. Cart Validation Function

**Category:** Checkout  
**Stack:** Shopify Function

Blocks checkout and shows a message if the cart contains subscription items mixed with one-time purchases totaling over $500.

Paste target: `extensions/checkout-validation/src/run.graphql`

```graphql
query Input {
  cart {
    lines {
      id
      quantity
      merchandise {
        __typename
        ... on ProductVariant {
          id
          title
          product {
            id
            title
            handle
            metafield(namespace: "custom", key: "is_subscription") {
              value
            }
          }
        }
      }
      cost {
        totalAmount {
          amount
        }
      }
      attribute(key: "subscription_frequency") {
        value
      }
    }
  }
}
```

_Metafield namespace is 'custom' and key is 'is_subscription'; set to true/false in product metafields. The attribute key 'subscription_frequency' is optional but helps identify subscription lines._

### Piece 2. Subscription Tagging Helper

**Category:** Products  
**Stack:** Theme extension

A snippet that tags each product metafield to flag whether it's a subscription item so the Function knows which to watch.

Paste target: `theme/snippets/subscription-flag.liquid`

```liquid
{% # Add this to a product page section or app block to set/review subscription flags %}
{% if product.metafields.custom.is_subscription %}
  {% assign is_sub = product.metafields.custom.is_subscription.value %}
{% else %}
  {% assign is_sub = false %}
{% endif %}

<div class="subscription-flag" style="padding: 12px; background: #f5f5f5; border-radius: 4px; margin: 16px 0;">
  <p style="margin: 0; font-size: 14px; font-weight: 500;">
    Subscription Item: 
    {% if is_sub == 'true' or is_sub == true %}
      <span style="color: #0a7d3d;">✓ Yes</span>
    {% else %}
      <span style="color: #6b7280;">No</span>
    {% endif %}
  </p>
</div>
```

_This is read-only display; to actually set the metafield, use the Admin API mutation in piece four or edit directly in admin product editor._

### Piece 3. Checkout Error Banner

**Category:** Storefront  
**Stack:** Checkout UI extension

Shows a red banner at checkout explaining the rule if the cart validation Function detects a mixed-subscription violation.

Paste target: `theme/snippets/cart-validation-banner.liquid`

```liquid
{% # Show this in your checkout or cart if mixed subscription/one-time detected %}
{% assign has_sub = false %}
{% assign has_onetime = false %}
{% assign onetime_total = 0 %}

{% for item in cart.items %}
  {% if item.properties.subscription_frequency %}
    {% assign has_sub = true %}
  {% else %}
    {% assign has_onetime = true %}
    {% assign onetime_total = onetime_total | plus: item.price | times: item.quantity %}
  {% endif %}
{% endfor %}

{% if has_sub and has_onetime and onetime_total > 50000 %}
  <div style="background: #fee2e2; border: 1px solid #fecaca; padding: 12px 16px; border-radius: 6px; margin: 16px 0;">
    <p style="margin: 0; color: #991b1b; font-weight: 500; font-size: 14px;">
      ⚠️ We can't mix subscription and one-time purchases above $500 in one order. Please split into separate carts or remove items.
    </p>
  </div>
{% endif %}
```

_This detects the condition client-side; the Function is server-side enforcement. Use together for best UX._

### Piece 4. Metafield Bulk Updater

**Category:** Admin  
**Stack:** Admin API script

A small script to mark multiple products as subscriptions at once so you don't have to edit each one manually in the product editor.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL
# Run this once per product or batch to set the subscription flag.
# Replace PRODUCT_ID with the actual product ID and set value to "true" or "false".

mutation SetSubscriptionFlag($metafields: [MetafieldsSetInput!]!) {
  metafieldsSet(metafields: $metafields) {
    metafields {
      id
      namespace
      key
      value
    }
    userErrors {
      field
      message
    }
  }
}

# Variables (paste in Variables panel):
# {
#   "metafields": [
#     {
#       "ownerId": "gid://shopify/Product/PRODUCT_ID_1",
#       "namespace": "custom",
#       "key": "is_subscription",
#       "type": "boolean",
#       "value": "true"
#     },
#     {
#       "ownerId": "gid://shopify/Product/PRODUCT_ID_2",
#       "namespace": "custom",
#       "key": "is_subscription",
#       "type": "boolean",
#       "value": "true"
#     }
#   ]
# }
```

_Replace PRODUCT_ID_1, PRODUCT_ID_2, etc. with real product IDs. You can batch up to ~25 products per call to avoid rate limits._

---

# Automatic Bulk Quantity Discount Engine

**Canonical URL:** https://www.tomsailors.com/b/automatic-bulk-quantity-discount-engine  
**Published:** 2026-05-21

## Problem

A mid-market DTC merchant sells products where bulk purchases are common and wanted a way to apply tiered discounts based on quantity without requiring customers to enter a code. The merchant needed the discount to calculate automatically at checkout and display the savings tiers to customers in real time as they adjusted quantities.

## Sketch

I'd build a Cart Transform Function that reads bulk pricing tiers stored on each product's metafield and applies the right discount instantly at checkout. On the storefront, I'd add a tier preview widget so customers see their savings threshold before they buy. The whole thing lives in product metadata — no codes, no manual intervention.

## Four pieces

### Piece 1. Bulk Tier Reader

**Category:** Cart & Checkout  
**Stack:** Shopify Function + metafield

Reads the bulk pricing tier data you've stored on each product and matches it against what's in the cart right now.

Paste target: `extensions/cart-transform/src/run.graphql`

```graphql
query Input {
  cart {
    lines {
      id
      quantity
      merchandise {
        __typename
        ... on ProductVariant {
          id
          title
          product {
            id
            title
            metafield(namespace: "bulk_pricing", key: "tiers") {
              value
            }
          }
        }
      }
    }
  }
}
```

_Store tiers as JSON in the metafield: [{"minQty": 5, "discountPercent": 10}, {"minQty": 10, "discountPercent": 15}]_

### Piece 2. Tier Discount Logic

**Category:** Cart & Checkout  
**Stack:** Function script

Calculates which tier each line qualifies for and computes the exact discount amount per item.

### Piece 3. Real-Time Cart Preview

**Category:** Storefront  
**Stack:** Theme extension + cart drawer

Shows customers the exact savings they'll get at each tier as they change the quantity in the cart or on product pages.

Paste target: `theme/snippets/bulk-tiers-preview.liquid`

```liquid
{% if product.metafields.bulk_pricing.tiers %}
  {% assign tiers = product.metafields.bulk_pricing.tiers | parse_json %}
  <div class="bulk-tiers-widget">
    <p class="rte"><strong>Buy more, save more:</strong></p>
    <ul style="list-style: none; padding: 0;">
      {% for tier in tiers %}
        <li style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
          <span>{{ tier.minQty }}+ items</span>
          <span style="font-weight: bold; color: #27ae60;">{{ tier.discountPercent }}% off</span>
        </li>
      {% endfor %}
    </ul>
  </div>
{% endif %}
```

_Render this snippet on the product page and in the cart drawer; update quantity input to re-render and show which tier the customer is about to hit._

### Piece 4. Metafield Setup & Admin Dashboard

**Category:** Operations  
**Stack:** Custom admin app

A simple form where you set bulk tiers for each product — the Function reads it automatically.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — set bulk pricing tiers on a product
mutation SetBulkTiers($ownerId: ID!, $tiers: String!) {
  metafieldsSet(metafields: [
    {
      ownerId: $ownerId
      namespace: "bulk_pricing"
      key: "tiers"
      type: "json"
      value: $tiers
    }
  ]) {
    metafields {
      id
      namespace
      key
      value
    }
    userErrors {
      field
      message
    }
  }
}

# Variables:
# {
#   "ownerId": "gid://shopify/Product/123456789",
#   "tiers": "[{\"minQty\": 3, \"discountPercent\": 8}, {\"minQty\": 10, \"discountPercent\": 15}]"
# }
```

_Build a simple React form in your admin app that collects tiers and calls this mutation for each product you want to enable bulk pricing on._

---

# Shipping Rate Failover & Alert

**Canonical URL:** https://www.tomsailors.com/b/shipping-rate-failover-alert  
**Published:** 2026-05-21

## Problem

A DTC merchant using ShipEngine for rate lookups was losing orders to checkout abandonment when carriers returned no rates for valid zip codes within their service area. The merchant needed a way to detect these rate gaps in real time, fall back to a secondary carrier, and alert operations before customers left checkout.

## Sketch

I'd build a checkout hook that catches zero-rate responses from the primary carrier, triggers an instant backup carrier lookup, and fires an alert if both fail. Then I'd surface those gaps in a dashboard so the merchant can fix service-area configs or carrier coverage before the next order hits the same zip.

## Four pieces

### Piece 1. Rate Validation Hook

**Category:** Checkout  
**Stack:** Custom checkout extension

Catches ShipEngine rate responses at checkout and flags when a carrier returns zero rates for a zip that should be in its service area.

Paste target: `extensions/delivery-customization/src/run.graphql`

```graphql
# Function input query — Delivery Customization
query Input {
  cart {
    deliveryGroups {
      id
      deliveryAddress {
        zip
        countryCode
      }
      deliveryOptions {
        handle
        title
        description
      }
    }
  }
}
```

_Removed buyerIdentity.deliveryAddress — BuyerIdentity does not expose address fields in Delivery Customization schema. Zip and country are available from cart.deliveryGroups[].deliveryAddress only._

### Piece 2. Backup Rate Engine

**Category:** Shipping  
**Stack:** Backend service + ShipEngine API calls

Automatically pulls a secondary carrier's rate for the zip when the primary carrier returns nothing, so checkout doesn't break.

### Piece 3. Dark-Zone Alert

**Category:** Operations  
**Stack:** Shopify Flow + webhook

Sends a Slack or email notification when a zip code in your service area returns zero rates from both your primary and backup carrier.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Custom webhook (backend fires when rate lookup fails for a zip)
Condition: 
  – Zip is in your approved service-area list (stored as app data)
  – Primary carrier returned no rates
  – Backup carrier also returned no rates
Action: 
  – Send Slack message to #shipping channel with zip, attempted carriers, timestamp
  – Tag the order (if created) as 'rate-gap-alert'
  – Optional: Create a Slack thread for manual override options

```

_Your backend sends the webhook; Flow route it to Slack, email, or PagerDuty depending on urgency._

### Piece 4. Failure Dashboard

**Category:** Operations  
**Stack:** Admin dashboard (custom app)

Live view of which zips are hitting rate gaps, how often, and which carrier is responsible—so you can contact ShipEngine or update service areas.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL — query your app's stored rate-failure logs
query RateGaps($first: Int!) {
  shop {
    id
    name
  }
  orders(first: $first, query: "tag:rate-gap-alert") {
    edges {
      node {
        id
        name
        shippingAddress {
          zip
          country
        }
        createdAt
        metafields(namespace: "rate_gaps", first: 10) {
          edges {
            node {
              key
              value
            }
          }
        }
      }
    }
  }
}
```

_Removed appInstallation (not exposed on Shop). Moved metafield query to orders themselves—each order can carry its own rate-gap context via metafields, which is the real pattern for per-order failure tracking._

---

# Live Shipping Rates with Graceful Failover

**Canonical URL:** https://www.tomsailors.com/b/live-shipping-rates-with-graceful-failover  
**Published:** 2026-05-21

## Problem

A mid-market Plus merchant with complex shipping needs wanted to surface live carrier rates from ShipEngine at checkout without risking cart abandonment due to slow third-party API calls. They needed the checkout to remain responsive even if the external rate engine exceeded acceptable latency.

## Sketch

I'd build this as a Shopify Function that fires the rate engines in parallel and returns whichever responds first—or falls back to Shopify's native rates if ShipEngine times out at 800ms. That way the merchant gets live carrier options when they're available, but checkout never stalls waiting for a third party.

## Four pieces

### Piece 1. Delivery Rates Function

**Category:** Shipping  
**Stack:** Shopify Function + Backend service

Captures the cart and delivery address at checkout, then decides which rate engine to use based on speed.

Paste target: `extensions/delivery-function/src/run.graphql`

```graphql
query Input {
  cart {
    lines {
      id
      quantity
      merchandise {
        __typename
        ... on ProductVariant {
          id
          product {
            id
            handle
            metafield(namespace: "shipengine", key: "carrier_code") { value }
          }
        }
      }
      cost {
        totalAmount { amount }
      }
    }
    deliveryGroups {
      deliveryAddress {
        address1
        city
        provinceCode
        zip
        countryCode
      }
    }
    buyerIdentity {
      customer { id }
    }
  }
}
```

_Changed postalCode to zip on MailingAddress type._

### Piece 2. Rate Engine Resolver

**Category:** Shipping  
**Stack:** Heroku-hosted Node backend

A backend service that receives cart and address data, fires ShipEngine and native rates in parallel, and returns whichever answers within 800ms.

### Piece 3. ShipEngine Connector

**Category:** Shipping  
**Stack:** Node.js + ShipEngine API client

Pulls live carrier rates directly from ShipEngine, formatted as Shopify delivery options for checkout.

### Piece 4. Failover & Monitoring

**Category:** Shipping  
**Stack:** Monitoring dashboard + logs

Logs timeouts and successes so you can track ShipEngine reliability and adjust the timeout if needed.

---

# Wholesale Collection Gate

**Canonical URL:** https://www.tomsailors.com/b/wholesale-collection-gate-2  
**Published:** 2026-05-21

## Problem

A wholesale-focused DTC on a standard Plus or higher plan needed to completely hide B2B collections from retail customers. Rather than redirecting or showing a generic message, non-verified accounts should encounter a 404 page, while tagged B2B accounts see the collections normally.

## Sketch

I'd build a three-layer gate: first, a Liquid tag check on the collection template itself that throws a 404 for anyone without the b2b tag trying to access restricted handles; second, hide those collections from navigation and search results for retail eyes only; third, a verification workflow in Shopify Flow to tag qualifying customers automatically or flag them for staff review. The key is that the 404 is the enforcement layer — the nav hiding is just UX polish.

## Four pieces

### Piece 1. Collection Tag Gate

**Category:** Storefront  
**Stack:** Theme collection template + Liquid

Checks if the logged-in customer has the b2b tag; if not, renders a 404 page instead of the collection.

Paste target: `theme/templates/collection.json (or theme/snippets/collection-gate.liquid called early in the template)`

```liquid
{% assign customer_is_b2b = false %}
{% if customer %}
  {% if customer.tags contains 'b2b' %}
    {% assign customer_is_b2b = true %}
  {% endif %}
{% endif %}

{% if collection.handle == 'wholesale' or collection.handle == 'b2b-only' %}
  {% unless customer_is_b2b %}
    {% assign request.page_type = 'notfound' %}
    {% render 'page-not-found' %}
  {% endunless %}
{% endif %}

{% if customer_is_b2b %}
  <!-- Render normal collection template -->
  <div class="collection">
    <h1>{{ collection.title }}</h1>
    <!-- Your existing collection content here -->
  </div>
{% endif %}
```

_Replace 'wholesale' and 'b2b-only' with your actual collection handles. The tag name 'b2b' must match the tag you assign to B2B customer records in the Shopify admin._

### Piece 2. B2B Customer Tag Sync

**Category:** B2B / Wholesale  
**Stack:** Custom Shopify app + admin dashboard

Automatically tags customers in Shopify when they sign up through your B2B company portal or are manually approved.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL
mutation TagB2BCustomer($customerId: ID!, $tags: [String!]!) {
  customerUpdate(input: { id: $customerId, tags: $tags }) {
    customer {
      id
      email
      tags
    }
    userErrors {
      field
      message
    }
  }
}

# Variables example:
# {
#   "customerId": "gid://shopify/Customer/123456",
#   "tags": ["b2b"]
# }
```

_Tags passed as an array replace existing tags entirely; to append without removing others, fetch the customer's current tags first, merge, then update._

### Piece 3. Search & Nav Blackout

**Category:** Storefront  
**Stack:** Theme snippets + Liquid conditionals

Removes wholesale collections from search results and main navigation menus for retail customers; B2B accounts see them normally.

Paste target: `theme/snippets/main-nav.liquid (or your header/navigation snippet)`

```liquid
{% assign show_wholesale_nav = false %}
{% if customer and customer.tags contains 'b2b' %}
  {% assign show_wholesale_nav = true %}
{% endif %}

<!-- In your navigation or menu loop: -->
{% for link in linklists.main-menu.links %}
  {% assign is_wholesale_collection = false %}
  {% if link.type == 'collection_link' %}
    {% if link.object.handle == 'wholesale' or link.object.handle == 'b2b-only' %}
      {% assign is_wholesale_collection = true %}
    {% endif %}
  {% endif %}
  
  {% unless is_wholesale_collection and show_wholesale_nav == false %}
    <li><a href="{{ link.url }}">{{ link.title }}</a></li>
  {% endunless %}
{% endfor %}
```

_Adjust the collection handle list to match your wholesale collections. This hides links but does not prevent direct URL access — rely on the Collection Tag Gate for that enforcement._

### Piece 4. B2B Verification Workflow

**Category:** Operations  
**Stack:** Shopify Flow + admin custom columns

A manual or automated process to review and tag new accounts as B2B within the Shopify admin, so they unlock collection access immediately.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Customer created or updated (by staff in admin)
Condition: Customer.email contains specific domain (e.g., @yourb2bpartner.com) OR customer added to a specific group/tag
Then: Tag customer with "b2b"
Alternative: Trigger a Shopify admin notification for manual review → Staff reviews → Tags customer in admin directly

For semi-automated: Trigger on order > X value from new customer → Assign "b2b" tag → Send approval email to staff.
```

_Pure automation (domain-based tagging) is fast but may mis-tag. Manual review is slower but safer. Hybrid (threshold-based notification) balances both._

---

# Wholesale Portal & Contract Pricing

**Canonical URL:** https://www.tomsailors.com/b/wholesale-portal-contract-pricing  
**Published:** 2026-05-21

## Problem

A mid-market wholesale distributor needs to serve multiple buyer tiers, each with negotiated per-product pricing, without exposing those prices to other customers or modifying their public catalog. Their buyers need a gated portal to browse at contract rates and submit purchase orders, with an approval workflow so the team can review, adjust, and confirm orders before conversion.

## Sketch

I'd build this in layers: first, a B2B Companies structure in Shopify where each wholesale buyer is tagged with their tier and company ID. Then a Shopify Function that reads that company ID from the customer metafield and applies the right contract price at cart time—no public catalog changes. The portal itself is a Remix app that gates access by company ID and swaps "Add to Cart" for "Add to PO," landing everything in draft orders. Finally, Shopify Flow watches for new draft orders, notifies the approval team, and lets them review and convert without the buyer having to wait in a public checkout.

## Four pieces

### Piece 1. Company Tier & Pricing Setup

**Category:** B2B / Wholesale  
**Stack:** Admin dashboard + metafields

Create wholesale companies in Shopify, assign them to tiers (tier 1, tier 2, etc), and attach contract-specific prices per product without changing your public catalog.

### Piece 2. Contract Price Override

**Category:** B2B / Wholesale  
**Stack:** Metafields + Shopify Functions

Store per-company product pricing on each company as metafields, so one buyer sees $50 while another sees $45 for the same SKU.

Paste target: `extensions/discount-or-cart-transform/src/run.graphql`

```graphql
# Function input query — Cart Transform or Discount Function
query Input {
  cart {
    lines {
      id
      quantity
      merchandise {
        __typename
        ... on ProductVariant {
          id
          sku
          product {
            id
            handle
          }
        }
      }
      cost {
        amountPerQuantity { amount }
        totalAmount { amount }
      }
    }
    buyerIdentity {
      customer {
        id
        metafield(namespace: "wholesale", key: "company_id") { value }
      }
    }
  }
}

# In your function logic:
# 1. Read cart.buyerIdentity.customer.metafield("wholesale", "company_id")
# 2. Fetch that company's contract prices from Shopify Admin
# 3. For each line, if a contract price exists, apply it via Cart Transform or Discount
```

_Requires Shopify Plus for Cart Transform Function; Discount Function available on all plans._

### Piece 3. Wholesale Buyer Portal

**Category:** B2B / Wholesale  
**Stack:** Custom Shopify app (Remix) + theme gating

Private storefront where only authenticated wholesale customers land, browse products at their contract prices, and submit orders as POs instead of completing checkout.

Paste target: `theme/snippets/wholesale-gate.liquid`

```liquid
{% comment %}
Place this at the top of your product/collection/cart pages.
It redirects non-wholesale customers to public storefront.
{% endcomment %}

{% if customer and customer.metafields.wholesale.company_id %}
  {% comment %} Wholesale customer — show contract price {% endcomment %}
  {% assign contract_price = customer.metafields.wholesale.product_price %}
  {% if contract_price %}
    <div style="background: #f0f0f0; padding: 1rem; margin-bottom: 1rem;">
      <strong>Contract Price:</strong> {{ contract_price | money }}
    </div>
  {% endif %}
  
  {% comment %} Show PO submission form button instead of "Add to Cart" {% endcomment %}
  <form method="post" action="/apps/wholesale-portal/submit-po">
    <input type="hidden" name="product_id" value="{{ product.id }}" />
    <input type="hidden" name="variant_id" value="{{ variant.id }}" />
    <label for="qty">Quantity:</label>
    <input type="number" id="qty" name="quantity" value="1" min="1" />
    <button type="submit">Add to Purchase Order</button>
  </form>

{% else %}
  {% comment %} Not a wholesale customer — redirect {% endcomment %}
  <script>
    window.location.href = "{{ shop.secure_url }}";
  </script>
{% endif %}
```

_Customize the redirect URL and styling to match your brand._

### Piece 4. Draft Order Approval Queue

**Category:** Operations  
**Stack:** Shopify Flow + admin dashboard

Every PO submission creates a draft order tagged for review; your team approves, adjusts line items or pricing, then converts it to a real order—buyer sees the status update in their portal.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: When a draft order is created

Condition: draft order has tag "wholesale_pending_approval"

Actions:
  1. Send Slack/email notification to #wholesale-team or approval-email@company.com
     Message: "New PO from [company name]: [order total]. Review in admin."
  2. Tag draft order with "wholesale_pending_approval"
  3. (Optional) Create a task or alert in your ticketing system

— 

Second Flow (on approval):

Trigger: When a draft order is completed

Condition: draft order tag contains "wholesale_approved"

Actions:
  1. Send email to customer with order confirmation and estimated ship date
  2. Tag the resulting order with "wholesale_order"
  3. (Optional) Log to a webhook to sync fulfillment to your warehouse system
```

_Draft order creation can be done via the portal app or Admin API; Flow listens for the tag change._

---

# B2B Net 30 Checkout Gate

**Canonical URL:** https://www.tomsailors.com/b/b2b-net-30-checkout-gate  
**Published:** 2026-05-21

## Problem

A B2B merchant on Plus needed to surface net 30 payment terms at checkout, but only for customers who had already been credit-approved through their internal process. Approved customers should complete checkout without entering payment details. The build had to route approved orders to an accounting system for manual invoice generation.

## Sketch

I'd use a customer metafield to flag credit-approved B2B accounts, then hide the payment step in Checkout Extensions for those customers. The storefront would display net 30 terms prominently, and Shopify Flow would catch each approved order and route it to the accounting backend with a tag for easy filtering.

## Four pieces

### Piece 1. Approval Metafield

**Category:** B2B / Wholesale  
**Stack:** Admin API + Shopify admin UI

Adds a credit-approved flag and terms date to each B2B customer so checkout knows who qualifies for net 30.

Paste target: `Admin GraphQL explorer`

```graphql
# Admin GraphQL
mutation SetApprovalStatus($metafields: [MetafieldsSetInput!]!) {
  metafieldsSet(metafields: $metafields) {
    metafields {
      id
      namespace
      key
      value
    }
    userErrors {
      field
      message
    }
  }
}

# Variables:
# {
#   "metafields": [
#     {
#       "ownerId": "gid://shopify/Customer/1234567",
#       "namespace": "b2b_credit",
#       "key": "approved",
#       "type": "boolean",
#       "value": "true"
#     },
#     {
#       "ownerId": "gid://shopify/Customer/1234567",
#       "namespace": "b2b_credit",
#       "key": "approval_date",
#       "type": "date",
#       "value": "2025-01-15"
#     }
#   ]
# }
```

_Replace Customer ID with the B2B account you are approving. Rerun to update status._

### Piece 2. Checkout Payment Gate

**Category:** B2B / Wholesale  
**Stack:** Checkout UI extension

Hides the payment step for approved customers, letting them proceed to order confirmation with net 30 terms.

Paste target: `extensions/payment-customization/src/run.graphql`

```graphql
# Function input query — Payment Customization
query Input {
  cart {
    buyerIdentity {
      customer {
        id
        metafield(namespace: "b2b_credit", key: "approved") {
          value
        }
      }
    }
  }
}
```

_Requires Shopify Plus Checkout Extensions. The function returns the approval flag; your handler hides payment UI if true._

### Piece 3. Net 30 Terms Display

**Category:** Storefront  
**Stack:** Theme snippet + section

Shows net 30 payment terms and no-payment-required messaging on cart and checkout pages for approved B2B accounts.

Paste target: `theme/snippets/b2b-net30-terms.liquid, included in cart/checkout template`

```liquid
{% if customer and customer.metafields.b2b_credit.approved.value == true %}
  <div class="b2b-net30-banner">
    <p>
      <strong>Net 30 Terms Applied</strong><br>
      Your account is credit-approved. No payment required at checkout.
      Invoice will be sent to {{ customer.email }} after fulfillment.
    </p>
  </div>
{% endif %}
```

_Include this snippet in your cart page and checkout page templates. Style the banner to match your site._

### Piece 4. Approved Order Router

**Category:** Operations  
**Stack:** Shopify Flow + backend webhook

Watches for orders from approved customers and routes them to your accounting system, tagged for manual invoice generation.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: When an order is created
Condition:
  - Customer metafield 'b2b_credit.approved' equals true
Then:
  1. Add tag 'b2b_net30_no_payment' to order
  2. Send to Zapier or Make webhook:
     - Payload: order ID, customer email, billing/shipping address, line items, total
  3. Send notification to accounting@company.com with order details link
```

_Adjust the webhook URL to your accounting system (QuickBooks, NetSuite, etc). Tag ensures you can filter approved orders in your admin._

---

# Recharge to Shopify Subscriptions Migration

**Canonical URL:** https://www.tomsailors.com/b/recharge-to-shopify-subscriptions-migration  
**Published:** 2026-05-21

## Problem

A subscription-focused DTC merchant was migrating from Recharge to native Shopify Subscriptions and needed to preserve billing dates, trial state, and customer segmentation across 4,000 active subscribers without disrupting their next scheduled charge.

## Sketch

I'd build this in four steps: export the active Recharge subscriptions with their billing dates and trial state, recreate each subscription in Shopify with exact timing preserved, track which customers migrated cleanly and flag failures, then re-route the customer into the right Klaviyo segments so their renewal notifications stay in sync with the new system.

## Four pieces

### Piece 1. Recharge Data Export

**Category:** Migration  
**Stack:** Recharge API + backend service

Exports active Recharge subscriptions with billing dates, trial state, and customer ID in a format the developer can load into the new system.

### Piece 2. Subscription Creator

**Category:** Subscriptions  
**Stack:** Admin API mutation + backend service

Creates a native Shopify Subscription for each migrated subscriber, preserving their billing date and trial end date so the next charge fires on time.

### Piece 3. Migration Status Tracker

**Category:** Operations  
**Stack:** Backend service + admin dashboard

Logs every subscription creation, flags failures or mismatches, and gives you a live dashboard so you know which customers migrated cleanly and which need manual follow-up.

Paste target: `Admin GraphQL explorer`

```graphql
query VerifyMigration($customerId: ID!) {
  customer(id: $customerId) {
    id
    email
    subscriptionContracts(first: 10) {
      edges {
        node {
          id
          status
          nextBillingDate
          createdAt
          lines(first: 5) {
            edges {
              node {
                id
                title
              }
            }
          }
        }
      }
    }
  }
}

Variables:
{
  "customerId": "gid://shopify/Customer/12345"
}
```

_Removed sortKey argument (not supported on subscriptionContracts connection) and trialEndsAt field (doesn't exist on SubscriptionContract). Kept the core query shape: customer, subscriptionContracts, status, nextBillingDate, createdAt, and line items for audit._

### Piece 4. Klaviyo Flow Bridge

**Category:** Customer Service  
**Stack:** Klaviyo API + Flow trigger

Re-enrolls each migrated subscriber into the correct Klaviyo segment and re-triggers subscription renewal flows so notifications stay in sync with Shopify Subscriptions.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Shopify subscription created
Condition: customer.tags contains 'Recharge:Migrated'
Action 1: Add profile to Klaviyo list: "Shopify Subscriptions"
Action 2: Set profile property subscription_platform = "native"
Action 3: Add profile to Klaviyo segment: based on product and billing frequency (e.g., "Monthly Box Subscribers")
Action 4: Trigger Klaviyo flow: "Subscription Welcome (Shopify)" with customer ID + next billing date
Action 5: Remove profile from list: "Recharge Subscribers" (optional, keeps history)
```

_Tag each customer as 'Recharge:Migrated' during subscription creation so Flow can route them. Adjust segment logic based on your product bundle structure._

---

# First-Time Customer Offer Gate

**Canonical URL:** https://www.tomsailors.com/b/first-time-customer-offer-gate  
**Published:** 2026-05-21

## Problem

A DTC merchant running paid acquisition campaigns wanted to isolate a specific product offer to first-time customers only, avoiding wasted ad spend on repeat buyers. The merchant needed a way to detect purchase history at the point of landing and route traffic accordingly while tracking redemption for campaign ROI measurement.

## Sketch

I'd build this with a lightweight Liquid gate that checks `customer.orders_count` at page load—if zero or no session, show the offer; if the customer has prior orders, redirect them to the main shop. Then layer in a Shopify Flow automation to tag first-time redeemers the moment their order posts, and configure GTM + GA4 to exclude returning customers from the campaign audience so the PPC budget stays on cold traffic.

## Four pieces

### Piece 1. New Customer Detector

**Category:** Storefront  
**Stack:** Theme app extension

Checks if the logged-in customer has ever placed an order; if not, shows the offer—if yes, redirects to your main shop.

Paste target: `theme/snippets/first-time-gate.liquid`

```liquid
{% if customer %}
  {% if customer.orders_count == 0 %}
    <!-- New customer: show the offer page -->
    <div class="offer-container">
      <h1>Exclusive First-Time Offer</h1>
      <!-- Your landing page content here -->
    </div>
  {% else %}
    <!-- Returning customer: redirect -->
    <script>
      window.location.href = '/';
    </script>
  {% endif %}
{% else %}
  <!-- Not logged in: show offer (they may buy as guest or log in) -->
  <div class="offer-container">
    <h1>Exclusive First-Time Offer</h1>
  </div>
{% endif %}
```

_Place this snippet in the landing page template; it runs on every load._

### Piece 2. Offer Landing Page

**Category:** Storefront  
**Stack:** Liquid theme section

A custom page template with the product, copy, and cart button designed to convert first-time buyers.

Paste target: `theme/sections/offer-landing.liquid`

```liquid
{% assign product = all_products['your-product-handle'] %}
<section class="offer-hero">
  <div class="offer-content">
    <h1>{{ product.title }}</h1>
    <p class="offer-badge">First-Time Customer Exclusive</p>
    <p>{{ product.description }}</p>
    <div class="offer-price">
      {% if product.selected_or_first_available_variant.compare_at_price %}
        <span class="original">{{ product.selected_or_first_available_variant.compare_at_price | money }}</span>
      {% endif %}
      <span class="sale-price">{{ product.selected_or_first_available_variant.price | money }}</span>
    </div>
    <form method="post" action="/cart/add">
      <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
      <input type="number" name="quantity" value="1" min="1">
      <button type="submit" class="btn btn-primary">Add to Cart</button>
    </form>
  </div>
</section>
```

_Replace 'your-product-handle' with your actual product handle._

### Piece 3. Offer Redemption Tag

**Category:** Operations  
**Stack:** Shopify Flow

Automatically tags a customer the moment they complete their first purchase from this campaign, enabling tracking of campaign performance and prevention of double-offers.

Paste target: `Shopify Flow editor: When → Then`

```text
Trigger: Order created
Condition: Customer's order count equals 1 (first order)
Then: Tag customer with "first-time-offer-redeemed"
Then: Add customer to Shopify segment "First-Time Offer Customers"
Then: Send notification (optional): "Congratulate them via email using Klaviyo or your ESP"
```

_Enables you to track campaign performance and prevent double-offers._

### Piece 4. PPC Pixel & Redirect

**Category:** Operations  
**Stack:** Theme script + GTM

Fires a custom event when a first-time visitor arrives, and sends repeat customers back to your homepage so they don't waste PPC impressions.