Maintain discount percentage filters in variant metafields, with Mechanic.

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

Maintain discount percentage filters in variant metafields

This task checks to see if a product's variants have been discounted (by having a price set below the compare at price), and for each variant that meets one ore more configured discount percentage thresholds, it will save the matched filters in a variant metafield for use with Online Store 2.0 filtering.

Runs Occurs whenever a product is updated, ordered, or variants are added, removed or updated and Occurs when a user manually triggers the task. Configuration includes metafield namespace and key and discount filters and percentage thresholds.

15-day free trial – unlimited tasks

Documentation

This task checks to see if a product's variants have been discounted (by having a price set below the compare at price), and for each variant that meets one ore more configured discount percentage thresholds, it will save the matched filters in a variant metafield for use with Online Store 2.0 filtering.

Variants that are not discounted or do not meet any of the configured thresholds will have their metafield deleted if it exists.

This task runs when products are updated, and may also be run manually to scan all variants in the shop.

Important notes:
- Before configuring and running this task, a metafield definition must be created in the Shopify admin with a "Single line text" type configured to "Accept list of values", per the documentation on creating custom metafield definitions.
- For the Discount filters and percentage thresholds task configuration field, enter the discount filter values that will display on the storefront (e.g. 25% off or more) on the left-hand side, and the percentage thresholds as whole numbers (e.g. 25) on the right.

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
shopify/products/update
mechanic/user/trigger
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
metafield namespace and key (required), discount filters and percentage thresholds (keyval, number, required)
Code
{% assign metafield_namespace_and_key = options.metafield_namespace_and_key__required | match: "(?<namespace>[\w-]{3,})\.(?<key>[\w-]{3,})" %}
{% assign discount_filters_and_percentage_thresholds = options.discount_filters_and_percentage_thresholds__keyval_number_required %}

{% assign metafield_namespace = metafield_namespace_and_key.named_captures.namespace %}
{% assign metafield_key = metafield_namespace_and_key.named_captures.key %}

{% if metafield_namespace == blank or metafield_key == blank %}
  {% error "The metafield key and namespace must each be at least 3 characters long and separated only by a '.'.\n\nValid characters are a-z, A-Z, 0-9, underscores, and dashes." %}
  {% break %}
{% endif %}

{% assign variants = array %}

{% if event.topic == "mechanic/user/trigger" or event.topic contains "mechanic/scheduler/" %}
  {% assign cursor = nil %}

  {% for n in (1..200) %}
    {% capture query %}
      query {
        productVariants(
          first: 250
          after: {{ cursor | json }}
        ) {
          pageInfo {
            hasNextPage
            endCursor
          }
          nodes {
            id
            displayName
            price
            compareAtPrice
            metafield(
              key: {{ metafield_namespace_and_key | json }}
            ) {
              value
            }
          }
        }
      }
    {% endcapture %}

    {% assign result = query | shopify %}

    {% assign variants
      = result.data.productVariants.nodes
      | default: array
      | concat: variants
    %}

    {% if result.data.productVariants.pageInfo.hasNextPage %}
      {% assign cursor = result.data.productVariants.pageInfo.endCursor %}
    {% else %}
      {% break %}
    {% endif %}
  {% endfor %}

