Accept a maximum number of orders per hour, with Mechanic.

Mechanic is a development platform for Shopify. :)

Accept a maximum number of orders per hour

by Isaac Bowen (team@usemechanic.com)

This task works by monitoring the number of orders created per hour, and clearing the inventory for all in-stock items when the hourly order limit is reached. Optionally, this task can restore inventory to its original levels at minute zero of the next hour, or on demand.

Runs when an order is created and every hour. Configuration includes maximum hourly orders, only clear inventory for products with this tag, restore inventory levels the next hour, and restore inventory levels on demand.

15-day free trial – unlimited tasks

Documentation

This task works by setting your inventory to zero when the hourly order limit is reached. (Specifically, this means setting inventory levels to 0 for all items that have a greater-than-zero inventory level.) There are no popups, or any specific messaging - your inventory will simply be dropped to zero, and if your shop is configured to stop selling out-of-stock products, your customers will be prevented from making additional purchases.

Optionally, this task can restore inventory to its original levels at midnight the next hour, or on demand. (Restore levels on demand by enabling this option, then using the "Run task" button.)

This task does not work well when you have multiple orders per minute, and we do not recommend using it for high-volume stores. It works by counting the current hour's orders, at the time of each purchase - if the current count exactly equals your maximum, it performs the inventory reset. Because that counting process can take a few seconds, receiving multiple orders per minute can result in missing that very specific window when the current count exactly equals your maximum.

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)
every hour (mechanic/scheduler/hourly)
Options
maximum hourly orders (number, required), only clear inventory for products with this tag, restore inventory levels the next hour (boolean), restore inventory levels on demand (boolean)
Script
{% comment %}
  Options order:

  {{ options.maximum_hourly_orders__number_required }}
  {{ options.only_clear_inventory_for_products_with_this_tag }}
  {{ options.restore_inventory_levels_the_next_hour__boolean }}
  {{ options.restore_inventory_levels_on_demand__boolean }}
{% endcomment %}

{% if options.maximum_hourly_orders__number_required <= 0 %}
  {% error "'Maximum hourly orders' must be at least 1. :)" %}
{% endif %}

