Cancel and close unpaid orders after x hours/days, with Mechanic.

Mechanic is a development and ecommerce automation platform for Shopify. :)

Cancel and close unpaid orders after x hours/days

This task scans for orders that are more than X days or hours old that have a financial status of "pending", and ensures that they are all closed/archived and cancelled. Orders that are already closed will be cancelled, and orders that are already cancelled will be closed. Optionally, choose to add a tag to such orders.

Runs Occurs every day at midnight (in local time) and Occurs when a user manually triggers the task. Configuration includes only process orders having this tag, ignore orders having this tag, period to wait before checking each order, period to wait is in hours, period to wait is in days, tag to add to the order, void payment when possible, restock line items, send cancellation email to customer, and test mode.

15-day free trial – unlimited tasks

Documentation

This task scans for orders that are more than X days or hours old that have a financial status of "pending", and ensures that they are all closed/archived and cancelled. Orders that are already closed will be cancelled, and orders that are already cancelled will be closed. Optionally, choose to add a tag to such orders.

If configured with an interval in hours, this task will run hourly. If configured with an interval in days, the task will run every night at midnight, in your store's local timezone. Run this task manually to perform the scan on demand.

Run first using test mode, to ensure expected results before running without it.

Developer details

Mechanic is designed to benefit everybody: merchants, customers, developers, agencies, Shopifolks, 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.

(By the way, have you seen our documentation? Have you joined the Slack community?)

Open source
View on GitHub to contribute to this task
Subscriptions
{% if options.period_to_wait_is_in_hours__boolean %}
  mechanic/scheduler/hourly
{% elsif options.period_to_wait_is_in_days__boolean %}
  mechanic/scheduler/daily
{% endif %}

mechanic/user/trigger
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
only process orders having this tag, ignore orders having this tag, period to wait before checking each order (number, required), period to wait is in hours (boolean), period to wait is in days (boolean), tag to add to the order, void payment when possible (boolean), restock line items (boolean), send cancellation email to customer (boolean), test mode (boolean)
Code
{% comment %}
  Preferred option order

  {{ options.only_process_orders_having_this_tag }}
  {{ options.ignore_orders_having_this_tag }}
  {{ options.period_to_wait_before_checking_each_order__number_required }}
  {{ options.period_to_wait_is_in_hours__boolean }}
  {{ options.period_to_wait_is_in_days__boolean }}
  {{ options.tag_to_add_to_the_order }}
  {{ options.void_payment_when_possible__boolean }}
  {{ options.restock_line_items__boolean }}
  {{ options.send_cancellation_email_to_customer__boolean }}
  {{ options.test_mode__boolean }}
{% endcomment %}

{% if options.period_to_wait_before_checking_each_order__number_required <= 0 %}
  {% error "Period must be positive! :)" %}
{% elsif options.period_to_wait_is_in_hours__boolean == false and options.period_to_wait_is_in_days__boolean == false %}
  {% error "Choose either 'Period to wait is in hours' or 'Period to wait is in days'." %}
{% elsif options.period_to_wait_is_in_hours__boolean and options.period_to_wait_is_in_days__boolean %}
  {% error "Choose exactly one of 'Period to wait is in hours' or 'Period to wait is in days'. :)" %}
{% elsif options.period_to_wait_is_in_hours__boolean %}
  {% assign period_unit = 60 | times: 60 %}
{% elsif options.period_to_wait_is_in_days__boolean %}
  {% assign period_unit = 60 | times: 60 | times: 24 %}
{% endif %}

{% assign period_s = period_unit | times: options.period_to_wait_before_checking_each_order__number_required %}
{% assign maximum_date_s = "now" | date: "%s" | minus: period_s %}

{% if options.test_mode__boolean %}
  {% assign test_mode_summaries = array %}
{% endif %}

{% capture orders_query %}
  financial_status:pending
  {% if options.only_process_orders_having_this_tag != blank %}
    AND tag:{{ options.only_process_orders_having_this_tag | strip | json }}
  {% endif %}
  {% if options.ignore_orders_having_this_tag != blank %}
    AND -tag:{{ options.ignore_orders_having_this_tag | strip | json }}
  {% endif %}
{% endcapture %}

{% assign orders_query = orders_query | strip_newlines %}

{% log orders_query: orders_query %}

{% assign cursor = nil %}