{% elsif event.topic == "shopify/products/update" %}
  {% capture query %}
    query {
      product(id: {{ product.admin_graphql_api_id | json }}) {
        variants(first: 100) {
          nodes {
            id
            displayName
            price
            compareAtPrice
            metafield(
              key: {{ metafield_namespace_and_key | json }}
            ) {
              value
            }
          }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% assign variants = result.data.product.variants.nodes %}
{% endif %}

{% if event.preview %}
  {% capture variants_json %}
    [
      {
        "id": "gid://shopify/ProductVariant/1234567890",
        "price": "0.00",
        "compareAtPrice": "1.00"
      }
    ]
  {% endcapture %}

  {% assign variants = variants_json | parse_json %}
{% endif %}

{% assign metafields_to_set = array %}
{% assign metafields_to_delete = array %}
{% assign variant_logs = array %}

{% for variant in variants %}
  {% assign variant_log = hash %}
  {% assign variant_log["variant"] = variant %}

  {% assign price = variant.price | times: 1.0 %}
  {% assign compare_at_price = variant.compareAtPrice | times: 1.0 %}
  {% assign metafield_value
    = variant.metafield.value
    | default: "[]"
    | parse_json
  %}

  {% comment %}
    Note: converting to floats above will handle cases where compareAtPrice is empty or "0.00", as it will become 0.0 in either case
  {% endcomment %}

  {% if compare_at_price <= price %}
    {% if metafield_value != blank %}
      {% assign metafield_to_delete = hash %}
      {% assign metafield_to_delete["ownerId"] = variant.id %}
      {% assign metafield_to_delete["namespace"] = metafield_namespace %}
      {% assign metafield_to_delete["key"] = metafield_key %}
      {% assign metafields_to_delete = metafields_to_delete | push: metafield_to_delete %}
    {% endif %}

    {% assign variant_log["message"] = "Variant is not discounted; removing metafield if it exists." %}
    {% assign variant_logs = variant_logs | push: variant_log %}

    {% continue %}
  {% endif %}

  {% assign discount_percentage
    = price
    | times: -100
    | divided_by: compare_at_price
    | plus: 100
  %}

  {% assign discount_filters_should_have = array %}

  {% for keyval in discount_filters_and_percentage_thresholds %}
    {% assign discount_filter = keyval.first %}
    {% assign percentage_threshold = keyval.last %}

    {% if discount_percentage >= percentage_threshold %}
      {% assign discount_filters_should_have = discount_filters_should_have | push: discount_filter %}
    {% endif %}
  {% endfor %}

  {% assign variant_log["discount_percentage"] = discount_percentage %}
  {% assign variant_log["discount_filters_should_have"] = discount_filters_should_have %}

  {% if discount_filters_should_have == blank %}
    {% if metafield_value != blank %}
      {% assign metafield_to_delete = hash %}
      {% assign metafield_to_delete["ownerId"] = variant.id %}
      {% assign metafield_to_delete["namespace"] = metafield_namespace %}
      {% assign metafield_to_delete["key"] = metafield_key %}
      {% assign metafields_to_delete = metafields_to_delete | push: metafield_to_delete %}
    {% endif %}

    {% assign variant_log["message"] = "Variant is discounted, but it does not meet any of the configured percentage thresholds; removing metafield if it exists." %}
    {% assign variant_logs = variant_logs | push: variant_log %}

    {% continue %}
  {% endif %}

  {% if metafield_value == discount_filters_should_have %}
    {% continue %}
  {% endif %}

  {% assign variant_log["message"] = "Variant is discounted and metafield does not match the discount filters it should have; updating." %}
  {% assign variant_logs = variant_logs | push: variant_log %}

  {% assign metafield_to_set = hash %}
  {% assign metafield_to_set["ownerId"] = variant.id %}
  {% assign metafield_to_set["namespace"] = metafield_namespace %}
  {% assign metafield_to_set["key"] = metafield_key %}
  {% assign metafield_to_set["type"] = "list.single_line_text_field" %}
  {% assign metafield_to_set["value"] = discount_filters_should_have | json %}
  {% assign metafields_to_set = metafields_to_set | push: metafield_to_set %}
{% endfor %}

{% log variant_logs %}

{% if metafields_to_set != blank %}
  {% assign groups_of_metafields_to_set = metafields_to_set | in_groups_of: 25, fill_with: false %}

  {% for group_of_metafields_to_set in groups_of_metafields_to_set %}
    {% action "shopify" %}
      mutation {
        metafieldsSet(
          metafields: {{ group_of_metafields_to_set | graphql_arguments }}
        ) {
          metafields {
            id
            namespace
            key
            type
            value
            owner {
              ... on ProductVariant {
                id
                displayName
              }
            }
          }
          userErrors {
            code
            field
            message
          }
        }
      }
    {% endaction %}
  {% endfor %}
{% endif %}

{% if metafields_to_delete != blank %}
  {% assign groups_of_metafields_to_delete = metafields_to_delete | in_groups_of: 250, fill_with: false %}

  {% for group_of_metafields_to_delete in groups_of_metafields_to_delete %}
    {% action "shopify" %}
      mutation {
        metafieldsDelete(
          metafields: {{ group_of_metafields_to_delete | graphql_arguments }}
        ) {
          deletedMetafields {
            ownerId
            namespace
            key
          }
          userErrors {
            field
            message
          }
        }
      }
    {% endaction %}
  {% endfor %}
{% endif %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more