{% if event.topic contains "shopify/orders" %}
  {% assign cursor = nil %}
  {% assign orders_this_hour = 0 %}
  {% assign previous_hour = "now" | date: "%Y-%m-%dT%H:00:00%z" %}
  {% assign previous_hour_s = previous_hour | date: "%s" %}
  {% assign cache_key = "inventory_to_restore:" | append: previous_hour_s %}
  {% assign inventory_levels_to_zero = cache[cache_key] | default: hash %}

  {% for n in (0..100) %}
    {% capture query %}
      query {
        orders(
          first: 250
          after: {{ cursor | json }}
          query: "created_at:>=\"{{ previous_hour }}\" -status:cancelled"
        ) {
          pageInfo {
            hasNextPage
          }
          edges {
            cursor
          }
        }
      }
    {% endcapture %}

    {% assign result = query | shopify %}

    {% if event.preview %}
      {% capture result_json %}
        {
          "data": {
            "orders": {
              "pageInfo": {
                "hasNextPage": false
              },
              "edges": [
                {% for n in (1..options.maximum_hourly_orders__number_required) %}
                  {
                    "cursor": "eyJsYXN0X2lkIjoyMTQ4MTQ2MjQ5NzczLCJsYXN0X3ZhbHVlIjoiMjAyMC0wNC0wMyAxNzowMjoxMyJ9"
                  }{% unless forloop.last %},{% endunless %}
                {% endfor %}
              ]
            }
          }
        }
      {% endcapture %}

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

    {% assign orders_this_hour = orders_this_hour | plus: result.data.orders.edges.size %}

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

  {% log orders_this_hour_thus_far: orders_this_hour %}

  {% if orders_this_hour == options.maximum_hourly_orders__number_required %}
    {% assign cursor = nil %}

    {% for n in (0..100) %}
      {% capture query %}
        query {
          inventoryItems(
            first: 65
            after: {{ cursor | json }}
          ) {
            pageInfo {
              hasNextPage
            }
            edges {
              cursor
              node {
                variant {
                  product {
                    tags
                  }
                }
                inventoryLevels(
                  first: 10
                ) {
                  edges {
                    node {
                      id
                      available
                    }
                  }
                }
              }
            }
          }
        }
      {% endcapture %}

      {% assign result = query | shopify %}

      {% if event.preview %}
        {% capture result_json %}
          {
            "data": {
              "inventoryItems": {
                "pageInfo": {
                  "hasNextPage": false
                },
                "edges": [
                  {
                    "cursor": "eyJsYXN0X2lkIjozNTIxODc4MTAxMjAxMywibGFzdF92YWx1ZSI6IjM1MjE4NzgxMDEyMDEzIn0=",
                    "node": {
                      "variant": {
                        "product": {
                          "tags": [
                            {% if options.only_clear_inventory_for_products_with_this_tag != blank %}
                              {{ options.only_clear_inventory_for_products_with_this_tag | json }}
                            {% endif %}
                          ]
                        }
                      },
                      "inventoryLevels": {
                        "edges": [
                          {
                            "node": {
                              "id": "gid://shopify/InventoryLevel/1234567890?inventory_item_id=1234567890",
                              "available": 20
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              }
            }
          }
        {% endcapture %}

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

      {% for inventoryItem_edge in result.data.inventoryItems.edges %}
        {% if options.only_clear_inventory_for_products_with_this_tag != blank %}
          {% unless inventoryItem_edge.node.variant.product.tags contains options.only_clear_inventory_for_products_with_this_tag %}
            {% continue %}
          {% endunless %}
        {% endif %}

        {% for inventoryLevel_edge in inventoryItem_edge.node.inventoryLevels.edges %}
          {% if inventoryLevel_edge.node.available <= 0 %}
            {% continue %}
          {% endif %}

          {% assign inventory_levels_to_zero[inventoryLevel_edge.node.id] = inventoryLevel_edge.node.available %}
        {% endfor %}
      {% endfor %}

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

    {% if inventory_levels_to_zero.size > 0 %}
      {% for keyval in inventory_levels_to_zero %}
        {% assign inventory_level_id = keyval[0] %}
        {% assign inventory_level_available = keyval[1] %}

        {% action "shopify" %}
          mutation {
            inventoryAdjustQuantity(
              input: {
                inventoryLevelId: {{ inventory_level_id | json }}
                availableDelta: {{ inventory_level_available | times: -1 | json }}
              }
            ) {
              inventoryLevel {
                available
              }
              userErrors {
                field
                message
              }
            }
          }
        {% endaction %}
      {% endfor %}

      {% if options.restore_inventory_levels_the_next_hour__boolean or options.restore_inventory_levels_on_demand__boolean %}
        {% action "cache", "set", cache_key, inventory_levels_to_zero %}
      {% endif %}
    {% endif %}
  {% endif %}
{% elsif event.topic contains "mechanic/" %}
  {% assign proceed = false %}

  {% if event.topic == "mechanic/scheduler/hourly" and options.restore_inventory_levels_the_next_hour__boolean %}
    {% assign proceed = true %}
  {% elsif event.topic == "mechanic/user/trigger" and options.restore_inventory_levels_on_demand__boolean %}
    {% assign proceed = true %}
  {% endif %}

  {% if proceed %}
    {% assign hour_in_s = 60 | times: 60 %}
    {% assign previous_hour = "now" | date: "%s" | minus: hour_in_s | date: "%Y-%m-%dT%H:00:00%z" %}
    {% assign previous_hour_s = previous_hour | date: "%s" %}
    {% assign cache_key = "inventory_to_restore:" | append: previous_hour_s %}

    {% assign inventory_levels_to_restore = cache[cache_key] | default: hash %}

    {% if inventory_levels_to_restore == blank %}
      {% assign previous_hour = "now" | date: "%Y-%m-%dT%H:00:00%z" %}
      {% assign previous_hour_s = previous_hour | date: "%s" %}
      {% assign cache_key = "inventory_to_restore:" | append: previous_hour_s %}
      {% assign inventory_levels_to_restore = cache[cache_key] | default: hash %}
    {% endif %}

    {% if event.preview %}
      {% assign inventory_levels_to_restore = hash %}
      {% assign id = "gid://shopify/InventoryLevel/1234567890?inventory_item_id=1234567890" %}
      {% assign inventory_levels_to_restore[id] = 20 %}
    {% endif %}

    {% log inventory_levels_to_restore_count: inventory_levels_to_restore.size, inventory_levels_to_restore: inventory_levels_to_restore, since: previous_hour %}
     
    {% for keyval in inventory_levels_to_restore %}
      {% assign inventory_level_id = keyval[0] %}
      {% assign inventory_level_available = keyval[1] %}

      {% action "shopify" %}
        mutation {
          inventoryAdjustQuantity(
            input: {
              inventoryLevelId: {{ inventory_level_id | json }}
              availableDelta: {{ inventory_level_available | json }}
            }
          ) {
            inventoryLevel {
              available
            }
            userErrors {
              field
              message
            }
          }
        }
      {% endaction %}
    {% endfor %}

    {% if inventory_levels_to_restore != blank %}
      {% action "cache", "del", cache_key %}
    {% endif %}
  {% endif %}
{% endif %}
Mechanic tasks are written in Liquid, which makes them easy to write and easy to modify. Learn more about our platform.
Defaults
Maximum hourly orders
10
Restore inventory levels the next hour
true