Combine fulfillments for a single order on demand with Mechanic.

Mechanic is an automation and development platform for Shopify. :)

Combine fulfillments for a single order on demand

by Isaac Bowen (team@usemechanic.com)

By submitting an order name/number, you can use this task to (a) cancel all existing fulfillments for that order, and then (b) create a new, single fulfillment for all line items on the order.

Runs when some text is submitted and when user/order_refulfill/action is triggered. Configuration includes ignore successful fulfillments, ignore open fulfillments, fulfillment location, notify customer about new fulfillment, and shipping method for new fulfillment.

15-day free trial – unlimited tasks

Documentation

Using the "Run task" button, enter an order name/number (e.g. "#1234" - the name must match exactly), and Mechanic will (a) cancel all existing fulfillments for this order, and then (b) create a new, single fulfillment for all line items on the order.

To route around a Shopify limitation, preventing multiple fulfillments from being cancelled simultaneously, this task operates in serial: it will run one action at a time, enqueuing a new event which will run the next action, and so on, until all necessary actions are performed. Learn how to find each child event.

By default, Mechanic will create fulfillments with your store's default fulfillment location (also called "the shipping origin"). To choose a different location, use the "Fulfillment location ID" option. Learn how to find the location ID.

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 some text is submitted (mechanic/user/text)
when user/order_refulfill/action is triggered (user/order_refulfill/action)
Options
ignore successful fulfillments (boolean), ignore open fulfillments (boolean), fulfillment location (number), notify customer about new fulfillment (boolean), shipping method for new fulfillment
Script
{% comment %}
  If things are happening too fast, and you're seeing Shopify API errors,
  increase this value. It's in seconds.
{% endcomment %}
{% assign event_gap_s = 5 %}