{% for n in (0..100) %}
  {% capture query %}
    query {
      orders(
        first: 50
        after: {{ cursor | json }}
        query: {{ orders_query | json }}

        # this piece is important for stores that have read_all_orders
        # disabled. without this, we run the risk of getting an empty
        # set of edges back, preventing us from getting any cursors
        # with which to move on to the next page.
        sortKey: CREATED_AT
        reverse: true
      ) {
        pageInfo {
          hasNextPage
        }
        edges {
          cursor
          node {
            id
            legacyResourceId
            name
            createdAt
            processedAt
            cancelledAt
            closedAt
            tags
            suggestedRefund(suggestFullRefund: true, refundShipping: true) {
              refundLineItems {
                lineItem {
                  id
                  refundableQuantity
                  quantity
                  restockable
                }
                location {
                  id
                }
              }
            }
            transactions {
              id
              gateway
            }
          }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "orders": {
            "edges": [
              {
                "node": {
                  "id": "gid://shopify/Order/1234567890",
                  "legacyResourceId": "1234567890",
                  "name": "#1234",
                  "processedAt": "1990-01-01",
                  "cancelledAt": null,
                  "closedAt": null,
                  "tags": [
                    {{ options.only_process_orders_having_this_tag | strip | json }}
                  ],
                  "suggestedRefund": {
                    "refundLineItems": [
                      {
                        "lineItem": {
                          "id": "gid://shopify/LineItem/1234567890",
                          "refundableQuantity": 5,
                          "restockable": true
                        },
                        "location": {
                          "id": "gid://shopify/Location/1234567890"
                        }
                      }
                    ]
                  },
                  "transactions": [
                    {
                      "id": "gid://shopify/OrderTransaction/1234567890",
                      "gateway": "bogus"
                    }
                  ]
                }
              }
            ]
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% endif %}

  {% for edge in result.data.orders.edges %}
    {% assign order = edge.node %}

    {% assign processed_at_s = order.processedAt | default: order.createdAt | date: "%s" | times: 1 %}

    {% if processed_at_s > maximum_date_s %}
      {% continue %}
    {% endif %}

    {% assign summary = hash %}
    {% assign summary["order_name"] = order.name %}
    {% assign summary["order_id"] = order.id %}
    {% assign summary["order_processed_at"] = processed_at_s | date: "%FT%T%:z" %}
    {% assign summary["threshold_processed_at"] = maximum_date_s | date: "%FT%T%:z" %}

    {% if order.cancelledAt == blank %}
      {% assign summary["qualifies_for_cancellation"] = true %}
    {% else %}
      {% assign summary["qualifies_for_cancellation"] = false %}
    {% endif %}

    {% if order.closedAt == blank %}
      {% assign summary["qualifies_for_closing"] = true %}
    {% else %}
      {% assign summary["qualifies_for_closing"] = false %}
    {% endif %}

    {% if order.fulfillment_status == "fulfilled" or order.fulfillment_status == "partial" %}
      {% assign summary["qualifies_for_closing"] = false %}
    {% else %}
      {% assign summary["qualifies_for_closing"] = true %}
    {% endif %}

    {% assign summary["qualifies_for_tagging"] = false %}
    {% if options.tag_to_add_to_the_order != blank %}
      {% assign order_tags = order.tags | join: "," | downcase | split: "," %}
      {% assign order_tag_to_match = options.tag_to_add_to_the_order | downcase | strip %}
      {% unless order_tags contains order_tag_to_match %}
        {% assign summary["qualifies_for_tagging"] = true %}
      {% endunless %}
    {% endif %}

    {% unless summary.qualifies_for_cancellation or summary.qualifies_for_closing or summary.qualifies_for_tagging %}
      {% continue %}
    {% endunless %}

    {% if options.test_mode__boolean %}
      {% assign test_mode_summaries[test_mode_summaries.size] = summary %}
      {% continue %}
    {% endif %}

    {% log summary %}

    {% if summary.qualifies_for_cancellation %}

      {% action "shopify" %}
        mutation {
          refundCreate(input: {
            orderId: {{ order.id | json }}
            {% assign transaction = order.transactions.first %}
            {% if options.void_payment_when_possible__boolean and transaction != nil %}
              transactions: [
                {
                  amount: "0.0"
                  gateway: {{ transaction.gateway | json }}
                  kind: VOID
                  orderId: {{ order.id | json }}
                  parentId: {{ transaction.id | json }}
                }
              ]
            {% endif %}
            refundLineItems: [
              {% for refund_line_item in order.suggestedRefund.refundLineItems %}
                {% assign line_item = refund_line_item.lineItem %}
                {
                  lineItemId: {{ line_item.id | json }}
                  {% if options.restock_line_items__boolean and line_item.restockable %}
                    locationId: {{ refund_line_item.location.id | json }}
                    quantity: {{ line_item.refundableQuantity | default: line_item.quantity | json }}
                    restockType: CANCEL
                  {% else %}
                    quantity: 0
                    restockType: NO_RESTOCK
                  {% endif %}
                }
                {% unless forloop.last %}
                  ,
                {% endunless %}
              {% endfor %}
            ]
            notify: false
            note: {{ task.name | json }}
          }) {
            userErrors {
              field
              message
            }
          }
        }
      {% endaction %}

      {% action "shopify" %}
        [
          "post",
          "/admin/orders/{{ order.legacyResourceId }}/cancel.json",
          {
            "email": {{ options.send_cancellation_email_to_customer__boolean | json }}
          }
        ]
      {% endaction %}
    {% endif %}

    {% if summary.qualifies_for_closing or summary.qualifies_for_tagging %}
      {% action "shopify" %}
        mutation {
          {% if summary.qualifies_for_closing %}
            orderClose(
              input: {
                id: {{ order.id | json }}
              }
            ) {
              order {
                closed
                closedAt
              }
              userErrors {
                field
                message
              }
            }
          {% endif %}

          {% if summary.qualifies_for_tagging %}
            tagsAdd(
              id: {{ order.id | json }}
              tags: {{ options.tag_to_add_to_the_order | json }}
            ) {
              userErrors {
                field
                message
              }
            }
          {% endif %}
        }
      {% endaction %}
    {% endif %}
  {% endfor %}

  {% if result.data.orders.pageInfo.hasNextPage %}
    {% assign cursor = result.data.orders.edges.last.cursor %}
  {% else %}
    {% break %}
  {% endif %}
{% endfor %}

{% if options.test_mode__boolean %}
  {% action "echo" orders_count: test_mode_summaries.size, order_summaries: test_mode_summaries %}
{% endif %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more
Defaults
Period to wait before checking each order
1
Period to wait is in days
true
Test mode
true