{
  "site": {
    "name": "Tom Sailors",
    "url": "https://www.tomsailors.com",
    "description": "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.",
    "author": {
      "name": "Tom Sailors",
      "url": "https://www.tomsailors.com",
      "sameAs": [
        "https://github.com/tomsailors",
        "https://www.linkedin.com/in/tomsailors",
        "https://x.com/sailorstom"
      ],
      "knowsAbout": [
        "Shopify",
        "Shopify Plus",
        "Liquid",
        "Shopify Functions",
        "Shopify B2B",
        "Shopify Subscriptions",
        "Admin GraphQL API",
        "Storefront API",
        "Shopify Flow",
        "theme app extensions",
        "ShipEngine",
        "Klaviyo",
        "Recharge"
      ]
    },
    "generatedAt": "2026-05-26T21:20:05.501Z"
  },
  "briefs": [
    {
      "url": "https://www.tomsailors.com/b/legacy-customer-import-with-loyalty-preservation",
      "slug": "legacy-customer-import-with-loyalty-preservation",
      "title": "Legacy Customer Import with Loyalty Preservation",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:37:44.523Z",
      "updatedAt": "2026-05-21T14:37:44.523Z",
      "pieces": [
        {
          "position": 1,
          "name": "Data Mapper & Validator",
          "category": "Migration",
          "description": "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.",
          "stack": "Admin dashboard + validation service",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — test customer creation + metafield attachment\nmutation CreateCustomerWithLoyalty($input: CustomerInput!) {\n  customerCreate(input: $input) {\n    customer {\n      id\n      email\n      firstName\n      lastName\n      tags\n      metafields(first: 10) { edges { node { namespace key value } } }\n    }\n    userErrors { field message }\n  }\n}\n\n# Variables example:\n# {\n#   \"input\": {\n#     \"email\": \"customer@example.com\",\n#     \"firstName\": \"Jane\",\n#     \"lastName\": \"Smith\",\n#     \"tags\": [\"gold-member\", \"high-value\"]\n#   }\n# }",
            "note": "Changed CustomerCreateInput to CustomerInput per Admin API schema."
          }
        },
        {
          "position": 2,
          "name": "Bulk Import Job",
          "category": "Migration",
          "description": "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.",
          "stack": "Backend service + job queue",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — batch upsert customers with metafields\nmutation UpsertCustomerWithMetafields($input: CustomerInput!, $metafields: [MetafieldsSetInput!]!) {\n  customerCreate(input: $input) {\n    customer { id email }\n    userErrors { field message }\n  }\n  metafieldsSet(metafields: $metafields) {\n    metafields { id namespace key value }\n    userErrors { field message }\n  }\n}\n\n# Variables example (one customer with loyalty data):\n# {\n#   \"input\": {\n#     \"email\": \"customer@example.com\",\n#     \"firstName\": \"Jane\",\n#     \"lastName\": \"Doe\"\n#   },\n#   \"metafields\": [\n#     {\n#       \"ownerId\": \"gid://shopify/Customer/12345\",\n#       \"namespace\": \"loyalty\",\n#       \"key\": \"lifetime_value\",\n#       \"type\": \"decimal\",\n#       \"value\": \"2450.00\"\n#     }\n#   ]\n# }",
            "note": "Fixed: customerCreate takes single CustomerInput, not array. For batch processing, loop this mutation per customer in backend job queue."
          }
        },
        {
          "position": 3,
          "name": "Loyalty Points Transfer",
          "category": "Operations",
          "description": "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.",
          "stack": "Integration service + app API",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — store loyalty balance as metafield for retrieval later\nmutation SetLoyaltyBalance($input: MetafieldsSetInput!) {\n  metafieldsSet(metafields: [$input]) {\n    metafields {\n      id\n      namespace\n      key\n      value\n    }\n    userErrors { field message }\n  }\n}\n\n# Variables:\n# {\n#   \"input\": {\n#     \"ownerId\": \"gid://shopify/Customer/12345\",\n#     \"namespace\": \"loyalty\",\n#     \"key\": \"points_balance\",\n#     \"type\": \"integer\",\n#     \"value\": \"1250\"\n#   }\n# }",
            "note": "If using Smile or Swell, replace with their respective APIs; this stores raw balance in Shopify for now."
          }
        },
        {
          "position": 4,
          "name": "Post-Import Audit Dashboard",
          "category": "Operations",
          "description": "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.",
          "stack": "Custom admin dashboard",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — count imported customers and fetch sample metafields\nquery AuditImport {\n  customers(first: 250, query: \"created:>2024-01-01\") {\n    edges {\n      node {\n        id\n        email\n        tags\n        createdAt\n        metafields(first: 10) {\n          edges {\n            node {\n              namespace\n              key\n              value\n            }\n          }\n        }\n      }\n    }\n    pageInfo { hasNextPage endCursor }\n  }\n  shop { name }\n}\n\n# Run this query to verify customer count, tags applied, and loyalty metafields synced.",
            "note": "Paginate with cursor to scan all 50K records; build a summary report offline."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/carrier-coverage-gap-delivery-filter",
      "slug": "carrier-coverage-gap-delivery-filter",
      "title": "Carrier Coverage Gap Delivery Filter",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:34:06.453Z",
      "updatedAt": "2026-05-21T14:34:06.453Z",
      "pieces": [
        {
          "position": 1,
          "name": "Zip Code Blocklist",
          "category": "Shipping",
          "description": "A private list of zip codes where the carrier doesn't actually deliver Saturday, so the Function knows which addresses to filter.",
          "stack": "Shopify metafield + custom app",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL\nmutation SetWeekendBlocklist($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields {\n      id\n      namespace\n      key\n      value\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Variables:\n# {\n#   \"metafields\": [\n#     {\n#       \"ownerId\": \"gid://shopify/Shop/[YOUR_SHOP_ID]\",\n#       \"namespace\": \"delivery_config\",\n#       \"key\": \"weekend_blocklist_zips\",\n#       \"type\": \"json\",\n#       \"value\": \"[\\\"90210\\\",\\\"60601\\\",\\\"10001\\\"]\"\n#     }\n#   ]\n# }",
            "note": "Replace YOUR_SHOP_ID with your shop ID and add your known problem zips as JSON strings in the value array."
          }
        },
        {
          "position": 2,
          "name": "Checkout Delivery Filter",
          "category": "Shipping",
          "description": "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.",
          "stack": "Shopify Delivery Customization Function",
          "proof": null
        },
        {
          "position": 3,
          "name": "Carrier Sync Tool",
          "category": "Shipping",
          "description": "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.",
          "stack": "Heroku-hosted sync + admin dashboard",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer or backend scheduled task",
            "code": "# Admin GraphQL — query to fetch current blocklist and update it\nquery GetBlocklist {\n  shop {\n    metafield(namespace: \"delivery_config\", key: \"weekend_blocklist_zips\") {\n      value\n    }\n  }\n}\n\nmutation UpdateBlocklist($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields {\n      id\n      value\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Your sync service calls the carrier API, parses the response,\n# compares it to shop metafield, and submits the mutation with new zips.",
            "note": "Run this on a weekly cron job to keep the blocklist fresh as carrier coverage changes."
          }
        },
        {
          "position": 4,
          "name": "Blocklist Admin Panel",
          "category": "Shipping",
          "description": "A simple dashboard where you can view, add, and remove zip codes from the Saturday blocklist without touching code.",
          "stack": "Custom Shopify app (React + Polaris)",
          "proof": null
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/bulk-packing-slip-pick-list-pdf",
      "slug": "bulk-packing-slip-pick-list-pdf",
      "title": "Bulk Packing Slip & Pick List PDF",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:32:13.679Z",
      "updatedAt": "2026-05-21T14:32:13.679Z",
      "pieces": [
        {
          "position": 1,
          "name": "Order Selection Bulk Tool",
          "category": "Operations",
          "description": "Adds a quick filter panel in the Orders admin page to tag, select, and stage batches of orders for printing without leaving Shopify.",
          "stack": "Admin UI extension, order tags",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Manual trigger in admin\nCondition: Order status is \"unfulfilled\" or \"partially fulfilled\"\nAction: Apply tag \"print-batch\" to selected orders\nAction: Send notification to staff channel (Slack or email) with count and link to order list filtered by tag",
            "note": "Flow runs on demand; a developer will wrap this in a custom admin extension for checkbox multi-select and batch tagging."
          }
        },
        {
          "position": 2,
          "name": "Packing Slip Template Builder",
          "category": "Operations",
          "description": "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.",
          "stack": "Custom Shopify app, template storage",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — fetch order details for template rendering\nquery GetOrdersForPrint($first: Int!, $query: String!) {\n  orders(first: $first, query: $query) {\n    edges {\n      node {\n        id\n        name\n        number\n        createdAt\n        shippingAddress {\n          name\n          address1\n          address2\n          city\n          provinceCode\n          countryCode\n          country\n        }\n        lineItems(first: 20) {\n          edges {\n            node {\n              id\n              title\n              quantity\n              sku\n              variantTitle\n            }\n          }\n        }\n        note\n        customAttributes {\n          key\n          value\n        }\n      }\n    }\n  }\n}",
            "note": "Replaced postalCode with countryCode on MailingAddress; postal code is returned as part of address formatting in Shopify's schema."
          }
        },
        {
          "position": 3,
          "name": "Bulk PDF Generation",
          "category": "Operations",
          "description": "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.",
          "stack": "Backend service, PDF library (e.g., PDFKit or Puppeteer)",
          "proof": null
        },
        {
          "position": 4,
          "name": "Print Queue & Status",
          "category": "Operations",
          "description": "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.",
          "stack": "Admin dashboard, order tags, status logs",
          "proof": null
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/carrier-specific-cutoff-ship-promises",
      "slug": "carrier-specific-cutoff-ship-promises",
      "title": "Carrier-Specific Cutoff Ship Promises",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:30:21.875Z",
      "updatedAt": "2026-05-21T14:30:21.875Z",
      "pieces": [
        {
          "position": 1,
          "name": "Cutoff Time Calculator",
          "category": "Shipping",
          "description": "Checks the current time against the UPS Ground cutoff, then calculates which day the order will actually ship and arrive.",
          "stack": "Backend service + Liquid snippet",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/shipping-cutoff.liquid",
            "code": "{% assign cutoff_hour = 14 %}\n{% assign cutoff_minute = 0 %}\n{% assign cutoff_tz = 'America/Chicago' %}\n\n{%- capture now_iso -%}\n  {{ 'now' | date: '%s' }}\n{%- endcapture -%}\n\n{% assign hours_until_cutoff = cutoff_hour | minus: now_iso %}\n\n{% if hours_until_cutoff > 0 %}\n  {% assign ship_day = 'today' %}\n  {% assign arrive_day = 'tomorrow' %}\n{% else %}\n  {% assign ship_day = 'tomorrow' %}\n  {% assign arrive_day = 'in 2 days' %}\n{% endif %}\n\n<div class=\"shipping-promise\">\n  <p>UPS Ground: Ships {{ ship_day }}, arrives {{ arrive_day }}</p>\n  <p class=\"cutoff-notice\">Order by {{ cutoff_hour }}:{{ cutoff_minute | append: '0' | last: 2 }} CT to ship today.</p>\n</div>",
            "note": "This snippet compares current time to 2pm CT; adapt cutoff_hour and cutoff_tz for other carriers or time zones."
          }
        },
        {
          "position": 2,
          "name": "PDP Ship Promise",
          "category": "Storefront",
          "description": "Displays the calculated ship-by date prominently on the product page so customers know exactly when they'll receive the item.",
          "stack": "Theme section + Liquid",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/sections/product-template.liquid or theme/snippets/product-shipping-badge.liquid",
            "code": "{% if product.available %}\n  {% assign cutoff_hour = 14 %}\n  {% assign now_seconds = 'now' | date: '%H' | times: 3600 %}\n  {% assign cutoff_seconds = cutoff_hour | times: 3600 %}\n  \n  {% if now_seconds < cutoff_seconds %}\n    {% assign ship_label = 'Ships today' %}\n    {% assign delivery_label = 'Arrives tomorrow' %}\n  {% else %}\n    {% assign ship_label = 'Ships tomorrow' %}\n    {% assign delivery_label = 'Arrives in 2 days' %}\n  {% endif %}\n  \n  <div class=\"product-shipping-badge\">\n    <strong>{{ ship_label }}</strong> with UPS Ground\n    <span class=\"delivery-window\">{{ delivery_label }}</span>\n  </div>\n{% endif %}",
            "note": "Place this near the price and 'Add to cart' button for maximum visibility."
          }
        },
        {
          "position": 3,
          "name": "Cart Cutoff Alert",
          "category": "Checkout",
          "description": "When a customer's cart is nearing the cutoff, shows an inline warning: order now or the ship date moves to tomorrow.",
          "stack": "Cart drawer / Liquid",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/cart-cutoff-warning.liquid (render in cart drawer or cart page)",
            "code": "{% assign cutoff_hour = 14 %}\n{% assign buffer_minutes = 15 %}\n{% assign now_hour = 'now' | date: '%H' | plus: 0 %}\n{% assign now_minute = 'now' | date: '%M' | plus: 0 %}\n{% assign cutoff_total_minutes = cutoff_hour | times: 60 %}\n{% assign now_total_minutes = now_hour | times: 60 | plus: now_minute %}\n{% assign minutes_remaining = cutoff_total_minutes | minus: now_total_minutes %}\n\n{% if minutes_remaining > 0 and minutes_remaining <= buffer_minutes %}\n  <div class=\"cart-cutoff-warning\" style=\"background: #fff3cd; padding: 12px; border-radius: 4px; margin-bottom: 16px;\">\n    <strong>⏰ Order in next {{ minutes_remaining }} minutes</strong>\n    <p>to ship today. Otherwise, ships tomorrow.</p>\n  </div>\n{% endif %}",
            "note": "Set buffer_minutes to when you need orders locked in (e.g., 15 min before 2pm CT to give you processing time)."
          }
        },
        {
          "position": 4,
          "name": "Cutoff Configuration & Monitoring",
          "category": "Operations",
          "description": "Backend dashboard where you set cutoff times per carrier, track fulfillment speed, and pause sales if you're running behind.",
          "stack": "Custom Shopify app + Heroku backend",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL – set and retrieve carrier cutoff metafields\nquery GetCarrierSettings {\n  shop {\n    metafield(namespace: \"shipping_cutoffs\", key: \"ups_ground_cutoff_hour\") {\n      value\n    }\n  }\n}\n\nmutation SetCarrierCutoff($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields {\n      id\n      namespace\n      key\n      value\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}",
            "note": "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."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/automated-order-lookup-chatbot-with-agent-escalation",
      "slug": "automated-order-lookup-chatbot-with-agent-escalation",
      "title": "Automated Order Lookup Chatbot with Agent Escalation",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:28:51.269Z",
      "updatedAt": "2026-05-21T14:28:51.269Z",
      "pieces": [
        {
          "position": 1,
          "name": "Order Data Sync",
          "category": "Customer Service",
          "description": "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.",
          "stack": "Backend service + Shopify Admin",
          "proof": null
        },
        {
          "position": 2,
          "name": "Chatbot Interface & Retrieval",
          "category": "Customer Service",
          "description": "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.",
          "stack": "Custom Shopify app + React frontend",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/support-chatbot.liquid",
            "code": "{% comment %}\n  Minimal chatbot embed snippet for storefront.\n  Paste into theme/snippets/support-chatbot.liquid and render it in your theme.\n{% endcomment %}\n\n<div id=\"support-chatbot-root\" data-shop=\"{{ shop.permanent_domain }}\" data-customer-id=\"{{ customer.id | default: 'anon' }}\"></div>\n\n<style>\n  #support-chatbot-root {\n    position: fixed;\n    bottom: 20px;\n    right: 20px;\n    width: 380px;\n    max-height: 600px;\n    border-radius: 8px;\n    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);\n    z-index: 999;\n  }\n</style>\n\n<script src=\"https://your-app-domain.com/chatbot.js\" defer></script>",
            "note": "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."
          }
        },
        {
          "position": 3,
          "name": "Klaviyo Event Trigger",
          "category": "Customer Service",
          "description": "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.",
          "stack": "Shopify Flow + Klaviyo API",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Chatbot conversation marked as resolved (webhook from your app)\n\nCondition: If escalation_flag = true, skip; otherwise proceed\n\nThen:\n  1. Call Shopify Flow HTTP action to POST to Klaviyo\n  2. Send payload:\n     - customer email\n     - conversation summary\n     - products mentioned (SKUs)\n     - order numbers referenced\n     - timestamp\n  3. Klaviyo receives event 'support_chat_resolved'\n  4. Your team sees message in Klaviyo customer profile",
            "note": "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."
          }
        },
        {
          "position": 4,
          "name": "Escalation & Agent Handoff",
          "category": "Customer Service",
          "description": "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.",
          "stack": "Custom Shopify app + Gorgias or email",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL\nmutation TagCustomerForSupport($customerId: ID!, $tags: [String!]!) {\n  customerUpdate(input: { id: $customerId, tags: $tags }) {\n    customer {\n      id\n      email\n      tags\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\nquery FetchEscalatedCustomers($query: String!) {\n  customers(first: 50, query: $query) {\n    edges {\n      node {\n        id\n        email\n        firstName\n        lastName\n        orders(first: 5) {\n          edges {\n            node {\n              id\n              name\n            }\n          }\n        }\n      }\n    }\n  }\n}",
            "note": "Changed variable from $tag to $query and used it in the customers query filter. Allows dynamic tag queries like query: \"tag:support-escalation\"."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/daily-operations-digest-email",
      "slug": "daily-operations-digest-email",
      "title": "Daily Operations Digest Email",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:27:51.049Z",
      "updatedAt": "2026-05-21T14:27:51.049Z",
      "pieces": [
        {
          "position": 1,
          "name": "Metrics Aggregator",
          "category": "Operations",
          "description": "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.",
          "stack": "Backend service + cron job",
          "proof": null
        },
        {
          "position": 2,
          "name": "Email Template Renderer",
          "category": "Operations",
          "description": "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.",
          "stack": "Email template + Handlebars",
          "proof": null
        },
        {
          "position": 3,
          "name": "Scheduled Digest Sender",
          "category": "Operations",
          "description": "Watches the calendar and sends the digest email to your team every morning at 7am, pulling fresh numbers at send time.",
          "stack": "Shopify Flow",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: \"Scheduled event\" (recurring daily at 06:50am UTC)\n\nAction 1: Run custom action → call your metrics aggregator endpoint\nAction 2: Render email template using response data\nAction 3: Send email to: you@yourdomain.com, ops@yourdomain.com, etc.\n\nCondition (optional): Skip if it's a Sunday or holiday (add logic to skip non-business days)",
            "note": "Shopify Flow's scheduled event runs on UTC; adjust the time to match your timezone. If timezone is not UTC, your backend should adjust."
          }
        },
        {
          "position": 4,
          "name": "Metrics Dashboard (Optional)",
          "category": "Operations",
          "description": "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.",
          "stack": "Custom Shopify app dashboard",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/dashboard-metrics.liquid",
            "code": "<!-- Paste into theme/snippets/dashboard-metrics.liquid -->\n<div class=\"metrics-dashboard\">\n  <h1>Today's Digest — {{ 'now' | date: '%B %d, %Y' }}</h1>\n  \n  <div class=\"metric-card sales\">\n    <h2>Sales vs. Forecast</h2>\n    <p class=\"value\">{{ page.sales_actual | money }}</p>\n    <p class=\"forecast\">Forecast: {{ page.sales_forecast | money }}</p>\n    {% if page.sales_variance > 0 %}\n      <span class=\"positive\">↑ {{ page.sales_variance | money }}</span>\n    {% elsif page.sales_variance < 0 %}\n      <span class=\"negative\">↓ {{ page.sales_variance | money }}</span>\n    {% endif %}\n  </div>\n  \n  <div class=\"metric-card shipping\">\n    <h2>Backup Rate Events</h2>\n    <p class=\"value\">{{ page.backup_events | default: 0 }} fallbacks</p>\n    <p class=\"detail\">Avg cost delta: {{ page.backup_cost_delta | money }}</p>\n  </div>\n  \n  <div class=\"metric-card support\">\n    <h2>Chat Conversations</h2>\n    <p class=\"value\">{{ page.chat_count | default: 0 }} chats</p>\n    <p class=\"detail\">Avg response time: {{ page.chat_response_time }}min</p>\n  </div>\n  \n  <div class=\"metric-card tax\">\n    <h2>Tax Reconciliation</h2>\n    <p class=\"status\">{{ page.tax_status }}</p>\n    <p class=\"variance\">Variance: {{ page.tax_variance | money }}</p>\n  </div>\n</div>",
            "note": "Requires your backend to populate page.* variables from the aggregator. This is a fallback UI for manual checks."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/autonomous-monitoring-agent-for-shopify-operations",
      "slug": "autonomous-monitoring-agent-for-shopify-operations",
      "title": "Autonomous Monitoring Agent for Shopify Operations",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:27:01.174Z",
      "updatedAt": "2026-05-21T14:27:01.174Z",
      "pieces": [
        {
          "position": 1,
          "name": "Dyno Health Monitor",
          "category": "Operations",
          "description": "Pings Heroku dynos on a regular interval, checks response times and error rates, alerts Slack if a dyno is slow or crashing.",
          "stack": "Monitoring service + Slack webhook",
          "proof": null
        },
        {
          "position": 2,
          "name": "Carrier API Uptime Check",
          "category": "Shipping",
          "description": "Tests carrier integrations (ShipEngine, FedEx, UPS) on schedule, confirms rate lookups and label generation work, flags when a carrier goes offline.",
          "stack": "Health-check worker + Slack webhook",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — test fulfillment orders to confirm carrier availability\nquery TestCarrierRates {\n  fulfillmentOrders(first: 10) {\n    edges {\n      node {\n        id\n        status\n        lineItems(first: 5) {\n          edges {\n            node {\n              id\n              lineItem {\n                quantity\n              }\n            }\n          }\n        }\n        deliveryMethod {\n          id\n        }\n      }\n    }\n  }\n}",
            "note": "Removed status argument (not supported on fulfillmentOrders); nested quantity under lineItem to access the FulfillmentLineItem parent."
          }
        },
        {
          "position": 3,
          "name": "Catalog Freshness Check",
          "category": "Inventory",
          "description": "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.",
          "stack": "Scheduled worker + Slack webhook",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — check product sync freshness\nquery CheckCatalogFreshness {\n  products(first: 1, query: \"updated_at:>2024-01-01\", sortKey: UPDATED_AT, reverse: true) {\n    edges {\n      node {\n        id\n        handle\n        updatedAt\n        metafield(namespace: \"custom\", key: \"last_sync\") {\n          value\n        }\n      }\n    }\n  }\n}\n# On the monitoring worker: compare updatedAt timestamps against current time.\n# If no products updated in the last 24h (or your interval), fire a Slack alert.",
            "note": "Set the timestamp threshold and sync window in your monitoring config; adjust 'last_sync' namespace/key to match your actual metafield."
          }
        },
        {
          "position": 4,
          "name": "Tax Sync Drift Detector",
          "category": "Operations",
          "description": "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.",
          "stack": "Drift-check worker + Slack webhook",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — sample recent orders for tax audit\nquery TaxDriftAudit {\n  orders(first: 10, query: \"created_at:>2024-01-15\", sortKey: CREATED_AT, reverse: true) {\n    edges {\n      node {\n        id\n        name\n        createdAt\n        totalTaxSet {\n          shopMoney {\n            amount\n            currencyCode\n          }\n        }\n        lineItems(first: 50) {\n          edges {\n            node {\n              id\n              sku\n              taxLines {\n                title\n                ratePercentage\n                price\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n# On the worker: fetch the same orders from TaxJar/Vertex and compare totalTaxSet amounts.",
            "note": "Removed selection set from taxLines.price—it is a scalar Money type, not an object. Price is already a decimal string."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/fraud-resistant-referral-rewards-engine",
      "slug": "fraud-resistant-referral-rewards-engine",
      "title": "Fraud-Resistant Referral Rewards Engine",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:26:25.483Z",
      "updatedAt": "2026-05-21T14:26:25.483Z",
      "pieces": [
        {
          "position": 1,
          "name": "Referral Link Generator",
          "category": "Marketing",
          "description": "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.",
          "stack": "Custom Shopify app + Shopify metafield",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL\nmutation GenerateReferralCode($customerId: ID!) {\n  metafieldsSet(metafields: [\n    {\n      ownerId: $customerId\n      namespace: \"referral\"\n      key: \"referral_code\"\n      type: \"single_line_text_field\"\n      value: \"REF_12345ABCDE\"\n    }\n  ]) {\n    metafields {\n      id\n      value\n      namespace\n      key\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\nquery GetReferralCode($customerId: ID!) {\n  customer(id: $customerId) {\n    id\n    email\n    metafield(namespace: \"referral\", key: \"referral_code\") {\n      value\n    }\n  }\n}",
            "note": "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."
          }
        },
        {
          "position": 2,
          "name": "Referee Tracker",
          "category": "Customer Verification",
          "description": "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.",
          "stack": "Shopify Flow + custom order tag",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Order is created\n\nCondition: Order source contains referral code query param (extract via Flow or store in cart attribute beforehand)\n  AND customer is placing their first order (check customer.orders.count = 1)\n  AND order contains at least one product line item (prevent gaming with empty orders)\n\nAction 1: Tag customer with \"referred-by-[referrer_code]\"\n\nAction 2: Tag customer with \"first_order_confirmed\"\n\nAction 3: Send event to custom app webhook: { referrer_code, referee_email, order_total, timestamp }\n  (Webhook payload triggers the referrer credit issuance in the next piece)",
            "note": "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)."
          }
        },
        {
          "position": 3,
          "name": "Credit Payout Gate",
          "category": "Operations",
          "description": "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.",
          "stack": "Custom Shopify app backend + Shopify Function",
          "proof": {
            "kind": "function",
            "language": "GraphQL",
            "pasteTarget": "extensions/discount/src/run.graphql",
            "code": "# Function input query — Discount Function\nquery Input {\n  cart {\n    buyerIdentity {\n      customer {\n        id\n        email\n        metafield(namespace: \"referral\", key: \"available_credit\") {\n          value\n        }\n      }\n    }\n    cost {\n      totalAmount {\n        amount\n      }\n    }\n  }\n}",
            "note": "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."
          }
        },
        {
          "position": 4,
          "name": "Referral Dashboard",
          "category": "Storefront",
          "description": "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.",
          "stack": "Theme app extension + React",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/referral-dashboard.liquid",
            "code": "{% if customer %}\n  <div class=\"referral-dashboard\">\n    <h2>Your Referral Link</h2>\n    \n    {% assign referral_code = customer.metafields.referral.referral_code %}\n    {% assign credit_balance = customer.metafields.referral.available_credit | default: 0 %}\n    {% assign referrals_count = customer.tags | where: \"referred-by-\" | size %}\n    \n    {% if referral_code %}\n      <p class=\"referral-url\">\n        {{ shop.url }}/pages/checkout?ref={{ referral_code }}\n      </p>\n      <button onclick=\"copyToClipboard(this)\">Copy Link</button>\n    {% endif %}\n    \n    <div class=\"referral-stats\">\n      <div class=\"stat\">\n        <strong>Friends Referred</strong>\n        <span>{{ referrals_count }}</span>\n      </div>\n      <div class=\"stat\">\n        <strong>Available Credit</strong>\n        <span>${{ credit_balance | default: 0 }}</span>\n      </div>\n    </div>\n  </div>\n{% else %}\n  <p>Sign in to see your referral rewards.</p>\n{% endif %}",
            "note": "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."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/custom-points-redemption-system",
      "slug": "custom-points-redemption-system",
      "title": "Custom Points & Redemption System",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:25:24.753Z",
      "updatedAt": "2026-05-21T14:25:24.753Z",
      "pieces": [
        {
          "position": 1,
          "name": "Points Accumulator",
          "category": "Loyalty",
          "description": "Records points to each customer's account whenever an order is paid, using a multiplier you choose — like 1 point per dollar spent.",
          "stack": "Custom app + order webhook",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL\nmutation SetCustomerPoints($ownerId: ID!, $points: String!) {\n  metafieldsSet(metafields: [{\n    ownerId: $ownerId\n    namespace: \"loyalty\"\n    key: \"points_balance\"\n    type: \"number_integer\"\n    value: $points\n  }]) {\n    metafields {\n      id\n      value\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Variables:\n# {\n#   \"ownerId\": \"gid://shopify/Customer/12345\",\n#   \"points\": \"150\"\n# }",
            "note": "Metafield values are always strings. Changed $points variable type from Int! to String! and adjusted the example variable from 150 to \"150\"."
          }
        },
        {
          "position": 2,
          "name": "Customer Portal",
          "category": "Loyalty",
          "description": "A dedicated page in your online store where customers log in to see their current points balance, recent earning history, and redemption history.",
          "stack": "Theme app extension + React",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/loyalty-portal.liquid",
            "code": "{% if customer %}\n  <div class=\"loyalty-portal\">\n    <h2>Your Points Balance</h2>\n    {% assign points = customer.metafields.loyalty.points_balance | default: 0 %}\n    <p class=\"balance\">{{ points }} points</p>\n    \n    <h3>Redeem</h3>\n    {% if points >= 100 %}\n      <form action=\"/apps/loyalty/redeem\" method=\"POST\">\n        <input type=\"hidden\" name=\"customer_id\" value=\"{{ customer.id }}\">\n        <label>\n          <input type=\"number\" name=\"redeem_points\" min=\"100\" max=\"{{ points }}\" value=\"100\">\n          points\n        </label>\n        <button type=\"submit\">Generate Code</button>\n      </form>\n    {% else %}\n      <p>Earn {{ 100 | minus: points }} more points to redeem.</p>\n    {% endif %}\n  </div>\n{% else %}\n  <p><a href=\"/account/login\">Log in</a> to view your points.</p>\n{% endif %}",
            "note": "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."
          }
        },
        {
          "position": 3,
          "name": "Redemption Engine",
          "category": "Loyalty",
          "description": "When a customer clicks redeem, creates a discount code pegged to their points spend, deducts the points from their account, and logs the redemption.",
          "stack": "Custom app backend + Discount API",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL\nmutation CreateDiscountCode($input: DiscountCodeAppInput!) {\n  discountCodeAppCreate(codeAppDiscount: $input) {\n    codeAppDiscount {\n      startsAt\n      endsAt\n      title\n      codes(first: 1) {\n        edges {\n          node {\n            code\n          }\n        }\n      }\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Variables:\n# {\n#   \"input\": {\n#     \"combinesWith\": {\"shippingDiscounts\": false, \"orderDiscounts\": true},\n#     \"startsAt\": \"2024-01-15T00:00:00Z\",\n#     \"endsAt\": \"2024-03-15T00:00:00Z\",\n#     \"title\": \"POINTS-REDEEM-12345\",\n#     \"codes\": [\"POINTS-REDEEM-12345\"],\n#     \"customerGets\": {\n#       \"value\": {\"amount\": \"50.00\", \"appliesOnEachItem\": false}\n#     }\n#   }\n# }",
            "note": "Fixed argument name from 'input' to 'codeAppDiscount', removed unsupported 'id' field, queried only available fields (startsAt, endsAt, title, codes)."
          }
        },
        {
          "position": 4,
          "name": "Loyalty Dashboard",
          "category": "Operations",
          "description": "Admin view showing total points issued, redemptions by customer, and top earners — helps you track loyalty health and spot trends.",
          "stack": "Custom app dashboard",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — fetch customers with points metafield\nquery GetLoyaltyMetrics($first: Int!) {\n  customers(first: $first) {\n    edges {\n      node {\n        id\n        displayName\n        email\n        numberOfOrders\n        points: metafield(namespace: \"loyalty\", key: \"points_balance\") {\n          value\n        }\n        redemptions: metafield(namespace: \"loyalty\", key: \"redemption_history\") {\n          value\n        }\n      }\n    }\n  }\n}\n\n# Variables:\n# { \"first\": 50 }\n\n# Note: redemption_history is a JSON string; parse on the app side to compute totals.",
            "note": "Store redemption history as JSON in loyalty.redemption_history metafield (array of {date, points_spent, code}); query and aggregate in the dashboard."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/woocommerce-to-shopify-plus-migration",
      "slug": "woocommerce-to-shopify-plus-migration",
      "title": "WooCommerce to Shopify Plus Migration",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:23:31.810Z",
      "updatedAt": "2026-05-21T14:23:31.810Z",
      "pieces": [
        {
          "position": 1,
          "name": "Product & Inventory Import",
          "category": "Migration",
          "description": "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.",
          "stack": "Custom Shopify app + Heroku backend",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — bulk product + metafield import\nquery GetProductsForImport($first: Int!) {\n  products(first: $first) {\n    edges {\n      node {\n        id\n        title\n        handle\n        productType\n        vendor\n        variants(first: 100) {\n          edges {\n            node {\n              id\n              sku\n              barcode\n              price\n              inventoryQuantity\n            }\n          }\n        }\n        metafields(first: 100, namespace: \"wc_legacy\") {\n          edges {\n            node {\n              id\n              namespace\n              key\n              value\n            }\n          }\n        }\n      }\n    }\n    pageInfo { hasNextPage endCursor }\n  }\n}\n\nmutation ImportProduct($input: ProductInput!) {\n  productCreate(input: $input) {\n    product {\n      id\n      title\n      handle\n    }\n    userErrors { field message }\n  }\n}",
            "note": "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."
          }
        },
        {
          "position": 2,
          "name": "URL Redirect Router",
          "category": "Migration",
          "description": "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.",
          "stack": "Shopify Plus theme extension + Heroku redirect service",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/legacy-redirect-check.liquid",
            "code": "{% comment %}\n  Legacy URL redirect checker — intercept old URLs and 301 them to new Shopify ones.\n  Store redirect map in a JSON metafield on the store's settings.\n{% endcomment %}\n\n{% assign legacy_redirects = shop.metafields.migrations.url_map | parse_json %}\n{% assign current_path = request.path | downcase %}\n\n{% for mapping in legacy_redirects %}\n  {% if current_path == mapping.old_path %}\n    {% comment %} Match found; render 301 redirect {% endcomment %}\n    <meta http-equiv=\"refresh\" content=\"0; url={{ mapping.new_url }}\" />\n    <script>\n      if (navigator.userAgent.indexOf('Googlebot') !== -1 || navigator.userAgent.indexOf('bingbot') !== -1) {\n        window.location.href = '{{ mapping.new_url }}';\n      }\n    </script>\n    {% break %}\n  {% endif %}\n{% endfor %}",
            "note": "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."
          }
        },
        {
          "position": 3,
          "name": "Historical Order Archive",
          "category": "Operations",
          "description": "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.",
          "stack": "Postgres database + custom admin dashboard",
          "proof": null
        },
        {
          "position": 4,
          "name": "Pre-Launch SEO Audit",
          "category": "Migration",
          "description": "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.",
          "stack": "Crawl service + Heroku validation job",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Manual workflow start or scheduled daily at 2 AM\n\nCondition: Check if \"migration_stage\" metafield equals \"pre_launch\"\n\nActions:\n  1. Send HTTP request to Heroku crawl service:\n     POST /crawl/validate-redirects\n     Body: { legacy_domain: \"original-site.com\", shopify_domain: \"new-site.myshopify.com\" }\n  2. Wait for response; parse redirect_success_count and redirect_fail_urls\n  3. If redirect_fail_urls.length > 0:\n     → Slack notification: \"Redirect audit failed on {{redirect_fail_urls.count}} URLs — review dashboard\"\n  4. If all pass:\n     → Email store owner: \"Pre-launch audit passed. Safe to flip DNS.\"",
            "note": ""
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/us-canada-split-with-unified-customers",
      "slug": "us-canada-split-with-unified-customers",
      "title": "US & Canada Split with Unified Customers",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:22:55.662Z",
      "updatedAt": "2026-05-21T14:22:55.662Z",
      "pieces": [
        {
          "position": 1,
          "name": "Unified Customer Sync",
          "category": "B2B / Wholesale",
          "description": "Keeps one customer record synced across both US and Canadian stores so they stay logged in and their order history is visible everywhere.",
          "stack": "Custom Shopify app + Storefront API",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL - Sync customer to secondary store\nmutation SyncCustomerAcrossStores($input: CustomerInput!) {\n  customerUpdate(input: $input) {\n    customer {\n      id\n      email\n      firstName\n      lastName\n      defaultAddress {\n        countryCode\n        provinceCode\n      }\n      metafield(namespace: \"sync\", key: \"paired_store_id\") {\n        value\n      }\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Use this mutation to apply the same email/name to the paired store\n# and set a metafield linking them\n# Variables:\n# {\n#   \"input\": {\n#     \"id\": \"gid://shopify/Customer/123456\",\n#     \"metafields\": [\n#       {\n#         \"namespace\": \"sync\",\n#         \"key\": \"paired_store_id\",\n#         \"type\": \"single_line_text\",\n#         \"value\": \"us-store-id\"\n#       }\n#     ]\n#   }\n# }",
            "note": "Run this on both stores to link paired accounts; a background job watches for new signups and auto-mirrors them to the secondary store."
          }
        },
        {
          "position": 2,
          "name": "Shared Inventory Bridge",
          "category": "Inventory",
          "description": "Reads stock from one central source and pushes the same quantities to both stores so products never oversell across regions.",
          "stack": "Heroku backend + inventory sync service",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer (run once per store location)",
            "code": "# Admin GraphQL - Adjust inventory on both stores after central sync\nmutation AdjustInventoryBothStores($input: InventoryAdjustQuantitiesInput!) {\n  inventoryAdjustQuantities(input: $input) {\n    inventoryAdjustmentGroup {\n      id\n      reason\n      changes {\n        delta\n        quantityAfterChange\n        item {\n          id\n          sku\n        }\n        location {\n          id\n          name\n        }\n      }\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Call this mutation twice — once for US store locations, once for Canadian\n# Variables example for US location:\n# {\n#   \"input\": {\n#     \"name\": \"Sync from warehouse\",\n#     \"reason\": \"central_stock_source\",\n#     \"changes\": [\n#       {\n#         \"inventoryItemId\": \"gid://shopify/InventoryItem/456\",\n#         \"locationId\": \"gid://shopify/Location/us-warehouse\",\n#         \"delta\": 42\n#       }\n#     ]\n#   }\n# }",
            "note": "The backend pulls from your warehouse system on schedule and calls this twice to sync US and CA locations in lockstep."
          }
        },
        {
          "position": 3,
          "name": "Checkout Region Router",
          "category": "Storefront",
          "description": "Detects a customer's shipping address at checkout and automatically redirects them to the correct store if they're in the wrong one.",
          "stack": "Theme app extension + Checkout UI",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/region-router.liquid, then include from theme.liquid",
            "code": "{% comment %}\n  Place in theme/snippets/region-router.liquid\n  Called from checkout.liquid or cart drawer\n{% endcomment %}\n\n{% if customer %}\n  {% assign shipping_country = customer.default_address.country %}\n  {% assign current_store_region = shop.metafields.region.code | default: 'us' %}\n  \n  {% if shipping_country == 'US' and current_store_region == 'ca' %}\n    <div class=\"region-notice\">\n      <p>You're shipping to the US. Redirecting you to our US store...</p>\n      <script>\n        window.location.href = 'https://us-store.myshopify.com' + window.location.pathname;\n      </script>\n    </div>\n  {% elsif shipping_country == 'CA' and current_store_region == 'us' %}\n    <div class=\"region-notice\">\n      <p>You're shipping to Canada. Redirecting you to our Canadian store...</p>\n      <script>\n        window.location.href = 'https://ca-store.myshopify.com' + window.location.pathname;\n      </script>\n    </div>\n  {% endif %}\n{% endif %}",
            "note": "Set shop metafield 'region.code' to 'us' or 'ca' on each store so the snippet knows which store it is."
          }
        },
        {
          "position": 4,
          "name": "Post-Purchase Event Hub",
          "category": "Operations",
          "description": "Sends every order from both stores to a central log so email, fulfillment, and BI tools see one unified order stream.",
          "stack": "Shopify Flow + webhook dispatcher",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Order Created (fires on both US and CA stores)\n\nCondition: Always (no filter needed)\n\nActions:\n  1. Send HTTP POST to central event hub\n     URL: https://your-backend.herokuapp.com/orders/dispatch\n     Body: Include order.id, order.customer.id, order.name, order.billing_address.country_code\n     \n  2. Send to Klaviyo (or your email platform)\n     Event: \"order_placed\"\n     Properties: order number, customer email, store region\n     \n  3. Log to data warehouse (or Airtable)\n     Create record with: Order ID, Store Region, Customer ID, Total\n\nResult: Every order lands in one place regardless of which store it came from.",
            "note": "Set up one flow on each store with the same HTTP endpoint; the backend dedupes by order ID."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/shop-pay-installments-b2b-and-high-value-order-control",
      "slug": "shop-pay-installments-b2b-and-high-value-order-control",
      "title": "Shop Pay Installments: B2B and High-Value Order Control",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:19:51.414Z",
      "updatedAt": "2026-05-21T14:19:51.414Z",
      "pieces": [
        {
          "position": 1,
          "name": "Installments Block Function",
          "category": "Checkout",
          "description": "Disables Shop Pay Installments at checkout if the customer is tagged B2B or if the cart total exceeds $5,000.",
          "stack": "Shopify Function (Payment Customization)",
          "proof": null
        },
        {
          "position": 2,
          "name": "Checkout Disclosure Badge",
          "category": "Storefront",
          "description": "Shows a small message at checkout if Shop Pay Installments is disabled, so B2B or high-value buyers understand why the option is unavailable.",
          "stack": "Theme extension (Checkout UI)",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/installment-disclosure.liquid",
            "code": "{% if customer and customer.tags contains 'b2b' %}\n  <div style=\"padding: 12px; margin: 12px 0; border-left: 3px solid #999; background: #f9f9f9; font-size: 13px; color: #666;\">\n    <strong>B2B Account:</strong> Installment payment options are not available for wholesale orders.\n  </div>\n{% elsif cart.total_price > 500000 %}\n  <div style=\"padding: 12px; margin: 12px 0; border-left: 3px solid #999; background: #f9f9f9; font-size: 13px; color: #666;\">\n    <strong>Large Order:</strong> Installment payment options are not available for orders over $5,000.\n  </div>\n{% endif %}\n",
            "note": "Insert this into your checkout.liquid or payment methods section. Price is in cents (500000 = $5,000)."
          }
        },
        {
          "position": 3,
          "name": "B2B Customer Tagger",
          "category": "B2B / Wholesale",
          "description": "Automatically tags new wholesale customers with 'b2b' when they create an account through your B2B portal or when manually marked by staff.",
          "stack": "Shopify Flow",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Customer created OR Customer updated (via B2B portal signup)\nCondition: customer.note contains \"wholesale\" OR customer has metafield company_id (indicates B2B)\nAction 1: Add tag \"b2b\" to customer\nAction 2: Send confirmation email to merchant (optional: notify fulfillment that this is a wholesale order)",
            "note": "Adjust the condition to match how you identify B2B customers in your store (company ID, sales channel, note field, custom metafield)."
          }
        },
        {
          "position": 4,
          "name": "Payment Rules Dashboard",
          "category": "Operations",
          "description": "A simple reference showing all customers tagged 'b2b' and flagging recent high-value orders, so you can spot rule mismatches or edge cases.",
          "stack": "Admin dashboard (custom app or report export)",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — fetch B2B customers and their order history\nquery B2BCustomerReport {\n  customers(first: 50, query: \"tag:b2b\") {\n    edges {\n      node {\n        id\n        firstName\n        lastName\n        email\n        tags\n        orders(first: 3, sortKey: CREATED_AT, reverse: true) {\n          edges {\n            node {\n              id\n              name\n              totalPriceSet {\n                shopMoney {\n                  amount\n                }\n              }\n              createdAt\n            }\n          }\n        }\n      }\n    }\n  }\n}\n",
            "note": "Run this to see all b2b-tagged customers and their last three orders; cross-check for any orders that should have been blocked."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/blocking-mixed-subscription-and-one-time-checkout",
      "slug": "blocking-mixed-subscription-and-one-time-checkout",
      "title": "Blocking Mixed Subscription and One-Time Checkout",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:19:19.958Z",
      "updatedAt": "2026-05-21T14:19:19.958Z",
      "pieces": [
        {
          "position": 1,
          "name": "Cart Validation Function",
          "category": "Checkout",
          "description": "Blocks checkout and shows a message if the cart contains subscription items mixed with one-time purchases totaling over $500.",
          "stack": "Shopify Function",
          "proof": {
            "kind": "function",
            "language": "GraphQL",
            "pasteTarget": "extensions/checkout-validation/src/run.graphql",
            "code": "query Input {\n  cart {\n    lines {\n      id\n      quantity\n      merchandise {\n        __typename\n        ... on ProductVariant {\n          id\n          title\n          product {\n            id\n            title\n            handle\n            metafield(namespace: \"custom\", key: \"is_subscription\") {\n              value\n            }\n          }\n        }\n      }\n      cost {\n        totalAmount {\n          amount\n        }\n      }\n      attribute(key: \"subscription_frequency\") {\n        value\n      }\n    }\n  }\n}",
            "note": "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."
          }
        },
        {
          "position": 2,
          "name": "Subscription Tagging Helper",
          "category": "Products",
          "description": "A snippet that tags each product metafield to flag whether it's a subscription item so the Function knows which to watch.",
          "stack": "Theme extension",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/subscription-flag.liquid",
            "code": "{% # Add this to a product page section or app block to set/review subscription flags %}\n{% if product.metafields.custom.is_subscription %}\n  {% assign is_sub = product.metafields.custom.is_subscription.value %}\n{% else %}\n  {% assign is_sub = false %}\n{% endif %}\n\n<div class=\"subscription-flag\" style=\"padding: 12px; background: #f5f5f5; border-radius: 4px; margin: 16px 0;\">\n  <p style=\"margin: 0; font-size: 14px; font-weight: 500;\">\n    Subscription Item: \n    {% if is_sub == 'true' or is_sub == true %}\n      <span style=\"color: #0a7d3d;\">✓ Yes</span>\n    {% else %}\n      <span style=\"color: #6b7280;\">No</span>\n    {% endif %}\n  </p>\n</div>",
            "note": "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."
          }
        },
        {
          "position": 3,
          "name": "Checkout Error Banner",
          "category": "Storefront",
          "description": "Shows a red banner at checkout explaining the rule if the cart validation Function detects a mixed-subscription violation.",
          "stack": "Checkout UI extension",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/cart-validation-banner.liquid",
            "code": "{% # Show this in your checkout or cart if mixed subscription/one-time detected %}\n{% assign has_sub = false %}\n{% assign has_onetime = false %}\n{% assign onetime_total = 0 %}\n\n{% for item in cart.items %}\n  {% if item.properties.subscription_frequency %}\n    {% assign has_sub = true %}\n  {% else %}\n    {% assign has_onetime = true %}\n    {% assign onetime_total = onetime_total | plus: item.price | times: item.quantity %}\n  {% endif %}\n{% endfor %}\n\n{% if has_sub and has_onetime and onetime_total > 50000 %}\n  <div style=\"background: #fee2e2; border: 1px solid #fecaca; padding: 12px 16px; border-radius: 6px; margin: 16px 0;\">\n    <p style=\"margin: 0; color: #991b1b; font-weight: 500; font-size: 14px;\">\n      ⚠️ We can't mix subscription and one-time purchases above $500 in one order. Please split into separate carts or remove items.\n    </p>\n  </div>\n{% endif %}",
            "note": "This detects the condition client-side; the Function is server-side enforcement. Use together for best UX."
          }
        },
        {
          "position": 4,
          "name": "Metafield Bulk Updater",
          "category": "Admin",
          "description": "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.",
          "stack": "Admin API script",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL\n# Run this once per product or batch to set the subscription flag.\n# Replace PRODUCT_ID with the actual product ID and set value to \"true\" or \"false\".\n\nmutation SetSubscriptionFlag($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields {\n      id\n      namespace\n      key\n      value\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Variables (paste in Variables panel):\n# {\n#   \"metafields\": [\n#     {\n#       \"ownerId\": \"gid://shopify/Product/PRODUCT_ID_1\",\n#       \"namespace\": \"custom\",\n#       \"key\": \"is_subscription\",\n#       \"type\": \"boolean\",\n#       \"value\": \"true\"\n#     },\n#     {\n#       \"ownerId\": \"gid://shopify/Product/PRODUCT_ID_2\",\n#       \"namespace\": \"custom\",\n#       \"key\": \"is_subscription\",\n#       \"type\": \"boolean\",\n#       \"value\": \"true\"\n#     }\n#   ]\n# }",
            "note": "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."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/automatic-bulk-quantity-discount-engine",
      "slug": "automatic-bulk-quantity-discount-engine",
      "title": "Automatic Bulk Quantity Discount Engine",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:18:26.606Z",
      "updatedAt": "2026-05-21T14:18:26.606Z",
      "pieces": [
        {
          "position": 1,
          "name": "Bulk Tier Reader",
          "category": "Cart & Checkout",
          "description": "Reads the bulk pricing tier data you've stored on each product and matches it against what's in the cart right now.",
          "stack": "Shopify Function + metafield",
          "proof": {
            "kind": "function",
            "language": "GraphQL",
            "pasteTarget": "extensions/cart-transform/src/run.graphql",
            "code": "query Input {\n  cart {\n    lines {\n      id\n      quantity\n      merchandise {\n        __typename\n        ... on ProductVariant {\n          id\n          title\n          product {\n            id\n            title\n            metafield(namespace: \"bulk_pricing\", key: \"tiers\") {\n              value\n            }\n          }\n        }\n      }\n    }\n  }\n}",
            "note": "Store tiers as JSON in the metafield: [{\"minQty\": 5, \"discountPercent\": 10}, {\"minQty\": 10, \"discountPercent\": 15}]"
          }
        },
        {
          "position": 2,
          "name": "Tier Discount Logic",
          "category": "Cart & Checkout",
          "description": "Calculates which tier each line qualifies for and computes the exact discount amount per item.",
          "stack": "Function script",
          "proof": null
        },
        {
          "position": 3,
          "name": "Real-Time Cart Preview",
          "category": "Storefront",
          "description": "Shows customers the exact savings they'll get at each tier as they change the quantity in the cart or on product pages.",
          "stack": "Theme extension + cart drawer",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/bulk-tiers-preview.liquid",
            "code": "{% if product.metafields.bulk_pricing.tiers %}\n  {% assign tiers = product.metafields.bulk_pricing.tiers | parse_json %}\n  <div class=\"bulk-tiers-widget\">\n    <p class=\"rte\"><strong>Buy more, save more:</strong></p>\n    <ul style=\"list-style: none; padding: 0;\">\n      {% for tier in tiers %}\n        <li style=\"display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;\">\n          <span>{{ tier.minQty }}+ items</span>\n          <span style=\"font-weight: bold; color: #27ae60;\">{{ tier.discountPercent }}% off</span>\n        </li>\n      {% endfor %}\n    </ul>\n  </div>\n{% endif %}",
            "note": "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."
          }
        },
        {
          "position": 4,
          "name": "Metafield Setup & Admin Dashboard",
          "category": "Operations",
          "description": "A simple form where you set bulk tiers for each product — the Function reads it automatically.",
          "stack": "Custom admin app",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — set bulk pricing tiers on a product\nmutation SetBulkTiers($ownerId: ID!, $tiers: String!) {\n  metafieldsSet(metafields: [\n    {\n      ownerId: $ownerId\n      namespace: \"bulk_pricing\"\n      key: \"tiers\"\n      type: \"json\"\n      value: $tiers\n    }\n  ]) {\n    metafields {\n      id\n      namespace\n      key\n      value\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Variables:\n# {\n#   \"ownerId\": \"gid://shopify/Product/123456789\",\n#   \"tiers\": \"[{\\\"minQty\\\": 3, \\\"discountPercent\\\": 8}, {\\\"minQty\\\": 10, \\\"discountPercent\\\": 15}]\"\n# }",
            "note": "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."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/shipping-rate-failover-alert",
      "slug": "shipping-rate-failover-alert",
      "title": "Shipping Rate Failover & Alert",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:16:25.501Z",
      "updatedAt": "2026-05-21T14:16:25.501Z",
      "pieces": [
        {
          "position": 1,
          "name": "Rate Validation Hook",
          "category": "Checkout",
          "description": "Catches ShipEngine rate responses at checkout and flags when a carrier returns zero rates for a zip that should be in its service area.",
          "stack": "Custom checkout extension",
          "proof": {
            "kind": "function",
            "language": "GraphQL",
            "pasteTarget": "extensions/delivery-customization/src/run.graphql",
            "code": "# Function input query — Delivery Customization\nquery Input {\n  cart {\n    deliveryGroups {\n      id\n      deliveryAddress {\n        zip\n        countryCode\n      }\n      deliveryOptions {\n        handle\n        title\n        description\n      }\n    }\n  }\n}",
            "note": "Removed buyerIdentity.deliveryAddress — BuyerIdentity does not expose address fields in Delivery Customization schema. Zip and country are available from cart.deliveryGroups[].deliveryAddress only."
          }
        },
        {
          "position": 2,
          "name": "Backup Rate Engine",
          "category": "Shipping",
          "description": "Automatically pulls a secondary carrier's rate for the zip when the primary carrier returns nothing, so checkout doesn't break.",
          "stack": "Backend service + ShipEngine API calls",
          "proof": null
        },
        {
          "position": 3,
          "name": "Dark-Zone Alert",
          "category": "Operations",
          "description": "Sends a Slack or email notification when a zip code in your service area returns zero rates from both your primary and backup carrier.",
          "stack": "Shopify Flow + webhook",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Custom webhook (backend fires when rate lookup fails for a zip)\nCondition: \n  – Zip is in your approved service-area list (stored as app data)\n  – Primary carrier returned no rates\n  – Backup carrier also returned no rates\nAction: \n  – Send Slack message to #shipping channel with zip, attempted carriers, timestamp\n  – Tag the order (if created) as 'rate-gap-alert'\n  – Optional: Create a Slack thread for manual override options\n",
            "note": "Your backend sends the webhook; Flow route it to Slack, email, or PagerDuty depending on urgency."
          }
        },
        {
          "position": 4,
          "name": "Failure Dashboard",
          "category": "Operations",
          "description": "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.",
          "stack": "Admin dashboard (custom app)",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL — query your app's stored rate-failure logs\nquery RateGaps($first: Int!) {\n  shop {\n    id\n    name\n  }\n  orders(first: $first, query: \"tag:rate-gap-alert\") {\n    edges {\n      node {\n        id\n        name\n        shippingAddress {\n          zip\n          country\n        }\n        createdAt\n        metafields(namespace: \"rate_gaps\", first: 10) {\n          edges {\n            node {\n              key\n              value\n            }\n          }\n        }\n      }\n    }\n  }\n}",
            "note": "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."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/live-shipping-rates-with-graceful-failover",
      "slug": "live-shipping-rates-with-graceful-failover",
      "title": "Live Shipping Rates with Graceful Failover",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:13:22.802Z",
      "updatedAt": "2026-05-21T14:13:22.802Z",
      "pieces": [
        {
          "position": 1,
          "name": "Delivery Rates Function",
          "category": "Shipping",
          "description": "Captures the cart and delivery address at checkout, then decides which rate engine to use based on speed.",
          "stack": "Shopify Function + Backend service",
          "proof": {
            "kind": "function",
            "language": "GraphQL",
            "pasteTarget": "extensions/delivery-function/src/run.graphql",
            "code": "query Input {\n  cart {\n    lines {\n      id\n      quantity\n      merchandise {\n        __typename\n        ... on ProductVariant {\n          id\n          product {\n            id\n            handle\n            metafield(namespace: \"shipengine\", key: \"carrier_code\") { value }\n          }\n        }\n      }\n      cost {\n        totalAmount { amount }\n      }\n    }\n    deliveryGroups {\n      deliveryAddress {\n        address1\n        city\n        provinceCode\n        zip\n        countryCode\n      }\n    }\n    buyerIdentity {\n      customer { id }\n    }\n  }\n}",
            "note": "Changed postalCode to zip on MailingAddress type."
          }
        },
        {
          "position": 2,
          "name": "Rate Engine Resolver",
          "category": "Shipping",
          "description": "A backend service that receives cart and address data, fires ShipEngine and native rates in parallel, and returns whichever answers within 800ms.",
          "stack": "Heroku-hosted Node backend",
          "proof": null
        },
        {
          "position": 3,
          "name": "ShipEngine Connector",
          "category": "Shipping",
          "description": "Pulls live carrier rates directly from ShipEngine, formatted as Shopify delivery options for checkout.",
          "stack": "Node.js + ShipEngine API client",
          "proof": null
        },
        {
          "position": 4,
          "name": "Failover & Monitoring",
          "category": "Shipping",
          "description": "Logs timeouts and successes so you can track ShipEngine reliability and adjust the timeout if needed.",
          "stack": "Monitoring dashboard + logs",
          "proof": null
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/wholesale-collection-gate-2",
      "slug": "wholesale-collection-gate-2",
      "title": "Wholesale Collection Gate",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:12:40.960Z",
      "updatedAt": "2026-05-21T14:12:40.960Z",
      "pieces": [
        {
          "position": 1,
          "name": "Collection Tag Gate",
          "category": "Storefront",
          "description": "Checks if the logged-in customer has the b2b tag; if not, renders a 404 page instead of the collection.",
          "stack": "Theme collection template + Liquid",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/templates/collection.json (or theme/snippets/collection-gate.liquid called early in the template)",
            "code": "{% assign customer_is_b2b = false %}\n{% if customer %}\n  {% if customer.tags contains 'b2b' %}\n    {% assign customer_is_b2b = true %}\n  {% endif %}\n{% endif %}\n\n{% if collection.handle == 'wholesale' or collection.handle == 'b2b-only' %}\n  {% unless customer_is_b2b %}\n    {% assign request.page_type = 'notfound' %}\n    {% render 'page-not-found' %}\n  {% endunless %}\n{% endif %}\n\n{% if customer_is_b2b %}\n  <!-- Render normal collection template -->\n  <div class=\"collection\">\n    <h1>{{ collection.title }}</h1>\n    <!-- Your existing collection content here -->\n  </div>\n{% endif %}",
            "note": "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."
          }
        },
        {
          "position": 2,
          "name": "B2B Customer Tag Sync",
          "category": "B2B / Wholesale",
          "description": "Automatically tags customers in Shopify when they sign up through your B2B company portal or are manually approved.",
          "stack": "Custom Shopify app + admin dashboard",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL\nmutation TagB2BCustomer($customerId: ID!, $tags: [String!]!) {\n  customerUpdate(input: { id: $customerId, tags: $tags }) {\n    customer {\n      id\n      email\n      tags\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Variables example:\n# {\n#   \"customerId\": \"gid://shopify/Customer/123456\",\n#   \"tags\": [\"b2b\"]\n# }",
            "note": "Tags passed as an array replace existing tags entirely; to append without removing others, fetch the customer's current tags first, merge, then update."
          }
        },
        {
          "position": 3,
          "name": "Search & Nav Blackout",
          "category": "Storefront",
          "description": "Removes wholesale collections from search results and main navigation menus for retail customers; B2B accounts see them normally.",
          "stack": "Theme snippets + Liquid conditionals",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/main-nav.liquid (or your header/navigation snippet)",
            "code": "{% assign show_wholesale_nav = false %}\n{% if customer and customer.tags contains 'b2b' %}\n  {% assign show_wholesale_nav = true %}\n{% endif %}\n\n<!-- In your navigation or menu loop: -->\n{% for link in linklists.main-menu.links %}\n  {% assign is_wholesale_collection = false %}\n  {% if link.type == 'collection_link' %}\n    {% if link.object.handle == 'wholesale' or link.object.handle == 'b2b-only' %}\n      {% assign is_wholesale_collection = true %}\n    {% endif %}\n  {% endif %}\n  \n  {% unless is_wholesale_collection and show_wholesale_nav == false %}\n    <li><a href=\"{{ link.url }}\">{{ link.title }}</a></li>\n  {% endunless %}\n{% endfor %}",
            "note": "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."
          }
        },
        {
          "position": 4,
          "name": "B2B Verification Workflow",
          "category": "Operations",
          "description": "A manual or automated process to review and tag new accounts as B2B within the Shopify admin, so they unlock collection access immediately.",
          "stack": "Shopify Flow + admin custom columns",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Customer created or updated (by staff in admin)\nCondition: Customer.email contains specific domain (e.g., @yourb2bpartner.com) OR customer added to a specific group/tag\nThen: Tag customer with \"b2b\"\nAlternative: Trigger a Shopify admin notification for manual review → Staff reviews → Tags customer in admin directly\n\nFor semi-automated: Trigger on order > X value from new customer → Assign \"b2b\" tag → Send approval email to staff.",
            "note": "Pure automation (domain-based tagging) is fast but may mis-tag. Manual review is slower but safer. Hybrid (threshold-based notification) balances both."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/wholesale-portal-contract-pricing",
      "slug": "wholesale-portal-contract-pricing",
      "title": "Wholesale Portal & Contract Pricing",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:11:46.272Z",
      "updatedAt": "2026-05-21T14:11:46.272Z",
      "pieces": [
        {
          "position": 1,
          "name": "Company Tier & Pricing Setup",
          "category": "B2B / Wholesale",
          "description": "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.",
          "stack": "Admin dashboard + metafields",
          "proof": null
        },
        {
          "position": 2,
          "name": "Contract Price Override",
          "category": "B2B / Wholesale",
          "description": "Store per-company product pricing on each company as metafields, so one buyer sees $50 while another sees $45 for the same SKU.",
          "stack": "Metafields + Shopify Functions",
          "proof": {
            "kind": "function",
            "language": "GraphQL",
            "pasteTarget": "extensions/discount-or-cart-transform/src/run.graphql",
            "code": "# Function input query — Cart Transform or Discount Function\nquery Input {\n  cart {\n    lines {\n      id\n      quantity\n      merchandise {\n        __typename\n        ... on ProductVariant {\n          id\n          sku\n          product {\n            id\n            handle\n          }\n        }\n      }\n      cost {\n        amountPerQuantity { amount }\n        totalAmount { amount }\n      }\n    }\n    buyerIdentity {\n      customer {\n        id\n        metafield(namespace: \"wholesale\", key: \"company_id\") { value }\n      }\n    }\n  }\n}\n\n# In your function logic:\n# 1. Read cart.buyerIdentity.customer.metafield(\"wholesale\", \"company_id\")\n# 2. Fetch that company's contract prices from Shopify Admin\n# 3. For each line, if a contract price exists, apply it via Cart Transform or Discount",
            "note": "Requires Shopify Plus for Cart Transform Function; Discount Function available on all plans."
          }
        },
        {
          "position": 3,
          "name": "Wholesale Buyer Portal",
          "category": "B2B / Wholesale",
          "description": "Private storefront where only authenticated wholesale customers land, browse products at their contract prices, and submit orders as POs instead of completing checkout.",
          "stack": "Custom Shopify app (Remix) + theme gating",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/wholesale-gate.liquid",
            "code": "{% comment %}\nPlace this at the top of your product/collection/cart pages.\nIt redirects non-wholesale customers to public storefront.\n{% endcomment %}\n\n{% if customer and customer.metafields.wholesale.company_id %}\n  {% comment %} Wholesale customer — show contract price {% endcomment %}\n  {% assign contract_price = customer.metafields.wholesale.product_price %}\n  {% if contract_price %}\n    <div style=\"background: #f0f0f0; padding: 1rem; margin-bottom: 1rem;\">\n      <strong>Contract Price:</strong> {{ contract_price | money }}\n    </div>\n  {% endif %}\n  \n  {% comment %} Show PO submission form button instead of \"Add to Cart\" {% endcomment %}\n  <form method=\"post\" action=\"/apps/wholesale-portal/submit-po\">\n    <input type=\"hidden\" name=\"product_id\" value=\"{{ product.id }}\" />\n    <input type=\"hidden\" name=\"variant_id\" value=\"{{ variant.id }}\" />\n    <label for=\"qty\">Quantity:</label>\n    <input type=\"number\" id=\"qty\" name=\"quantity\" value=\"1\" min=\"1\" />\n    <button type=\"submit\">Add to Purchase Order</button>\n  </form>\n\n{% else %}\n  {% comment %} Not a wholesale customer — redirect {% endcomment %}\n  <script>\n    window.location.href = \"{{ shop.secure_url }}\";\n  </script>\n{% endif %}",
            "note": "Customize the redirect URL and styling to match your brand."
          }
        },
        {
          "position": 4,
          "name": "Draft Order Approval Queue",
          "category": "Operations",
          "description": "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.",
          "stack": "Shopify Flow + admin dashboard",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: When a draft order is created\n\nCondition: draft order has tag \"wholesale_pending_approval\"\n\nActions:\n  1. Send Slack/email notification to #wholesale-team or approval-email@company.com\n     Message: \"New PO from [company name]: [order total]. Review in admin.\"\n  2. Tag draft order with \"wholesale_pending_approval\"\n  3. (Optional) Create a task or alert in your ticketing system\n\n— \n\nSecond Flow (on approval):\n\nTrigger: When a draft order is completed\n\nCondition: draft order tag contains \"wholesale_approved\"\n\nActions:\n  1. Send email to customer with order confirmation and estimated ship date\n  2. Tag the resulting order with \"wholesale_order\"\n  3. (Optional) Log to a webhook to sync fulfillment to your warehouse system",
            "note": "Draft order creation can be done via the portal app or Admin API; Flow listens for the tag change."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/b2b-net-30-checkout-gate",
      "slug": "b2b-net-30-checkout-gate",
      "title": "B2B Net 30 Checkout Gate",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:10:46.175Z",
      "updatedAt": "2026-05-21T14:10:46.175Z",
      "pieces": [
        {
          "position": 1,
          "name": "Approval Metafield",
          "category": "B2B / Wholesale",
          "description": "Adds a credit-approved flag and terms date to each B2B customer so checkout knows who qualifies for net 30.",
          "stack": "Admin API + Shopify admin UI",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "# Admin GraphQL\nmutation SetApprovalStatus($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields {\n      id\n      namespace\n      key\n      value\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n\n# Variables:\n# {\n#   \"metafields\": [\n#     {\n#       \"ownerId\": \"gid://shopify/Customer/1234567\",\n#       \"namespace\": \"b2b_credit\",\n#       \"key\": \"approved\",\n#       \"type\": \"boolean\",\n#       \"value\": \"true\"\n#     },\n#     {\n#       \"ownerId\": \"gid://shopify/Customer/1234567\",\n#       \"namespace\": \"b2b_credit\",\n#       \"key\": \"approval_date\",\n#       \"type\": \"date\",\n#       \"value\": \"2025-01-15\"\n#     }\n#   ]\n# }",
            "note": "Replace Customer ID with the B2B account you are approving. Rerun to update status."
          }
        },
        {
          "position": 2,
          "name": "Checkout Payment Gate",
          "category": "B2B / Wholesale",
          "description": "Hides the payment step for approved customers, letting them proceed to order confirmation with net 30 terms.",
          "stack": "Checkout UI extension",
          "proof": {
            "kind": "function",
            "language": "GraphQL",
            "pasteTarget": "extensions/payment-customization/src/run.graphql",
            "code": "# Function input query — Payment Customization\nquery Input {\n  cart {\n    buyerIdentity {\n      customer {\n        id\n        metafield(namespace: \"b2b_credit\", key: \"approved\") {\n          value\n        }\n      }\n    }\n  }\n}",
            "note": "Requires Shopify Plus Checkout Extensions. The function returns the approval flag; your handler hides payment UI if true."
          }
        },
        {
          "position": 3,
          "name": "Net 30 Terms Display",
          "category": "Storefront",
          "description": "Shows net 30 payment terms and no-payment-required messaging on cart and checkout pages for approved B2B accounts.",
          "stack": "Theme snippet + section",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/b2b-net30-terms.liquid, included in cart/checkout template",
            "code": "{% if customer and customer.metafields.b2b_credit.approved.value == true %}\n  <div class=\"b2b-net30-banner\">\n    <p>\n      <strong>Net 30 Terms Applied</strong><br>\n      Your account is credit-approved. No payment required at checkout.\n      Invoice will be sent to {{ customer.email }} after fulfillment.\n    </p>\n  </div>\n{% endif %}",
            "note": "Include this snippet in your cart page and checkout page templates. Style the banner to match your site."
          }
        },
        {
          "position": 4,
          "name": "Approved Order Router",
          "category": "Operations",
          "description": "Watches for orders from approved customers and routes them to your accounting system, tagged for manual invoice generation.",
          "stack": "Shopify Flow + backend webhook",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: When an order is created\nCondition:\n  - Customer metafield 'b2b_credit.approved' equals true\nThen:\n  1. Add tag 'b2b_net30_no_payment' to order\n  2. Send to Zapier or Make webhook:\n     - Payload: order ID, customer email, billing/shipping address, line items, total\n  3. Send notification to accounting@company.com with order details link",
            "note": "Adjust the webhook URL to your accounting system (QuickBooks, NetSuite, etc). Tag ensures you can filter approved orders in your admin."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/recharge-to-shopify-subscriptions-migration",
      "slug": "recharge-to-shopify-subscriptions-migration",
      "title": "Recharge to Shopify Subscriptions Migration",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T14:10:01.445Z",
      "updatedAt": "2026-05-21T14:10:01.445Z",
      "pieces": [
        {
          "position": 1,
          "name": "Recharge Data Export",
          "category": "Migration",
          "description": "Exports active Recharge subscriptions with billing dates, trial state, and customer ID in a format the developer can load into the new system.",
          "stack": "Recharge API + backend service",
          "proof": null
        },
        {
          "position": 2,
          "name": "Subscription Creator",
          "category": "Subscriptions",
          "description": "Creates a native Shopify Subscription for each migrated subscriber, preserving their billing date and trial end date so the next charge fires on time.",
          "stack": "Admin API mutation + backend service",
          "proof": null
        },
        {
          "position": 3,
          "name": "Migration Status Tracker",
          "category": "Operations",
          "description": "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.",
          "stack": "Backend service + admin dashboard",
          "proof": {
            "kind": "graphql",
            "language": "GraphQL",
            "pasteTarget": "Admin GraphQL explorer",
            "code": "query VerifyMigration($customerId: ID!) {\n  customer(id: $customerId) {\n    id\n    email\n    subscriptionContracts(first: 10) {\n      edges {\n        node {\n          id\n          status\n          nextBillingDate\n          createdAt\n          lines(first: 5) {\n            edges {\n              node {\n                id\n                title\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\nVariables:\n{\n  \"customerId\": \"gid://shopify/Customer/12345\"\n}",
            "note": "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."
          }
        },
        {
          "position": 4,
          "name": "Klaviyo Flow Bridge",
          "category": "Customer Service",
          "description": "Re-enrolls each migrated subscriber into the correct Klaviyo segment and re-triggers subscription renewal flows so notifications stay in sync with Shopify Subscriptions.",
          "stack": "Klaviyo API + Flow trigger",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Shopify subscription created\nCondition: customer.tags contains 'Recharge:Migrated'\nAction 1: Add profile to Klaviyo list: \"Shopify Subscriptions\"\nAction 2: Set profile property subscription_platform = \"native\"\nAction 3: Add profile to Klaviyo segment: based on product and billing frequency (e.g., \"Monthly Box Subscribers\")\nAction 4: Trigger Klaviyo flow: \"Subscription Welcome (Shopify)\" with customer ID + next billing date\nAction 5: Remove profile from list: \"Recharge Subscribers\" (optional, keeps history)",
            "note": "Tag each customer as 'Recharge:Migrated' during subscription creation so Flow can route them. Adjust segment logic based on your product bundle structure."
          }
        }
      ]
    },
    {
      "url": "https://www.tomsailors.com/b/first-time-customer-offer-gate",
      "slug": "first-time-customer-offer-gate",
      "title": "First-Time Customer Offer Gate",
      "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.",
      "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.",
      "publishedAt": "2026-05-21T13:19:53.754Z",
      "updatedAt": "2026-05-21T13:19:53.754Z",
      "pieces": [
        {
          "position": 1,
          "name": "New Customer Detector",
          "category": "Storefront",
          "description": "Checks if the logged-in customer has ever placed an order; if not, shows the offer—if yes, redirects to your main shop.",
          "stack": "Theme app extension",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/snippets/first-time-gate.liquid",
            "code": "{% if customer %}\n  {% if customer.orders_count == 0 %}\n    <!-- New customer: show the offer page -->\n    <div class=\"offer-container\">\n      <h1>Exclusive First-Time Offer</h1>\n      <!-- Your landing page content here -->\n    </div>\n  {% else %}\n    <!-- Returning customer: redirect -->\n    <script>\n      window.location.href = '/';\n    </script>\n  {% endif %}\n{% else %}\n  <!-- Not logged in: show offer (they may buy as guest or log in) -->\n  <div class=\"offer-container\">\n    <h1>Exclusive First-Time Offer</h1>\n  </div>\n{% endif %}",
            "note": "Place this snippet in the landing page template; it runs on every load."
          }
        },
        {
          "position": 2,
          "name": "Offer Landing Page",
          "category": "Storefront",
          "description": "A custom page template with the product, copy, and cart button designed to convert first-time buyers.",
          "stack": "Liquid theme section",
          "proof": {
            "kind": "liquid",
            "language": "Shopify Liquid",
            "pasteTarget": "theme/sections/offer-landing.liquid",
            "code": "{% assign product = all_products['your-product-handle'] %}\n<section class=\"offer-hero\">\n  <div class=\"offer-content\">\n    <h1>{{ product.title }}</h1>\n    <p class=\"offer-badge\">First-Time Customer Exclusive</p>\n    <p>{{ product.description }}</p>\n    <div class=\"offer-price\">\n      {% if product.selected_or_first_available_variant.compare_at_price %}\n        <span class=\"original\">{{ product.selected_or_first_available_variant.compare_at_price | money }}</span>\n      {% endif %}\n      <span class=\"sale-price\">{{ product.selected_or_first_available_variant.price | money }}</span>\n    </div>\n    <form method=\"post\" action=\"/cart/add\">\n      <input type=\"hidden\" name=\"id\" value=\"{{ product.selected_or_first_available_variant.id }}\">\n      <input type=\"number\" name=\"quantity\" value=\"1\" min=\"1\">\n      <button type=\"submit\" class=\"btn btn-primary\">Add to Cart</button>\n    </form>\n  </div>\n</section>",
            "note": "Replace 'your-product-handle' with your actual product handle."
          }
        },
        {
          "position": 3,
          "name": "Offer Redemption Tag",
          "category": "Operations",
          "description": "Automatically tags a customer the moment they complete their first purchase from this campaign, enabling tracking of campaign performance and prevention of double-offers.",
          "stack": "Shopify Flow",
          "proof": {
            "kind": "flow",
            "language": "text/plain",
            "pasteTarget": "Shopify Flow editor: When → Then",
            "code": "Trigger: Order created\nCondition: Customer's order count equals 1 (first order)\nThen: Tag customer with \"first-time-offer-redeemed\"\nThen: Add customer to Shopify segment \"First-Time Offer Customers\"\nThen: Send notification (optional): \"Congratulate them via email using Klaviyo or your ESP\"",
            "note": "Enables you to track campaign performance and prevent double-offers."
          }
        },
        {
          "position": 4,
          "name": "PPC Pixel & Redirect",
          "category": "Operations",
          "description": "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.",
          "stack": "Theme script + GTM",
          "proof": null
        }
      ]
    }
  ]
}