{% if event.topic == "mechanic/user/text" %}
  {% if event.preview %}
    {% assign order_name = "#1234" %}
  {% else %}
    {% assign order_name = event.data | strip %}
    {% if order_name == blank %}
      {"error": "Please try again, submitting an order name as input."}
    {% endif %}
  {% endif %}

  {% assign order_node = nil %}

  {% capture query %}
    query {
      orders(
        first: 250
        query: {{ event.data | json }}
      ) {
        edges {
          node {
            id
            legacyResourceId
            name
            fulfillments {
              legacyResourceId
              name
              status
            }
          }
        }
      }
    }
  {% endcapture %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "orders": {
            "edges": [
              {
                "node": {
                  "id": "gid://shopify/Order/1234567890",
                  "name": "#1234",
                  "legacyResourceId": "1234567890",
                  "fulfillments": [
                    {
                      "legacyResourceId": "1234567890",
                      "name": "#1234-F1",
                      "status": "PENDING"
                    }
                  ]
                }
              }
            ]
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% else %}
    {% assign result = query | shopify %}
  {% endif %}

  {% for order_edge in result.data.orders.edges %}
    {% assign some_order_node = order_edge.node %}
    {% if order_edge.node.name == order_name %}
      {% assign order_node = order_edge.node %}
      {% break %}
    {% endif %}
  {% endfor %}

  {% if order_node == nil %}
    {"error": {{ "Failed to locate an order with the name " | append: order_name | json }}}
  {% else %}
    {% assign cancellation_actions = array %}
    {% for fulfillment_node in order_node.fulfillments %}
      {% assign skip_fulfillment = false %}

      {% if fulfillment_node.status == "CANCELLED" %}
        {% assign skip_fulfillment = true %}
      {% elsif options.ignore_successful_fulfillments__boolean and fulfillment_node.status == "SUCCESS" %}
        {% assign skip_fulfillment = true %}
      {% elsif options.ignore_open_fulfillments__boolean and fulfillment_node.status == "OPEN" %}
        {% assign skip_fulfillment = true %}
      {% endif %}

      {% if skip_fulfillment %}
        {"log": {{ "Skipping fulfillment " | append: fulfillment_node.name | append: "; current status is " | append: fulfillment_node.status | json }}}
        {% continue %}
      {% endif %}

      {% capture action %}
        {% action "shopify" %}
          [
            "post",
            "/admin/orders/{{ order_node.legacyResourceId }}/fulfillments/{{ fulfillment_node.legacyResourceId }}/cancel.json",
            {}
          ]
        {% endaction %}
      {% endcapture %}

      {% assign cancellation_actions[cancellation_actions.size] = action | parse_json %}
    {% endfor %}

    {% if cancellation_actions == empty %}
      {"error": "Didn't find any fulfillments to cancel."}
    {% else %}
      {% if options.fulfillment_location_id__number %}
        {% assign location = shop.locations[options.fulfillment_location_id__number] %}
      {% else %}
        {% assign location = shop.locations[shop.primary_location_id] %}
      {% endif %}

      {% capture fulfillment_action %}
        {% action "shopify" %}
          mutation {
            fulfillmentCreate(
              input: {
                orderId: {{ order_node.id | json }}
                locationId: {{ location.admin_graphql_api_id | json }}
                notifyCustomer: {{ options.notify_customer_about_new_fulfillment__boolean | json }}
                {% if options.shipping_method_for_new_fulfillment != blank %}
                  shippingMethod: {{ options.shipping_method_for_new_fulfillment | json }}
                {% endif %}
              }
            ) {
              userErrors {
                field
                message
              }
              fulfillment {
                id
              }
              order {
                fulfillments {
                  id
                  status
                  name
                }
              }
            }
          }
        {% endaction %}
      {% endcapture %}

      {% assign actions_to_enqueue = cancellation_actions %}
      {% assign actions_to_enqueue[actions_to_enqueue.size] = fulfillment_action | parse_json %}

      {% action "event" %}
        {
          "topic": "user/order_refulfill/action",
          "run_at": {{ "now" | date: "%s" | plus: event_gap_s | json }},
          "data": {{ actions_to_enqueue | json }}
        }
      {% endaction %}
    {% endif %}
  {% endif %}
{% elsif event.topic == "user/order_refulfill/action" %}
  {% if event.preview %}
    {% capture event_json %}
      {
        "preview": true,
        "topic": "user/order_refulfill/action",
        "data": [
          {
            "action": {
              "type": "shopify",
              "options": [
                "post",
                "/admin/orders/1234567890/fulfillments/1234567890/cancel.json",
                {}
              ]
            }
          },
          {
            "action": {
              "type": "shopify",
              "options": "\n          mutation {\n            fulfillmentCreate(\n              input: {\n                orderId: \"gid://shopify/Order/1234567890\"\n                locationId: null\n                notifyCustomer: false\n                \n              }\n            ) {\n              userErrors {\n                field\n                message\n              }\n              fulfillment {\n                id\n              }\n              order {\n                fulfillments {\n                  id\n                  status\n                  name\n                }\n              }\n            }\n          }\n        "
            }
          }
        ]
      }
    {% endcapture %}

    {% assign event = event_json | parse_json %}
  {% endif %}

  {% assign actions_to_run = array %}
  {% assign actions_to_enqueue = array %}

  {% for action in event.data %}
    {% if event.preview or forloop.first %}
      {% assign actions_to_run[actions_to_run.size] = action %}
    {% else %}
      {% assign actions_to_enqueue[actions_to_enqueue.size] = action %}
    {% endif %}
  {% endfor %}

  {% if actions_to_run != empty %}
    {% for action in actions_to_run %}
      {{ action | json }}
    {% endfor %}
  {% endif %}

  {% if actions_to_enqueue != empty %}
    {% action "event" %}
      {
        "topic": "user/order_refulfill/action",
        "run_at": {{ "now" | date: "%s" | plus: event_gap_s | json }},
        "data": {{ actions_to_enqueue | json }}
      }
    {% endaction %}
  {% endif %}
{% endif %}
Mechanic tasks are written in Liquid, which makes them easy to write and easy to modify. Learn more about our platform.
Defaults
Ignore successful fulfillments
true
Ignore open fulfillments
true