Sync variant inventory within a product by pack size with Mechanic.

Mechanic is the one-tool-does-it-all automation app for Shopify. :)

Sync variant inventory within a product by pack size

by Isaac Bowen (team@usemechanic.com)

Use this task to sell a single product in different pack sizes, keeping inventory levels synchronized appropriately, storing the "master" inventory level in a product tag (e.g. "inventory:50").

Runs when an order is created, when a product is created, and when a product is updated. Configuration includes product inventory tag prefix, variant pack size option name, variant pack size metafield namespace, variant pack size metafield key, and run when orders are paid instead of when they are created.

15-day free trial – unlimited tasks

Documentation

Use this task to sell a single product in different pack sizes, keeping inventory levels synchronized appropriately, storing the "master" inventory level in a product tag (e.g. "inventory:50").

New orders for a pack-sized variant will result in the product's inventory tag being updated, and all pack-sized variants having their inventory levels re-synced. And, manually updating a product's inventory tag will automatically re-sync variant inventory levels as well.

The pack size for each variant can be determined by a numeric product option (e.g. "Pack size: 50"), or by metafield.

This task will skip any products that do not have an inventory tag, and will skip any variants ordered that have no pack size information.

Developer details

Mechanic is designed to benefit everybody: merchants, customers, developers, agencies, Gurus, everybody.

That’s why we make it easy to configure automation without code, why we make it easy to tweak the underlying code once tasks are installed, and why we publish it all here for everyone to learn from.

Events
when an order is created (shopify/orders/create)
when a product is created (shopify/products/create)
when a product is updated (shopify/products/update)
Options
product inventory tag prefix (required), variant pack size option name, variant pack size metafield namespace, variant pack size metafield key, run when orders are paid instead of when they are created (boolean)
Script
{% assign product_inventory_tag_prefix = options.product_inventory_tag_prefix__required %}
{% assign product_inventory_tag_prefix_size = product_inventory_tag_prefix.size %}
{% assign pack_size_option_name = options.variant_pack_size_option_name %}
{% assign metafield_namespace = options.variant_pack_size_metafield_namespace %}
{% assign metafield_key = options.variant_pack_size_metafield_key %}

{% if pack_size_option_name == blank and metafield_namespace == blank and metafield_key == blank %}
  {"error": "Choose a method for determining variant pack size: either option name, or metafield."}
{% elsif pack_size_option_name != blank and metafield_namespace != blank %}
  {"error": "Choose to determine the variant pack size by *either* option name or by metafield - not both."}
{% elsif pack_size_option_name != blank and metafield_key != blank %}
  {"error": "Choose to determine the variant pack size by *either* option name or by metafield - not both."}
{% elsif pack_size_option_name != blank %}
  {% comment %} cool {% endcomment %}
{% elsif metafield_namespace == blank or metafield_key == blank %}
  {"error": "If using metafields to determine variant pack size, fill in both metafield namespace and key."}
{% endif %}

{% if pack_size_option_name != blank %}
  {% assign pack_size_mode = "option" %}
{% else %}
  {% assign pack_size_mode = "metafield" %}
{% endif %}

{% assign product_considerations = array %}

{% if event.topic contains "shopify/orders/" %}
  {% capture order_query %}
    query {
      order(id: {{ order.admin_graphql_api_id | json }}) {
        lineItems(first: 249) {
          edges {
            node {
              quantity
              variant {
                product {
                  id
                  tags
                }
                {% if pack_size_mode == "option" %}
                  selectedOptions {
                    name
                    value
                  }
                {% else %}
                  packSizeMetafield: metafield(
                    namespace: {{ metafield_namespace | json }}
                    key: {{ metafield_key | json }}
                  ) {
                    value
                  }
                {% endif %}
              }
            }
          }
        }
      }
    }
  {% endcapture %}

  {% assign order_result = order_query | shopify %}
  {% assign lineItem_nodes = order_result.data.order.lineItems.edges | map: "node" %}

  {% if event.preview %}
    {% capture lineItem_node_json %}
      {
        "quantity": 2,
        "variant": {
          "product": {
            "id": "gid://shopify/Product/1234567890",
            "tags": {{ product_inventory_tag_prefix | append: 80 | json }}
          },
          {% if pack_size_mode == "option" %}
            "selectedOptions": [{
              "name": {{ pack_size_option_name | json }},
              "value": "25"
            }]
          {% else %}
            "packSizeMetafield": {
              "value": "25"
            }
          {% endif %}
        }
      }
    {% endcapture %}

    {% assign lineItem_nodes = array %}
    {% assign lineItem_nodes[0] = lineItem_node_json | parse_json %}
  {% endif %}

  {% for lineItem_node in lineItem_nodes %}
    {% assign pack_size = nil %}

    {% if pack_size_mode == "option" %}
      {% assign pack_size_option = lineItem_node.variant.selectedOptions | where: "name", pack_size_option_name | first %}
      {% assign pack_size = pack_size_option.value %}
    {% else %}
      {% assign pack_size = lineItem_node.variant.packSizeMetafield.value %}
    {% endif %}

    {% if pack_size == nil %}
      {% continue %}
    {% endif %}

    {% assign quantity_times_pack_size = lineItem_node.quantity | times: pack_size %}
    {% assign product_inventory_new = product_inventory_old | minus: quantity_times_pack_size %}

    {% assign product_consideration = hash %}
    {% assign product_consideration["id"] = lineItem_node.variant.product.id %}
    {% assign product_consideration["tags"] = lineItem_node.variant.product.tags %}
    {% assign product_consideration["inventory_difference"] = quantity_times_pack_size %}
    {% assign product_considerations[product_considerations.size] = product_consideration %}
  {% endfor %}
{% elsif event.topic contains "shopify/products/" %}
  {% assign product_id = product.admin_graphql_api_id %}
  {% assign product_tags = product.tags | split: ", " %}

  {% if event.preview %}
    {% assign product_id = "gid://shopify/Product/1234576890" %}
    {% assign product_tags = product_inventory_tag_prefix | append: 80 %}
  {% endif %}

  {% assign product_consideration = hash %}
  {% assign product_consideration["id"] = product_id %}
  {% assign product_consideration["tags"] = product_tags %}
  {% assign product_considerations[product_considerations.size] = product_consideration %}
{% endif %}

{% for product_consideration in product_considerations %}
  {% assign product_inventory_old = nil %}

  {% for product_tag in product_consideration.tags %}
    {% assign product_tag_possible_prefix = product_tag | slice: 0, product_inventory_tag_prefix_size %}
    {% if product_tag_possible_prefix == product_inventory_tag_prefix %}
      {% assign product_inventory_old = product_tag | slice: product_inventory_tag_prefix_size, 100 | times: 1 %}
      {% break %}
    {% endif %}
  {% endfor %}

  {% if product_inventory_old == nil %}
    {% continue %}
  {% endif %}

  {% assign product_inventory_new = product_inventory_old | minus: product_consideration.inventory_difference %}

  {% capture product_query %}
    query {
      product(id: {{ product_consideration.id | json }}) {
        variants(first: 250) {
          edges {
            node {
              inventoryQuantity
              inventoryItem {
                id
              }
              {% if pack_size_mode == "option" %}
                selectedOptions {
                  name
                  value
                }
              {% else %}
                packSizeMetafield: metafield(
                  namespace: {{ metafield_namespace | json }}
                  key: {{ metafield_key | json }}
                ) {
                  value
                }
              {% endif %}
            }
          }
        }
      }
    }
  {% endcapture %}

  {% assign product_result = product_query | shopify %}
  {% assign variant_nodes = product_result.data.product.variants.edges | map: "node" %}

  {% if event.preview %}
    {% capture variant_nodes_json %}
      [
        {
          "inventoryQuantity": {% if event.topic contains "shopify/orders/" %}3{% else %}0{% endif %},
          "inventoryItem": {
            "id": "gid://shopify/InventoryItem/1234567890"
          },
          {% if pack_size_mode == "option" %}
            "selectedOptions": [{
              "name": {{ pack_size_option_name | json }},
              "value": "25"
            }]
          {% else %}
            "packSizeMetafield": {
              "value": "25"
            }
          {% endif %}
        },
        {
          "inventoryQuantity": {% if event.topic contains "shopify/orders/" %}1{% else %}0{% endif %},
          "inventoryItem": {
            "id": "gid://shopify/InventoryItem/2345678901"
          },
          {% if pack_size_mode == "option" %}
            "selectedOptions": [{
              "name": {{ pack_size_option_name | json }},
              "value": "50"
            }]
          {% else %}
            "packSizeMetafield": {
              "value": "50"
            }
          {% endif %}
        }
      ]
    {% endcapture %}

    {% assign variant_nodes = variant_nodes_json | parse_json %}
  {% endif %}

  {% assign inventory_item_adjustments = array %}

  {% for variant_node in variant_nodes %}
    {% assign pack_size = nil %}

    {% if pack_size_mode == "option" %}
      {% assign pack_size_option = variant_node.selectedOptions | where: "name", pack_size_option_name | first %}
      {% assign pack_size = pack_size_option.value %}
    {% else %}
      {% assign pack_size = variant_node.packSizeMetafield.value %}
    {% endif %}

    {% if pack_size == nil %}
      {% continue %}
    {% endif %}

    {% assign new_inventory_level = product_inventory_new | times: 1.0 | divided_by: pack_size | floor %}

    {% if variant_node.inventoryQuantity == new_inventory_level %}
      {% continue %}
    {% endif %}

    {% capture adjustment %}
      {
        inventoryItemId: {{ variant_node.inventoryItem.id | json }}
        availableDelta: {{ new_inventory_level | minus: variant_node.inventoryQuantity | json }}
      }
    {% endcapture %}

    {% assign inventory_item_adjustments[inventory_item_adjustments.size] = adjustment %}
  {% endfor %}

  {% capture mutations %}
    {% if product_inventory_old != product_inventory_new %}
      tagsRemove(
        id: {{ product_consideration.id | json }}
        tags: {{ product_inventory_tag_prefix | append: product_inventory_old | json }}
      ) {
        userErrors {
          field
          message
        }
      }

      tagsAdd(
        id: {{ product_consideration.id | json }}
        tags: {{ product_inventory_tag_prefix | append: product_inventory_new | json }}
      ) {
        userErrors {
          field
          message
        }
      }
    {% endif %}

    {% if inventory_item_adjustments != empty %}
      inventoryBulkAdjustQuantityAtLocation(
        locationId: {{ shop.locations[shop.primary_location_id].admin_graphql_api_id | json }}
        inventoryItemAdjustments: [
          {{ inventory_item_adjustments | join: newline }}
        ]
      ) {
        userErrors {
          field
          message
        }
      }
    {% endif %}
  {% endcapture %}

  {% if mutations != blank %}
    {% action "shopify" %}
      mutation {
        {{ mutations }}
      }
    {% endaction %}
  {% endif %}
{% endfor %}
Mechanic tasks are written in Liquid, which makes them easy to write and easy to modify. Learn more about our platform.
Defaults
Product inventory tag prefix
inventory:
Variant pack size option name
Pack size