diff --git a/docs/advanced-scheduled-price-changes/script.liquid b/docs/advanced-scheduled-price-changes/script.liquid index 7635d68d..a03a0d24 100644 --- a/docs/advanced-scheduled-price-changes/script.liquid +++ b/docs/advanced-scheduled-price-changes/script.liquid @@ -75,7 +75,7 @@ {% if valid_start_datetime == blank %} {% unless event.preview %} - {% error "The event start date is not a valid date. Please re-enter it (per the task documentaion) and try scheduling the event again." %} + {% error "The event start date is not a valid date. Please re-enter it (per the task documentation) and try scheduling the event again." %} {% break %} {% endunless %} {% endif %} @@ -88,7 +88,7 @@ {% if valid_end_datetime == blank %} {% unless event.preview %} - {% error "The event end date is not a valid date. Please re-enter it (per the task documentaion) and try scheduling the event again." %} + {% error "The event end date is not a valid date. Please re-enter it (per the task documentation) and try scheduling the event again." %} {% break %} {% endunless %} {% endif %} @@ -100,7 +100,7 @@ {% if start_datetime_s <= now or start_datetime_s >= end_datetime_s %} {% unless event.preview %} - {% error "The event start and end dates must be future dates and the start date must be before the end date. Please re-enter them (per the task documentaion) and try scheduling the event again." %} + {% error "The event start and end dates must be future dates and the start date must be before the end date. Please re-enter them (per the task documentation) and try scheduling the event again." %} {% break %} {% endunless %} {% endif %} @@ -137,7 +137,7 @@ {% if discount != discount_string %} {% unless event.preview %} {% error - message: "Percentage discount entry incorrect. Please update the task configuration (per the task documentaion) and try scheduling the event again.", + message: "Percentage discount entry incorrect. Please update the task configuration (per the task documentation) and try scheduling the event again.", discount: discount %} {% break %} @@ -147,7 +147,7 @@ {% if discount_percentage <= 0 or discount_percentage >= 100 %} {% unless event.preview %} {% error - message: "Percentage discount entries should contain an number between 0 and 100. Please update the task configuration (per the task documentaion) and try scheduling the event again.", + message: "Percentage discount entries should contain an number between 0 and 100. Please update the task configuration (per the task documentation) and try scheduling the event again.", discount: discount %} {% break %} @@ -160,7 +160,7 @@ {% if discount_absolute == 0 %} {% unless event.preview %} {% error - message: "Fixed price and fixed discount entries must contain an absolute number > 0. Please update the task configuration (per the task documentaion) and try scheduling the event again.", + message: "Fixed price and fixed discount entries must contain an absolute number > 0. Please update the task configuration (per the task documentation) and try scheduling the event again.", discount: discount %} {% break %} @@ -1014,7 +1014,7 @@ {% for product in products %} {% assign variant_updates = array %} - {% assign mutations = array %} + {% assign metafields_to_delete = array %} {% assign variants_with_metafield = product.variants.edges | map: "node" | where: "metafield" %} @@ -1026,44 +1026,48 @@ -- revert the prices on this variant and delete the metafield {% endcomment %} - {% capture variant_update %} - { - id: {{ variant.id | json}} - price: {{ metafield.original_price | json }} - {% if metafield.original_compare_at_price %}compareAtPrice: {{ metafield.original_compare_at_price | json }}{% endif %} - } - {% endcapture %} - + {% assign variant_update = hash %} + {% assign variant_update["id"] = variant.id %} + {% assign variant_update["price"] = metafield.original_price %} + {% if metafield.original_compare_at_price %} + {% assign variant_update["compareAtPrice"] = metafield.original_compare_at_price %} + {% endif %} {% assign variant_updates = variant_updates | push: variant_update %} - {% capture mutation %} - mutation { - metafieldDelete( - input: { - id: {{ variant.metafield.id | json }} - } - ) { - deletedId - userErrors { - field - message - } - } - } - {% endcapture %} - - {% assign mutations = mutations | push: mutation %} + {% assign metafield_to_delete = hash %} + {% assign metafield_to_delete["ownerId"] = variant.id %} + {% assign metafield_to_delete["namespace"] = variant_metafield_namespace | json %} + {% assign metafield_to_delete["key"] = variant_metafield_key | json %} + {% assign metafields_to_delete = metafields_to_delete | push: metafield_to_delete %} {% endif %} {% endfor %} + {% if metafields_to_delete != blank %} + {% action "shopify" %} + mutation { + metafieldsDelete( + metafields: {{ metafields_to_delete | graphql_arguments }} + ) { + deletedMetafields { + ownerId + namespace + key + } + userErrors { + field + message + } + } + } + {% endaction %} + {% endif %} + {% if variant_updates != blank %} {% action "shopify" %} mutation { productVariantsBulkUpdate( productId: {{ product.id | json }} - variants: [ - {{ variant_updates | join: newline }} - ] + variants: {{ variant_updates | graphql_arguments }} ) { product { id @@ -1084,10 +1088,6 @@ } {% endaction %} {% endif %} - - {% for mutation in mutations %} - {% action "shopify" mutation %} - {% endfor %} {% endfor %} {% if products_result.data.products.pageInfo.hasNextPage %} @@ -1267,7 +1267,7 @@ {% for product in products %} {% assign variant_updates = array %} - {% assign mutations = array %} + {% assign metafields_to_delete = array %} {% assign variants_with_metafield = product.variants.edges | map: "node" | where: "metafield" %} @@ -1284,43 +1284,47 @@ -- revert the prices on this variant and delete the metafield {% endcomment %} - {% capture variant_update %} - { - id: {{ variant.id | json}} - price: {{ metafield.original_price | json }} - {% if metafield.original_compare_at_price %}compareAtPrice: {{ metafield.original_compare_at_price | json }}{% endif %} - } - {% endcapture %} - + {% assign variant_update = hash %} + {% assign variant_update["id"] = variant.id %} + {% assign variant_update["price"] = metafield.original_price %} + {% if metafield.original_compare_at_price %} + {% assign variant_update["compareAtPrice"] = metafield.original_compare_at_price %} + {% endif %} {% assign variant_updates = variant_updates | push: variant_update %} - {% capture mutation %} + {% assign metafield_to_delete = hash %} + {% assign metafield_to_delete["ownerId"] = variant.id %} + {% assign metafield_to_delete["namespace"] = variant_metafield_namespace | json %} + {% assign metafield_to_delete["key"] = variant_metafield_key | json %} + {% assign metafields_to_delete = metafields_to_delete | push: metafield_to_delete %} + {% endfor %} + + {% if metafields_to_delete != blank %} + {% action "shopify" %} mutation { - metafieldDelete( - input: { - id: {{ variant.metafield.id | json }} - } + metafieldsDelete( + metafields: {{ metafields_to_delete | graphql_arguments }} ) { - deletedId + deletedMetafields { + ownerId + namespace + key + } userErrors { field message } } } - {% endcapture %} - - {% assign mutations = mutations | push: mutation %} - {% endfor %} + {% endaction %} + {% endif %} {% if variant_updates != blank %} {% action "shopify" %} mutation { productVariantsBulkUpdate( productId: {{ product.id | json }} - variants: [ - {{ variant_updates | join: newline }} - ] + variants: {{ variant_updates | graphql_arguments }} ) { product { id @@ -1341,10 +1345,6 @@ } {% endaction %} {% endif %} - - {% for mutation in mutations %} - {% action "shopify" mutation %} - {% endfor %} {% endfor %} {% if products_result.data.products.pageInfo.hasNextPage %} @@ -1360,12 +1360,20 @@ {% action "shopify" %} mutation { - metafieldDelete( - input: { - id: {{ shop.metafield.id | json }} - } + metafieldsDelete( + metafields: [ + { + ownerId: {{ shop.id | json }} + namespace: {{ shop_metafield_namespace | json }} + key: {{ shop_metafield_key | json }} + } + ] ) { - deletedId + deletedMetafields { + ownerId + namespace + key + } userErrors { field message diff --git a/docs/auto-remove-a-customer-tag-x-days-after-its-added/README.md b/docs/auto-remove-a-customer-tag-x-days-after-its-added/README.md index 344dbd25..76e91a6d 100644 --- a/docs/auto-remove-a-customer-tag-x-days-after-its-added/README.md +++ b/docs/auto-remove-a-customer-tag-x-days-after-its-added/README.md @@ -2,7 +2,7 @@ Tags: Schedule, Untag -Use this this task to monitor for the addition of a new customer tag, and to schedule the customer to be untagged some number of days later. Useful for granting temporary access to discounts, or other resources. +Use this this task to monitor for the addition of a specific customer tag, and to schedule the customer to be untagged a configurable number of days later. Useful for granting temporary access to discounts, or other resources. * View in the task library: [tasks.mechanic.dev/auto-remove-a-customer-tag-x-days-after-its-added](https://tasks.mechanic.dev/auto-remove-a-customer-tag-x-days-after-its-added) * Task JSON, for direct import: [task.json](../../tasks/auto-remove-a-customer-tag-x-days-after-its-added.json) @@ -30,13 +30,11 @@ user/task/untag_customer ## Documentation -Use this this task to monitor for the addition of a new customer tag, and to schedule the customer to be untagged some number of days later. Useful for granting temporary access to discounts, or other resources. +Use this this task to monitor for the addition of a specific customer tag, and to schedule the customer to be untagged a configurable number of days later. Useful for granting temporary access to discounts, or other resources. -This task monitors new and updated customers, watching for the configured customer tag. - -As soon as that tag is detected, the task will _add_ a second tag, indicating that the customer is scheduled to be untagged. (For example, if the task is configured to watch for the tag "Approved", the task will _add_ the tag "Approved - will be auto-removed by Mechanic".) The task will then schedule a followup event for the future, according to the configured number of days to wait. At that time, the task will remove both tags. - -Important note: To _prevent_ the task from untagging the customer later, manually remove the task's additional tag (i.e. the "will be auto-removed by Mechanic" tag). If the additional tag is found missing, the task will leave the tag in place instead of auto-removing it. +As soon as that tag is detected, the task will _add_ a second tag, indicating that the customer is scheduled to be untagged. (For example, if the task is configured to watch for the tag "Approved", the task will _add_ the tag "Approved - will be auto-removed by Mechanic".) The task will then schedule a followup event for the future, according to the configured number of days to wait. At that time, the task will remove both tags. + +Important note: To _prevent_ the task from untagging the customer later, manually remove the task's additional tag (i.e. the "will be auto-removed by Mechanic" tag). ## Installing this task diff --git a/docs/auto-remove-a-customer-tag-x-days-after-its-added/script.liquid b/docs/auto-remove-a-customer-tag-x-days-after-its-added/script.liquid index 23760508..b6a51b78 100644 --- a/docs/auto-remove-a-customer-tag-x-days-after-its-added/script.liquid +++ b/docs/auto-remove-a-customer-tag-x-days-after-its-added/script.liquid @@ -1,15 +1,16 @@ +{% assign tag_to_monitor = options.tag_to_monitor__required %} +{% assign tag_removal_interval_s = options.days_to_wait_before_untagging__number_required | times: 24 | times: 60 | times: 60 | round %} + +{% assign untag_flag_tag = tag_to_monitor | append: " - will be auto-removed by Mechanic" %} +{% assign now_s = "now" | date: "%s" | times: 1 %} +{% assign metafield_key = task.id | sha256 | slice: 0, 7 %} + {% if event.topic == "user/task/untag_customer" %} {% assign customer_id = event.data.customer_id %} {% else %} {% assign customer_id = customer.admin_graphql_api_id %} {% endif %} -{% assign tag = options.tag_to_monitor__required %} -{% assign untag_flag_tag = tag | append: " - will be auto-removed by Mechanic" %} -{% assign tag_removal_interval_s = options.days_to_wait_before_untagging__number_required | times: 24 | times: 60 | times: 60 | round %} -{% assign now_s = "now" | date: "%s" | times: 1 %} -{% assign metafield_key = task.id | sha256 | slice: 0, 7 %} - {% capture query %} query { customer(id: {{ customer_id | json }}) { @@ -34,8 +35,7 @@ "data": { "customer": { "id": "gid://shopify/Customer/1234567890", - "tags": [{{ tag | json }}], - "metafield": null + "tags": {{ tag_to_monitor | json }} } } } @@ -46,10 +46,10 @@ {% assign customer = result.data.customer %} -{% if customer.tags contains tag %} +{% if customer.tags contains tag_to_monitor %} {% assign time_to_remove_s = customer.metafield.value | times: 1 %} - {% if customer.metafield == nil or time_to_remove_s == 0 %} + {% if customer.metafield == blank or time_to_remove_s == 0 %} {% action "shopify" %} mutation { tagsAdd( @@ -67,28 +67,32 @@ } } - customerUpdate( - input: { - id: {{ customer.id | json }} - metafields: [ - { - namespace: "mechanic" - key: {{ metafield_key | json }} - value: {{ now_s | plus: tag_removal_interval_s | append: "" | json }} - type: "number_integer" - } - ] - } - ) { - customer { - metafield( + metafieldsSet( + metafields: [ + { + ownerId: {{ customer.id | json }} namespace: "mechanic" key: {{ metafield_key | json }} - ) { - id + value: {{ now_s | plus: tag_removal_interval_s | append: "" | json }} + type: "number_integer" + } + ] + ) { + metafields { + id + namespace + key + type + value + owner { + ... on Customer { + id + displayName + } } } userErrors { + code field message } @@ -106,16 +110,31 @@ "task_id": {{ task.id | json }} } {% endaction %} + {% elsif customer.tags contains untag_flag_tag %} {% if now_s < time_to_remove_s %} - {% log message: "This customer is scheduled to be untagged, but it's not time yet. Skipping.", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %} + {% log + message: "This customer is scheduled to be untagged, but it's not time yet. Skipping.", + tag_to_monitor: tag_to_monitor, + untag_flag_tag: untag_flag_tag, + now_s: now_s, + time_to_remove_s: time_to_remove_s + %} + {% else %} - {% log message: "This customer is scheduled to be untagged, and that time is now.", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %} + {% log + message: "This customer is scheduled to be untagged, and that time is now.", + tag_to_monitor: tag_to_monitor, + untag_flag_tag: untag_flag_tag, + now_s: now_s, + time_to_remove_s: time_to_remove_s + %} + {% action "shopify" %} mutation { tagsRemove( id: {{ customer.id | json }} - tags: [{{ tag | json }}, {{ untag_flag_tag | json }}] + tags: {{ array | push: tag_to_monitor, untag_flag_tag | json }} ) { node { ... on Customer { @@ -128,11 +147,20 @@ } } - metafieldDelete( - input: { - id: {{ customer.metafield.id | json }} - } + metafieldsDelete( + metafields: [ + { + ownerId: {{ customer.id | json }} + namespace: "mechanic" + key: {{ metafield_key | json }} + } + ] ) { + deletedMetafields { + ownerId + namespace + key + } userErrors { field message @@ -141,9 +169,21 @@ } {% endaction %} {% endif %} + {% else %} - {% log message: "This customer has a tag removal time recorded, but the 'untag flag tag' has been removed. Skipping.", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %} + {% log + message: "This customer has a tag removal time recorded, but the 'untag flag tag' has been removed. Skipping.", + tag_to_monitor: tag_to_monitor, + untag_flag_tag: untag_flag_tag, + now_s: now_s, + time_to_remove_s: time_to_remove_s + %} {% endif %} + {% elsif event.topic == "user/task/untag_customer" %} - {% log message: "The tag auto-removal event has arrived, but the customer has already been untagged by someone/something else. Skipping.", tag: tag, customer_tags: customer.tags %} + {% log + message: "The tag auto-removal event has arrived, but the customer has already been untagged by someone/something else. Skipping.", + tag_to_monitor: tag_to_monitor, + customer_tags: customer.tags + %} {% endif %} diff --git a/docs/auto-remove-a-product-tag-x-days-after-its-added/README.md b/docs/auto-remove-a-product-tag-x-days-after-its-added/README.md index 9b9a21a1..0949ddad 100644 --- a/docs/auto-remove-a-product-tag-x-days-after-its-added/README.md +++ b/docs/auto-remove-a-product-tag-x-days-after-its-added/README.md @@ -2,7 +2,7 @@ Tags: Auto-Tag, Schedule, Untag -Use this this task to monitor for the addition of a new product tag, and to schedule the product to be untagged some number of days later. Useful for temporarily adding a product to a collection, or qualifying the product for some other temporary functionality. +Use this this task to monitor for the addition of a specific product tag, and to schedule the product to be untagged a configurable number of days later. Useful for temporarily adding a product to a collection, or qualifying the product for some other temporary functionality. * View in the task library: [tasks.mechanic.dev/auto-remove-a-product-tag-x-days-after-its-added](https://tasks.mechanic.dev/auto-remove-a-product-tag-x-days-after-its-added) * Task JSON, for direct import: [task.json](../../tasks/auto-remove-a-product-tag-x-days-after-its-added.json) @@ -30,13 +30,11 @@ user/task/untag_product ## Documentation -Use this this task to monitor for the addition of a new product tag, and to schedule the product to be untagged some number of days later. Useful for temporarily adding a product to a collection, or qualifying the product for some other temporary functionality. +Use this this task to monitor for the addition of a specific product tag, and to schedule the product to be untagged a configurable number of days later. Useful for temporarily adding a product to a collection, or qualifying the product for some other temporary functionality. -This task monitors new and updated products, watching for the configured product tag. - -As soon as that tag is detected, the task will _add_ a second tag, indicating that the product is scheduled to be untagged. (For example, if the task is configured to watch for the tag "Approved", the task will _add_ the tag "Approved - will be auto-removed by Mechanic".) The task will then schedule a followup event for the future, according to the configured number of days to wait. At that time, the task will remove both tags. - -Important note: To _prevent_ the task from untagging the product later, manually remove the task's additional tag (i.e. the "will be auto-removed by Mechanic" tag). If the additional tag is found missing, the task will leave the tag in place instead of auto-removing it. +As soon as that tag is detected, the task will _add_ a second tag, indicating that the product is scheduled to be untagged. (For example, if the task is configured to watch for the tag "Approved", the task will _add_ the tag "Approved - will be auto-removed by Mechanic".) The task will then schedule a followup event for the future, according to the configured number of days to wait. At that time, the task will remove both tags. + +Important note: To _prevent_ the task from untagging the product later, manually remove the task's additional tag (i.e. the "will be auto-removed by Mechanic" tag). ## Installing this task diff --git a/docs/auto-remove-a-product-tag-x-days-after-its-added/script.liquid b/docs/auto-remove-a-product-tag-x-days-after-its-added/script.liquid index 104a02f9..0d6e3281 100644 --- a/docs/auto-remove-a-product-tag-x-days-after-its-added/script.liquid +++ b/docs/auto-remove-a-product-tag-x-days-after-its-added/script.liquid @@ -1,15 +1,16 @@ +{% assign tag_to_monitor = options.tag_to_monitor__required %} +{% assign tag_removal_interval_s = options.days_to_wait_before_untagging__number_required | times: 24 | times: 60 | times: 60 | round %} + +{% assign untag_flag_tag = tag_to_monitor | append: " - will be auto-removed by Mechanic" %} +{% assign now_s = "now" | date: "%s" | times: 1 %} +{% assign metafield_key = task.id | sha256 | slice: 0, 7 %} + {% if event.topic == "user/task/untag_product" %} {% assign product_id = event.data.product_id %} {% else %} {% assign product_id = product.admin_graphql_api_id %} {% endif %} -{% assign tag = options.tag_to_monitor__required %} -{% assign untag_flag_tag = tag | append: " - will be auto-removed by Mechanic" %} -{% assign tag_removal_interval_s = options.days_to_wait_before_untagging__number_required | times: 24 | times: 60 | times: 60 | round %} -{% assign now_s = "now" | date: "%s" | times: 1 %} -{% assign metafield_key = task.id | sha256 | slice: 0, 7 %} - {% capture query %} query { product(id: {{ product_id | json }}) { @@ -34,8 +35,7 @@ "data": { "product": { "id": "gid://shopify/Product/1234567890", - "tags": [{{ tag | json }}], - "metafield": null + "tags": {{ tag_to_monitor | json }} } } } @@ -46,10 +46,10 @@ {% assign product = result.data.product %} -{% if product.tags contains tag %} +{% if product.tags contains tag_to_monitor %} {% assign time_to_remove_s = product.metafield.value | times: 1 %} - {% if product.metafield == nil or time_to_remove_s == 0 %} + {% if product.metafield == blank or time_to_remove_s == 0 %} {% action "shopify" %} mutation { tagsAdd( @@ -67,28 +67,32 @@ } } - productUpdate( - input: { - id: {{ product.id | json }} - metafields: [ - { - namespace: "mechanic" - key: {{ metafield_key | json }} - value: {{ now_s | plus: tag_removal_interval_s | append: "" | json }} - type: "number_integer" - } - ] - } - ) { - product { - metafield( + metafieldsSet( + metafields: [ + { + ownerId: {{ product.id | json }} namespace: "mechanic" key: {{ metafield_key | json }} - ) { - id + value: {{ now_s | plus: tag_removal_interval_s | append: "" | json }} + type: "number_integer" + } + ] + ) { + metafields { + id + namespace + key + type + value + owner { + ... on Product { + id + title + } } } userErrors { + code field message } @@ -106,16 +110,31 @@ "task_id": {{ task.id | json }} } {% endaction %} + {% elsif product.tags contains untag_flag_tag %} {% if now_s < time_to_remove_s %} - {% log message: "This product is scheduled to be untagged, but it's not time yet. Skipping.", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %} + {% log + message: "This product is scheduled to be untagged, but it's not time yet. Skipping.", + tag_to_monitor: tag_to_monitor, + untag_flag_tag: untag_flag_tag, + now_s: now_s, + time_to_remove_s: time_to_remove_s + %} + {% else %} - {% log message: "This product is scheduled to be untagged, and that time is now.", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %} + {% log + message: "This product is scheduled to be untagged, and that time is now.", + tag_to_monitor: tag_to_monitor, + untag_flag_tag: untag_flag_tag, + now_s: now_s, + time_to_remove_s: time_to_remove_s + %} + {% action "shopify" %} mutation { tagsRemove( id: {{ product.id | json }} - tags: [{{ tag | json }}, {{ untag_flag_tag | json }}] + tags: {{ array | push: tag_to_monitor, untag_flag_tag | json }} ) { node { ... on Product { @@ -128,11 +147,20 @@ } } - metafieldDelete( - input: { - id: {{ product.metafield.id | json }} - } + metafieldsDelete( + metafields: [ + { + ownerId: {{ product.id | json }} + namespace: "mechanic" + key: {{ metafield_key | json }} + } + ] ) { + deletedMetafields { + ownerId + namespace + key + } userErrors { field message @@ -141,9 +169,21 @@ } {% endaction %} {% endif %} + {% else %} - {% log message: "This product has a tag removal time recorded, but the 'untag flag tag' has been removed. Skipping.", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %} + {% log + message: "This product has a tag removal time recorded, but the 'untag flag tag' has been removed. Skipping.", + tag_to_monitor: tag_to_monitor, + untag_flag_tag: untag_flag_tag, + now_s: now_s, + time_to_remove_s: time_to_remove_s + %} {% endif %} + {% elsif event.topic == "user/task/untag_product" %} - {% log message: "The tag auto-removal event has arrived, but the product has already been untagged by someone/something else. Skipping.", tag: tag, product_tags: product.tags %} + {% log + message: "The tag auto-removal event has arrived, but the product has already been untagged by someone/something else. Skipping.", + tag_to_monitor: tag_to_monitor, + product_tags: product.tags + %} {% endif %} diff --git a/docs/auto-tag-new-products-by-back-in-stock-age/script.liquid b/docs/auto-tag-new-products-by-back-in-stock-age/script.liquid index 9ff53c8c..582d2a08 100644 --- a/docs/auto-tag-new-products-by-back-in-stock-age/script.liquid +++ b/docs/auto-tag-new-products-by-back-in-stock-age/script.liquid @@ -253,19 +253,19 @@ {% log product: product, back_in_stock_inventory_level: back_in_stock_inventory_level %} - {% assign metafield_inputs = array %} + {% assign metafields_to_set = array %} {% comment %} -- save the first seen time in a metafield if it doesn't already exist {% endcomment %} {% if product.back_in_stock_first_seen_metafield == blank %} - {% assign metafield_input = hash %} - {% assign metafield_input["namespace"] = "mechanic" %} - {% assign metafield_input["key"] = "back_in_stock_first_seen_s" %} - {% assign metafield_input["type"] = "number_integer" %} - {% assign metafield_input["value"] = now_s %} - {% assign metafield_inputs = metafield_inputs | push: metafield_input %} + {% assign metafield_to_set = hash %} + {% assign metafield_to_set["namespace"] = "mechanic" %} + {% assign metafield_to_set["key"] = "back_in_stock_first_seen_s" %} + {% assign metafield_to_set["type"] = "number_integer" %} + {% assign metafield_to_set["value"] = now_s %} + {% assign metafields_to_set = metafields_to_set | push: metafield_to_set %} {% endif %} {% if product.totalInventory >= back_in_stock_inventory_level %} @@ -274,12 +274,12 @@ {% endcomment %} {% if product.back_in_stock_metafield == blank %} - {% assign metafield_input = hash %} - {% assign metafield_input["namespace"] = "mechanic" %} - {% assign metafield_input["key"] = "back_in_stock_s" %} - {% assign metafield_input["type"] = "number_integer" %} - {% assign metafield_input["value"] = now_s %} - {% assign metafield_inputs = metafield_inputs | push: metafield_input %} + {% assign metafield_to_set = hash %} + {% assign metafield_to_set["namespace"] = "mechanic" %} + {% assign metafield_to_set["key"] = "back_in_stock_s" %} + {% assign metafield_to_set["type"] = "number_integer" %} + {% assign metafield_to_set["value"] = now_s %} + {% assign metafields_to_set = metafields_to_set | push: metafield_to_set %} {% endif %} {% elsif product.back_in_stock_metafield != blank %} @@ -289,11 +289,20 @@ {% action "shopify" %} mutation { - metafieldDelete( - input: { - id: {{ product.back_in_stock_metafield.id | json }} - } + metafieldsDelete( + metafields: [ + { + ownerId: {{ product.id | json }} + namespace: "mechanic" + key: "back_in_stock_s" + } + ] ) { + deletedMetafields { + ownerId + namespace + key + } userErrors { field message @@ -303,16 +312,27 @@ {% endaction %} {% endif %} - {% if metafield_inputs != blank %} + {% if metafields_to_set != blank %} {% action "shopify" %} mutation { - productUpdate( - input: { - id: {{ product.id | json }} - metafields: {{ metafield_inputs | graphql_arguments }} - } + metafieldsSet( + metafields: {{ metafields_to_set | graphql_arguments }} ) { + metafields { + id + namespace + key + type + value + owner { + ... on Product { + id + title + } + } + } userErrors { + code field message } diff --git a/docs/temporarily-enable-tax-exempt-status-when-a-customer-is-tagged/script.liquid b/docs/temporarily-enable-tax-exempt-status-when-a-customer-is-tagged/script.liquid index b1711db8..b92f918c 100644 --- a/docs/temporarily-enable-tax-exempt-status-when-a-customer-is-tagged/script.liquid +++ b/docs/temporarily-enable-tax-exempt-status-when-a-customer-is-tagged/script.liquid @@ -21,6 +21,7 @@ {% assign customer_qualifies = false %} {% assign customer_tags = customer.tags | split: ", " %} + {% if customer_tags contains options.tax_exempt_tag__required %} {% if customer.metafields[metafield_namespace][metafield_key] != nil %} {% log "Customer already has their metafield set - skipping" %} @@ -76,6 +77,7 @@ } {% endaction %} {% endif %} + {% elsif event.topic == expiry_event_topic %} {% capture customer_query %} query { @@ -148,11 +150,20 @@ } } - metafieldDelete( - input: { - id: {{ customer_result.data.customer.metafield.id | json }} - } + metafieldsDelete( + metafields: [ + { + ownerId: {{ customer.id | json }} + namespace: {{ metafield_namespace | json }} + key: {{ metafield_key | json }} + } + ] ) { + deletedMetafields { + ownerId + namespace + key + } userErrors { field message diff --git a/docs/unpublish-products-that-have-been-out-of-stock-for-x-days/script.liquid b/docs/unpublish-products-that-have-been-out-of-stock-for-x-days/script.liquid index d3066615..0dbcc8db 100644 --- a/docs/unpublish-products-that-have-been-out-of-stock-for-x-days/script.liquid +++ b/docs/unpublish-products-that-have-been-out-of-stock-for-x-days/script.liquid @@ -1,10 +1,10 @@ {% comment %} 1. Watch for updates to inventory levels. 2. Look up the associated product, when an update comes in. - 3. If the product's total inventory is 0, record the current time in a metafield. If the + 3. If the product's total inventory is <=0, record the current time in a metafield. If the total inventory is not 0, delete that metafield, if it exists. 4. Scan all products, on a schedule, retrieving each product's out-of-stock time metafield. - For products found with a total inventory of 0, with a recorded time that's at least a + For products found with a total inventory of <=0, with a recorded time that's at least a configurable distance in days from the current time, unpublish the product. {% endcomment %} @@ -112,11 +112,20 @@ {% else %} {% action "shopify" %} mutation { - metafieldDelete( - input: { - id: {{ product.metafield.id | json }} - } + metafieldsDelete( + metafields: [ + { + ownerId: {{ product.id | json }} + namespace: "mechanic" + key: "out_of_stock_at" + } + ] ) { + deletedMetafields { + ownerId + namespace + key + } userErrors { field message diff --git a/tasks/advanced-scheduled-price-changes.json b/tasks/advanced-scheduled-price-changes.json index f5e3fb70..9b20652e 100644 --- a/tasks/advanced-scheduled-price-changes.json +++ b/tasks/advanced-scheduled-price-changes.json @@ -16,7 +16,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% assign shop_metafield_namespace = \"mechanic\" %}\n{% assign shop_metafield_key = \"price_change_events\" %}\n{% assign variant_metafield_namespace = \"mechanic\" %}\n{% assign variant_metafield_key = \"price_change_event\" %}\n\n{% assign email_recipients = options.notification_email_recipients__array_required %}\n\n{% capture task_admin_link -%}\n{{ task.name | remove: \"ADVANCED: \" }}\n{%- endcapture %}\n\n{% comment %}\n -- query for shop data on every task run, since it will be needed for each valid action keyword and custom event\n{% endcomment %}\n\n{% capture shop_query %}\n query {\n shop {\n id\n metafield(\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n{% endcapture %}\n\n{% assign shop_result = shop_query | shopify %}\n\n{% if event.preview %}\n {% capture shop_result_json %}\n {\n \"data\": {\n \"shop\": {\n \"id\": \"gid://shopify/Shop/1234567890\",\n \"metafield\": {\n \"id\": \"gid://shopify/Metafield/9876543210\",\n \"value\": \"\\n{\\n \\\"price_change_event__1234567890\\\": {\\n \\\"status\\\": \\\"scheduled\\\",\\n \\\"start\\\": \\\"2021-12-30 08:00\\\",\\n \\\"end\\\": \\\"2021-12-31 20:00\\\",\\n \\\"set_compare_at_prices\\\": true,\\n \\\"collection_handles_and_discounts\\\": {\\n \\\"collection-alpha\\\": \\\"20%\\\",\\n \\\"collection-beta\\\": \\\"-10\\\",\\n \\\"collection-gamma\\\": \\\"20\\\"\\n },\\n \\\"skus_to_include\\\": [],\\n \\\"sku_discount\\\": \\\"\\\",\\n \\\"skus_to_exclude\\\": [],\\n \\\"exclude_products_tagged_with\\\": [\\n \\\"clearance\\\"\\n ]\\n }\\n}\\n\"\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign shop_result = shop_result_json | parse_json %}\n{% endif %}\n\n{% assign shop = shop_result.data.shop %}\n{% assign price_change_events = shop.metafield.value | default: \"{}\" | parse_json %}\n\n{% if event.topic == \"mechanic/user/text\" %}\n {% comment %}\n -- check text entry to see what action to take\n {% endcomment %}\n\n {% assign action_keyword = event.data | downcase %}\n\n {% if event.preview %}\n {% assign action_keyword = \"schedule\" %}\n {% endif %}\n\n {% case action_keyword %}\n {% when \"schedule\" %}\n {% comment %}\n -- NOTE: run scheduling logic here instead of in custom event like most other keywords, so user gets immediate feedback on any configuration errors\n {% endcomment %}\n\n {% assign valid_start_datetime = options.event_start_datetime__required | parse_date: \"%Y-%m-%d %H:%M\" %}\n\n {% if valid_start_datetime == blank %}\n {% assign valid_start_datetime = options.event_start_datetime__required | parse_date: \"%Y-%m-%d\" %}\n\n {% if valid_start_datetime == blank %}\n {% unless event.preview %}\n {% error \"The event start date is not a valid date. Please re-enter it (per the task documentaion) and try scheduling the event again.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n {% endif %}\n\n {% assign valid_end_datetime = options.event_end_datetime__required | parse_date: \"%Y-%m-%d %H:%M\" %}\n\n {% if valid_end_datetime == blank %}\n {% assign valid_end_datetime = options.event_end_datetime__required | parse_date: \"%Y-%m-%d\" %}\n\n {% if valid_end_datetime == blank %}\n {% unless event.preview %}\n {% error \"The event end date is not a valid date. Please re-enter it (per the task documentaion) and try scheduling the event again.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n {% endif %}\n\n {% assign now = \"now\" | date: \"%s\" %}\n {% assign start_datetime_s = options.event_start_datetime__required | date: \"%s\" %}\n {% assign end_datetime_s = options.event_end_datetime__required | date: \"%s\" %}\n\n {% if start_datetime_s <= now or start_datetime_s >= end_datetime_s %}\n {% unless event.preview %}\n {% error \"The event start and end dates must be future dates and the start date must be before the end date. Please re-enter them (per the task documentaion) and try scheduling the event again.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% assign set_compare_at_prices = options.set_compare_at_prices_to_original_price_during_event__boolean %}\n {% assign collection_handles_and_discounts = options.collection_handles_and_discounts__keyval %}\n {% assign skus_to_include = options.skus_to_include__array %}\n {% assign sku_discount = options.sku_discount %}\n {% assign exclude_products_tagged_with = options.exclude_products_tagged_with__array %}\n {% assign skus_to_exclude = options.skus_to_exclude__array %}\n\n {% comment %}\n -- validate discount entry formats: XX% (percentage discount), X.XX (fixed price), or -X.XX (fixed discount)\n {% endcomment %}\n\n {% assign discounts = collection_handles_and_discounts | values | default: array %}\n\n {% if skus_to_include != blank and sku_discount == blank %}\n {% unless event.preview %}\n {% error \"A SKU discount must be configured when specific SKUs are included.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% if sku_discount != blank %}\n {% assign discounts = discounts | push: sku_discount %}\n {% endif %}\n\n {% for discount in discounts %}\n {% if discount contains \"%\" %}\n {% assign discount_percentage = discount | times: 1 %}\n {% assign discount_string = discount_percentage | append: \"%\" %}\n\n {% if discount != discount_string %}\n {% unless event.preview %}\n {% error\n message: \"Percentage discount entry incorrect. Please update the task configuration (per the task documentaion) and try scheduling the event again.\",\n discount: discount\n %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% if discount_percentage <= 0 or discount_percentage >= 100 %}\n {% unless event.preview %}\n {% error\n message: \"Percentage discount entries should contain an number between 0 and 100. Please update the task configuration (per the task documentaion) and try scheduling the event again.\",\n discount: discount\n %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% else %}\n {% assign discount_absolute = discount | times: 1 | abs %}\n\n {% if discount_absolute == 0 %}\n {% unless event.preview %}\n {% error\n message: \"Fixed price and fixed discount entries must contain an absolute number > 0. Please update the task configuration (per the task documentaion) and try scheduling the event again.\",\n discount: discount\n %}\n {% break %}\n {% endunless %}\n {% endif %}\n {% endif %}\n {% endfor %}\n\n {% assign price_change_event = hash %}\n\n {% assign price_change_event[\"status\"] = \"scheduled\" %}\n {% assign price_change_event[\"start\"] = start_datetime_s %}\n {% assign price_change_event[\"end\"] = end_datetime_s %}\n {% assign price_change_event[\"set_compare_at_prices\"] = set_compare_at_prices %}\n {% assign price_change_event[\"collection_handles_and_discounts\"] = collection_handles_and_discounts %}\n {% assign price_change_event[\"skus_to_include\"] = skus_to_include %}\n {% assign price_change_event[\"sku_discount\"] = sku_discount %}\n {% assign price_change_event[\"skus_to_exclude\"] = skus_to_exclude %}\n {% assign price_change_event[\"exclude_products_tagged_with\"] = exclude_products_tagged_with %}\n\n {% log\n task_options: task.options,\n price_change_event: price_change_event\n %}\n\n {% assign price_change_event_id = event.id | default: task.id %}\n\n {% assign price_change_events[price_change_event_id] = price_change_event %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n value: {{ price_change_events | json | json }}\n type: \"json\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Shop {\n id\n name\n myshopifyDomain\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% comment %}\n -- schedule start and end events\n {% endcomment %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/price_changes/start\",\n \"task_id\": {{ task.id | json }},\n \"run_at\": {{ start_datetime_s | json }},\n \"data\": {\n \"price_change_event_id\": {{ price_change_event_id | json }}\n }\n }\n {% endaction %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/price_changes/end\",\n \"task_id\": {{ task.id | json }},\n \"run_at\": {{ end_datetime_s | json }},\n \"data\": {\n \"price_change_event_id\": {{ price_change_event_id | json }}\n }\n }\n {% endaction %}\n\n {% capture email_subject %}New price change event scheduled ({{ price_change_event_id }}){% endcapture %}\n\n {% capture email_body %}\n A new price change event has been scheduled, from the {{ task_admin_link }} task within the Mechanic app.\n\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event[\"status\"] }}\n Event start: {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n\n Note: To cancel this event while it is still scheduled or ongoing, use the \"cancel\" keyword along with the price change event ID when running the task.\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n {% when \"list\" %}\n {% action \"echo\" price_change_events: price_change_events %}\n\n {% when \"email\" %}\n {% if price_change_events == blank %}\n {% assign email_body = \"There are currently no configured price change events\" %}\n\n {% else %}\n {% capture email_body %}\n Currently configured price change events, using the {{ task_admin_link }} task within the Mechanic app.\n\n {% for price_change_event in price_change_events -%}\n {%- assign price_change_event_id = price_change_event[0] -%}\n {%- assign price_change_event_data = price_change_event[1] -%}\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event_data[\"status\"] }}\n Event start: {{ price_change_event_data[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event_data[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event_data[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event_data[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event_data[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event_data[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event_data[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event_data[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n
\n {%- endfor %}\n {% endcapture %}\n {% endif %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": \"All price change events as of {{ \"now\" | date: \"%F\" }}\",\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n {% when \"reset\" %}\n {% action \"event\" %}\n {\n \"topic\": \"user/price_changes/reset\",\n \"task_id\": {{ task.id | json }}\n }\n {% endaction %}\n\n {% else %}\n {% if action_keyword contains \"cancel\" %}\n {% assign price_change_event_id = action_keyword | remove: \"cancel \" %}\n {% assign price_change_event = price_change_events[price_change_event_id] %}\n\n {% if price_change_event == blank %}\n {% error \"Keyword of 'cancel' entered with an invalid price change event ID. Run task with 'list' keyword to see a list of all configured price change events.\" %}\n {% break %}\n {% endif %}\n\n {% if price_change_event.status == \"scheduled\" %}\n {% comment %}\n -- price change event has not started, so can just update it to cancelled and when the start/end events run they will ignore this event\n {% endcomment %}\n\n {% assign price_change_event[\"status\"] = \"cancelled\" %}\n {% assign price_change_events[price_change_event_id] = price_change_event %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n value: {{ price_change_events | json | json }}\n type: \"json\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Shop {\n id\n name\n myshopifyDomain\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% capture email_subject %}Scheduled price change event has been cancelled ({{ price_change_event_id }}){% endcapture %}\n\n {% capture email_body %}\n A scheduled price change event has been cancelled, from the {{ task_admin_link }} task within the Mechanic app.\n\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event[\"status\"] }}\n Event start: {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n {% elsif price_change_event.status == \"ongoing\" %}\n {% comment %}\n -- price change event in progress, so we have to revert the price changes to properly cancel it\n {% endcomment %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/price_changes/end\",\n \"task_id\": {{ task.id | json }},\n \"data\": {\n \"price_change_event_id\": {{ price_change_event_id | json }},\n \"cancel\": true\n }\n }\n {% endaction %}\n\n {% else %}\n {% action \"echo\"\n message: \"Price change event cannot be cancelled because it does not have a status of 'scheduled' or 'ongoing'\",\n price_change_event: price_change_event\n %}\n {% endif %}\n\n {% else %}\n {% error\n message: \"Unrecognized action keyword. Action keyword must be one of 'schedule', 'list', 'email', or 'cancel'\",\n action_keyword: action_keyword\n %}\n {% endif %}\n {% endcase %}\n\n{% elsif event.topic == \"user/price_changes/start\" %}\n {% assign price_change_event_id = event.data.price_change_event_id %}\n {% assign price_change_event = price_change_events[price_change_event_id] %}\n\n {% if event.preview %}\n {% assign price_change_event_id = \"01234567-89ab-cdef\" %}\n\n {% capture price_change_event_json %}\n {\n \"status\": \"scheduled\",\n \"start\": {{ \"now + 1 day\" | date: \"%s\" }},\n \"end\": {{ \"now + 2 days\" | date: \"%s\" }},\n \"set_compare_at_prices\": false,\n \"collection_handles_and_discounts\": {\n \"alpha-beta-gamma\": \"10%\",\n \"sticks-and-stones\": \"-5.50\",\n \"lorem-ipsum\": \"3\"\n },\n \"skus_to_include\": [\n \"SKU-123\",\n \"SKU-456\"\n ],\n \"sku_discount\": \"15%\",\n \"skus_to_exclude\": [\n \"SKU-987\"\n ],\n \"exclude_products_tagged_with\": [\n \"do-not-discount\"\n ]\n }\n {% endcapture %}\n\n {% assign price_change_event = price_change_event_json | parse_json %}\n {% endif %}\n\n {% assign status = price_change_event.status %}\n {% assign set_compare_at_prices = price_change_event.set_compare_at_prices %}\n {% assign collection_handles_and_discounts = price_change_event.collection_handles_and_discounts %}\n {% assign skus_to_include = price_change_event.skus_to_include %}\n {% assign sku_discount = price_change_event.sku_discount %}\n {% assign exclude_products_tagged_with = price_change_event.exclude_products_tagged_with %}\n {% assign skus_to_exclude = price_change_event.skus_to_exclude %}\n\n {% if status != \"scheduled\" %}\n {% log\n message: \"This price change event does not have a status of scheduled, and thus will not start.\",\n price_change_event: price_change_event\n %}\n {% break %}\n {% endif %}\n\n {% comment %}\n -- get collection ids for products query\n {% endcomment %}\n\n {% assign in_collection_checks = array %}\n\n {% for keyval in collection_handles_and_discounts %}\n {% capture collection_query %}\n query {\n collectionByHandle(handle: {{ keyval[0] | json }}) {\n id\n handle\n }\n }\n {% endcapture %}\n\n {% assign collection_result = collection_query | shopify %}\n {% assign collection = collection_result.data.collectionByHandle %}\n\n {% if collection != blank %}\n {% capture in_collection_check -%}\n inCollection_{{ collection.handle | replace: \"-\", \"_\" }}: inCollection(id: {{ collection.id | json }})\n {%- endcapture %}\n\n {% assign in_collection_checks = in_collection_checks | push: in_collection_check %}\n {% endif %}\n {% endfor %}\n\n {% assign query_filter = \"gift_card:false\" %}\n\n {% for tag in exclude_products_tagged_with %}\n {% assign query_filter = tag | json | prepend: \" tag_not:\" | prepend: query_filter %}\n {% endfor %}\n\n {% log query_filter: query_filter %}\n\n {% assign products = array %}\n\n {% assign cursor = nil %}\n\n {% for n in (1..10000) %}\n {% capture products_query %}\n query {\n products(\n first: 4\n after: {{ cursor | json }}\n query: {{ query_filter | json }}\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n title\n tags\n variants(first: 100) {\n edges {\n node {\n id\n title\n sku\n price\n compareAtPrice\n metafield(\n namespace: {{ variant_metafield_namespace | json }}\n key: {{ variant_metafield_key | json }}\n ) {\n value\n }\n }\n }\n }\n {{ in_collection_checks | join: newline }}\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_query | shopify %}\n\n {% if event.preview %}\n {% capture products_result_json %}\n {\n \"data\": {\n \"products\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"variants\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/ProductVariant/1234567890\",\n \"sku\": \"ACME-BRICK-RED\",\n \"price\": \"10.00\",\n \"compareAtPrice\": \"15.00\",\n \"metafield\": null\n }\n }\n ]\n },\n \"inCollection_{{ collection_handles_and_discounts.first.first | replace: \"-\", \"_\" | default: \"sample\" }}\": true\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_result_json | parse_json %}\n {% endif %}\n\n {% assign products = products_result.data.products.edges | map: \"node\" %}\n\n {% for product in products %}\n {% comment %}\n -- For each product, match to a collection discount if applicable, then, if needed, loop through variants to see if there are any overrides\n {% endcomment %}\n\n {% assign product_level_discount = nil %}\n\n {% for keyval in collection_handles_and_discounts %}\n {% assign in_collection_label\n = \"inCollection_\"\n | append: keyval[0]\n | replace: \"-\", \"_\"\n %}\n\n {% if product[in_collection_label] %}\n {% assign product_level_discount = keyval[1] %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if product_level_discount == blank\n and skus_to_include == blank\n and skus_to_exclude == blank\n %}\n {% log\n message: \"This product is not within any configured collections, nor does the price change event have any sku inclusion or exclusion settings; skipping. \",\n product: product\n %}\n {% continue %}\n {% endif %}\n\n {% assign variants = product.variants.edges | map: \"node\" %}\n\n {% assign variant_updates = array %}\n\n {% for variant in variants %}\n {% if skus_to_exclude != blank and skus_to_exclude contains variant.sku %}\n {% log\n message: \"This variant was excluded by SKU\",\n variant: variant\n %}\n {% continue %}\n {% endif %}\n\n {% if variant.metafield != blank %}\n {% log\n message: \"This variant is already part of a price change event; skipping.\",\n variant: variant\n %}\n {% continue %}\n {% endif %}\n\n {% assign discount_to_apply = product_level_discount %}\n\n {% if skus_to_include != blank and skus_to_include contains variant.sku %}\n {% assign discount_to_apply = sku_discount %}\n {% endif %}\n\n {% log\n product_title: product.title,\n sku: variant.sku,\n skus_to_include: skus_to_include,\n sku_discount: sku_discount,\n discount_to_apply: discount_to_apply\n %}\n\n {% if discount_to_apply == blank %}\n {% log\n message: \"This variant's product is not within any configured collections, nor has this variant been specifically included by sku; skipping.\",\n variant: variant\n %}\n {% continue %}\n {% endif %}\n\n {% assign price_to_set = nil %}\n {% assign compare_at_price_to_set = nil %}\n\n {% if discount_to_apply contains \"%\" %}\n {% assign price_to_set\n = discount_to_apply\n | remove: \"%\"\n | minus: 100\n | abs\n | times: variant.price\n | divided_by: 100\n | round: 2\n %}\n\n {% elsif discount_to_apply contains \"-\" %}\n {% assign price_to_set\n = variant.price\n | plus: discount_to_apply\n | at_least: 0.0\n %}\n\n {% else %}\n {% assign price_to_set = discount_to_apply %}\n {% endif %}\n\n {% if set_compare_at_prices %}\n {% assign compare_at_price_to_set = variant.price %}\n {% endif %}\n\n {% log\n product: product,\n product_level_discount: product_level_discount,\n variant: variant,\n discount_to_apply: discount_to_apply,\n price_to_set: price_to_set,\n compare_at_price_to_set: compare_at_price_to_set\n %}\n\n {% capture variant_metafield_value %}\n {\n \"price_change_event_id\": {{ price_change_event_id | json }},\n \"discount_to_apply\": {{ discount_to_apply | json }},\n {% if set_compare_at_prices %}\n \"original_compare_at_price\": {{ variant.compareAtPrice | json }},\n \"compare_at_price_to_set\": {{ compare_at_price_to_set | json }},\n {% endif %}\n \"original_price\": {{ variant.price | json }},\n \"price_to_set\": {{ price_to_set | json }}\n }\n {% endcapture %}\n\n {% capture variant_update %}\n {\n id: {{ variant.id | json}}\n price: {{ price_to_set | json }}\n {% if set_compare_at_prices %}compareAtPrice: {{ variant.price | json }}{% endif %}\n metafields: [\n {\n key: {{ variant_metafield_key | json }}\n namespace: {{ variant_metafield_namespace | json }}\n type: \"json\"\n value: {{ variant_metafield_value | json }}\n }\n ]\n }\n {% endcapture %}\n\n {% assign variant_updates = variant_updates | push: variant_update %}\n {% endfor %}\n\n {% if variant_updates != blank %}\n {% action \"shopify\" %}\n mutation {\n productVariantsBulkUpdate(\n productId: {{ product.id | json }}\n variants: [\n {{ variant_updates | join: newline }}\n ]\n ) {\n product {\n id\n title\n tags\n }\n productVariants {\n id\n title\n sku\n price\n compareAtPrice\n metafield(\n namespace: {{ variant_metafield_namespace | json }}\n key: {{ variant_metafield_key | json }}\n ) {\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n\n {% if products_result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = products_result.data.products.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- update the price change event\n {% endcomment %}\n\n {% assign price_change_event[\"status\"] = \"ongoing\" %}\n {% assign price_change_events[price_change_event_id] = price_change_event %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n value: {{ price_change_events | json | json }}\n type: \"json\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Shop {\n id\n name\n myshopifyDomain\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% capture email_subject %}A scheduled price change event has started ({{ price_change_event_id }}){% endcapture %}\n\n {% capture email_body %}\n A scheduled price change event has started, using the {{ task_admin_link }} task within the Mechanic app.\n\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event[\"status\"] }}\n Event start: {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n\n Note: To cancel this event while it is ongoing, use the \"cancel\" keyword along with the price change event ID when running the task.\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n{% elsif event.topic == \"user/price_changes/end\" %}\n {% assign price_change_event_id = event.data.price_change_event_id %}\n {% assign price_change_event = price_change_events[price_change_event_id] %}\n\n {% if event.preview %}\n {% assign price_change_event_id = \"01234567-89ab-cdef\" %}\n {% assign price_change_event = hash %}\n {% assign price_change_event[\"status\"] = \"ongoing\" %}\n {% endif %}\n\n {% if event.data.cancel %}\n {% comment %}\n -- go ahead and cancel, as status was already checked to be \"scheduled\" prior to custom event call\n {% endcomment %}\n\n {% assign price_change_event[\"status\"] = \"cancelled\" %}\n\n {% else %}\n {% comment %}\n -- since this was a scheduled run, need to make sure the event status is \"ongoing\" before reverting changes\n {% endcomment %}\n\n {% if price_change_event.status != \"ongoing\" %}\n {% log\n message: \"This price change event does not have a status of 'ongoing', and thus will not be reverted.\",\n price_change_event: price_change_event\n %}\n {% break %}\n {% endif %}\n\n {% assign price_change_event[\"status\"] = \"completed\" %}\n {% endif %}\n\n {% comment %}\n -- To revert the price change event, check every variant in the shop to see if the metafield exists and contains this price change event ID\n -- Note: Query from the product level so that productVariantsBulkUpdate can be used, causing only one product update event to fire\n {% endcomment %}\n\n {% assign products = array %}\n\n {% assign cursor = nil %}\n\n {% for n in (1..10000) %}\n {% capture products_query %}\n query {\n products(\n first: 4\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n title\n tags\n variants(first: 100) {\n edges {\n node {\n id\n title\n sku\n price\n compareAtPrice\n metafield(\n namespace: {{ variant_metafield_namespace | json }}\n key: {{ variant_metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_query | shopify %}\n\n {% if event.preview %}\n {% capture products_result_json %}\n {\n \"data\": {\n \"products\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"variants\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/ProductVariant/1234567890\",\n \"sku\": \"ACME-BRICK-RED\",\n \"price\": \"7.50\",\n \"compareAtPrice\": \"10.00\",\n \"metafield\": {\n \"id\": \"gid://shopify/Metafield/9876543210\",\n \"value\": \"\\n{\\n \\\"price_change_event_id\\\": \\\"01234567-89ab-cdef\\\",\\n \\\"discount_to_apply\\\": \\\"25%\\\",\\n \\\"original_compare_at_price\\\": \\\"15.00\\\",\\n \\\"compare_at_price_to_set\\\": 7.50,\\n \\\"original_price\\\": \\\"10.00\\\",\\n \\\"price_to_set\\\": 7.50\\n}\\n\"\n }\n }\n }\n ]\n }\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_result_json | parse_json %}\n {% endif %}\n\n {% assign products = products_result.data.products.edges | map: \"node\" %}\n\n {% for product in products %}\n {% assign variant_updates = array %}\n {% assign mutations = array %}\n\n {% assign variants_with_metafield = product.variants.edges | map: \"node\" | where: \"metafield\" %}\n\n {% for variant in variants_with_metafield %}\n {% assign metafield = variant.metafield.value | parse_json %}\n\n {% if metafield.price_change_event_id == price_change_event_id %}\n {% comment %}\n -- revert the prices on this variant and delete the metafield\n {% endcomment %}\n\n {% capture variant_update %}\n {\n id: {{ variant.id | json}}\n price: {{ metafield.original_price | json }}\n {% if metafield.original_compare_at_price %}compareAtPrice: {{ metafield.original_compare_at_price | json }}{% endif %}\n }\n {% endcapture %}\n\n {% assign variant_updates = variant_updates | push: variant_update %}\n\n {% capture mutation %}\n mutation {\n metafieldDelete(\n input: {\n id: {{ variant.metafield.id | json }}\n }\n ) {\n deletedId\n userErrors {\n field\n message\n }\n }\n }\n {% endcapture %}\n\n {% assign mutations = mutations | push: mutation %}\n {% endif %}\n {% endfor %}\n\n {% if variant_updates != blank %}\n {% action \"shopify\" %}\n mutation {\n productVariantsBulkUpdate(\n productId: {{ product.id | json }}\n variants: [\n {{ variant_updates | join: newline }}\n ]\n ) {\n product {\n id\n title\n }\n productVariants {\n id\n displayName\n sku\n price\n compareAtPrice\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% for mutation in mutations %}\n {% action \"shopify\" mutation %}\n {% endfor %}\n {% endfor %}\n\n {% if products_result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = products_result.data.products.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- update the price change event\n {% endcomment %}\n\n {% assign price_change_events[price_change_event_id] = price_change_event %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n value: {{ price_change_events | json | json }}\n type: \"json\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Shop {\n id\n name\n myshopifyDomain\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% comment %}\n -- send email notification about the price change event status change\n {% endcomment %}\n\n {% capture email_subject %}A price change event has been {{ price_change_event[\"status\"] }} ({{ price_change_event_id }}){% endcapture %}\n\n {% capture email_body %}\n A price change event has been {{ price_change_event[\"status\"] }}, using the {{ task_admin_link }} task within the Mechanic app.\n\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event[\"status\"] }}\n Event start: {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n{% elsif event.topic == \"user/price_changes/reset\" %}\n {% comment %}\n -- To reset price change events, check every variant in the shop to see if the price change event metafield exists\n -- Note: Query from the product level so that productVariantsBulkUpdate can be used, causing only one product update event to fire\n {% endcomment %}\n\n {% assign products = array %}\n\n {% assign cursor = nil %}\n\n {% for n in (1..10000) %}\n {% capture products_query %}\n query {\n products(\n first: 4\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n title\n tags\n variants(first: 100) {\n edges {\n node {\n id\n title\n sku\n price\n compareAtPrice\n metafield(\n namespace: {{ variant_metafield_namespace | json }}\n key: {{ variant_metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_query | shopify %}\n\n {% if event.preview %}\n {% capture products_result_json %}\n {\n \"data\": {\n \"products\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"variants\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/ProductVariant/1234567890\",\n \"sku\": \"ACME-BRICK-RED\",\n \"price\": \"7.50\",\n \"compareAtPrice\": \"10.00\",\n \"metafield\": {\n \"id\": \"gid://shopify/Metafield/9876543210\",\n \"value\": \"\\n{\\n \\\"price_change_event_id\\\": \\\"01234567-89ab-cdef\\\",\\n \\\"discount_to_apply\\\": \\\"25%\\\",\\n \\\"original_compare_at_price\\\": \\\"15.00\\\",\\n \\\"compare_at_price_to_set\\\": 7.50,\\n \\\"original_price\\\": \\\"10.00\\\",\\n \\\"price_to_set\\\": 7.50\\n}\\n\"\n }\n }\n }\n ]\n }\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_result_json | parse_json %}\n {% endif %}\n\n {% assign products = products_result.data.products.edges | map: \"node\" %}\n\n {% for product in products %}\n {% assign variant_updates = array %}\n {% assign mutations = array %}\n\n {% assign variants_with_metafield = product.variants.edges | map: \"node\" | where: \"metafield\" %}\n\n {% log\n product_id: product.id,\n variants_count: product.variants.edges.size,\n variants_with_metafield: variants_with_metafield.size\n %}\n\n {% for variant in variants_with_metafield %}\n {% assign metafield = variant.metafield.value | parse_json %}\n\n {% comment %}\n -- revert the prices on this variant and delete the metafield\n {% endcomment %}\n\n {% capture variant_update %}\n {\n id: {{ variant.id | json}}\n price: {{ metafield.original_price | json }}\n {% if metafield.original_compare_at_price %}compareAtPrice: {{ metafield.original_compare_at_price | json }}{% endif %}\n }\n {% endcapture %}\n\n {% assign variant_updates = variant_updates | push: variant_update %}\n\n {% capture mutation %}\n mutation {\n metafieldDelete(\n input: {\n id: {{ variant.metafield.id | json }}\n }\n ) {\n deletedId\n userErrors {\n field\n message\n }\n }\n }\n {% endcapture %}\n\n {% assign mutations = mutations | push: mutation %}\n {% endfor %}\n\n {% if variant_updates != blank %}\n {% action \"shopify\" %}\n mutation {\n productVariantsBulkUpdate(\n productId: {{ product.id | json }}\n variants: [\n {{ variant_updates | join: newline }}\n ]\n ) {\n product {\n id\n title\n }\n productVariants {\n id\n displayName\n sku\n price\n compareAtPrice\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% for mutation in mutations %}\n {% action \"shopify\" mutation %}\n {% endfor %}\n {% endfor %}\n\n {% if products_result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = products_result.data.products.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- delete the price change events shop metafield\n {% endcomment %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldDelete(\n input: {\n id: {{ shop.metafield.id | json }}\n }\n ) {\n deletedId\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% comment %}\n -- send email notification about the price change events reset\n {% endcomment %}\n\n {% capture email_subject %}All price change events have been cleared{% endcapture %}\n\n {% capture email_body %}\n All price change events have been reverted and cleared, using the {{ task_admin_link }} task within the Mechanic app.\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n{% endif %}", + "script": "{% assign shop_metafield_namespace = \"mechanic\" %}\n{% assign shop_metafield_key = \"price_change_events\" %}\n{% assign variant_metafield_namespace = \"mechanic\" %}\n{% assign variant_metafield_key = \"price_change_event\" %}\n\n{% assign email_recipients = options.notification_email_recipients__array_required %}\n\n{% capture task_admin_link -%}\n{{ task.name | remove: \"ADVANCED: \" }}\n{%- endcapture %}\n\n{% comment %}\n -- query for shop data on every task run, since it will be needed for each valid action keyword and custom event\n{% endcomment %}\n\n{% capture shop_query %}\n query {\n shop {\n id\n metafield(\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n{% endcapture %}\n\n{% assign shop_result = shop_query | shopify %}\n\n{% if event.preview %}\n {% capture shop_result_json %}\n {\n \"data\": {\n \"shop\": {\n \"id\": \"gid://shopify/Shop/1234567890\",\n \"metafield\": {\n \"id\": \"gid://shopify/Metafield/9876543210\",\n \"value\": \"\\n{\\n \\\"price_change_event__1234567890\\\": {\\n \\\"status\\\": \\\"scheduled\\\",\\n \\\"start\\\": \\\"2021-12-30 08:00\\\",\\n \\\"end\\\": \\\"2021-12-31 20:00\\\",\\n \\\"set_compare_at_prices\\\": true,\\n \\\"collection_handles_and_discounts\\\": {\\n \\\"collection-alpha\\\": \\\"20%\\\",\\n \\\"collection-beta\\\": \\\"-10\\\",\\n \\\"collection-gamma\\\": \\\"20\\\"\\n },\\n \\\"skus_to_include\\\": [],\\n \\\"sku_discount\\\": \\\"\\\",\\n \\\"skus_to_exclude\\\": [],\\n \\\"exclude_products_tagged_with\\\": [\\n \\\"clearance\\\"\\n ]\\n }\\n}\\n\"\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign shop_result = shop_result_json | parse_json %}\n{% endif %}\n\n{% assign shop = shop_result.data.shop %}\n{% assign price_change_events = shop.metafield.value | default: \"{}\" | parse_json %}\n\n{% if event.topic == \"mechanic/user/text\" %}\n {% comment %}\n -- check text entry to see what action to take\n {% endcomment %}\n\n {% assign action_keyword = event.data | downcase %}\n\n {% if event.preview %}\n {% assign action_keyword = \"schedule\" %}\n {% endif %}\n\n {% case action_keyword %}\n {% when \"schedule\" %}\n {% comment %}\n -- NOTE: run scheduling logic here instead of in custom event like most other keywords, so user gets immediate feedback on any configuration errors\n {% endcomment %}\n\n {% assign valid_start_datetime = options.event_start_datetime__required | parse_date: \"%Y-%m-%d %H:%M\" %}\n\n {% if valid_start_datetime == blank %}\n {% assign valid_start_datetime = options.event_start_datetime__required | parse_date: \"%Y-%m-%d\" %}\n\n {% if valid_start_datetime == blank %}\n {% unless event.preview %}\n {% error \"The event start date is not a valid date. Please re-enter it (per the task documentation) and try scheduling the event again.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n {% endif %}\n\n {% assign valid_end_datetime = options.event_end_datetime__required | parse_date: \"%Y-%m-%d %H:%M\" %}\n\n {% if valid_end_datetime == blank %}\n {% assign valid_end_datetime = options.event_end_datetime__required | parse_date: \"%Y-%m-%d\" %}\n\n {% if valid_end_datetime == blank %}\n {% unless event.preview %}\n {% error \"The event end date is not a valid date. Please re-enter it (per the task documentation) and try scheduling the event again.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n {% endif %}\n\n {% assign now = \"now\" | date: \"%s\" %}\n {% assign start_datetime_s = options.event_start_datetime__required | date: \"%s\" %}\n {% assign end_datetime_s = options.event_end_datetime__required | date: \"%s\" %}\n\n {% if start_datetime_s <= now or start_datetime_s >= end_datetime_s %}\n {% unless event.preview %}\n {% error \"The event start and end dates must be future dates and the start date must be before the end date. Please re-enter them (per the task documentation) and try scheduling the event again.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% assign set_compare_at_prices = options.set_compare_at_prices_to_original_price_during_event__boolean %}\n {% assign collection_handles_and_discounts = options.collection_handles_and_discounts__keyval %}\n {% assign skus_to_include = options.skus_to_include__array %}\n {% assign sku_discount = options.sku_discount %}\n {% assign exclude_products_tagged_with = options.exclude_products_tagged_with__array %}\n {% assign skus_to_exclude = options.skus_to_exclude__array %}\n\n {% comment %}\n -- validate discount entry formats: XX% (percentage discount), X.XX (fixed price), or -X.XX (fixed discount)\n {% endcomment %}\n\n {% assign discounts = collection_handles_and_discounts | values | default: array %}\n\n {% if skus_to_include != blank and sku_discount == blank %}\n {% unless event.preview %}\n {% error \"A SKU discount must be configured when specific SKUs are included.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% if sku_discount != blank %}\n {% assign discounts = discounts | push: sku_discount %}\n {% endif %}\n\n {% for discount in discounts %}\n {% if discount contains \"%\" %}\n {% assign discount_percentage = discount | times: 1 %}\n {% assign discount_string = discount_percentage | append: \"%\" %}\n\n {% if discount != discount_string %}\n {% unless event.preview %}\n {% error\n message: \"Percentage discount entry incorrect. Please update the task configuration (per the task documentation) and try scheduling the event again.\",\n discount: discount\n %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% if discount_percentage <= 0 or discount_percentage >= 100 %}\n {% unless event.preview %}\n {% error\n message: \"Percentage discount entries should contain an number between 0 and 100. Please update the task configuration (per the task documentation) and try scheduling the event again.\",\n discount: discount\n %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% else %}\n {% assign discount_absolute = discount | times: 1 | abs %}\n\n {% if discount_absolute == 0 %}\n {% unless event.preview %}\n {% error\n message: \"Fixed price and fixed discount entries must contain an absolute number > 0. Please update the task configuration (per the task documentation) and try scheduling the event again.\",\n discount: discount\n %}\n {% break %}\n {% endunless %}\n {% endif %}\n {% endif %}\n {% endfor %}\n\n {% assign price_change_event = hash %}\n\n {% assign price_change_event[\"status\"] = \"scheduled\" %}\n {% assign price_change_event[\"start\"] = start_datetime_s %}\n {% assign price_change_event[\"end\"] = end_datetime_s %}\n {% assign price_change_event[\"set_compare_at_prices\"] = set_compare_at_prices %}\n {% assign price_change_event[\"collection_handles_and_discounts\"] = collection_handles_and_discounts %}\n {% assign price_change_event[\"skus_to_include\"] = skus_to_include %}\n {% assign price_change_event[\"sku_discount\"] = sku_discount %}\n {% assign price_change_event[\"skus_to_exclude\"] = skus_to_exclude %}\n {% assign price_change_event[\"exclude_products_tagged_with\"] = exclude_products_tagged_with %}\n\n {% log\n task_options: task.options,\n price_change_event: price_change_event\n %}\n\n {% assign price_change_event_id = event.id | default: task.id %}\n\n {% assign price_change_events[price_change_event_id] = price_change_event %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n value: {{ price_change_events | json | json }}\n type: \"json\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Shop {\n id\n name\n myshopifyDomain\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% comment %}\n -- schedule start and end events\n {% endcomment %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/price_changes/start\",\n \"task_id\": {{ task.id | json }},\n \"run_at\": {{ start_datetime_s | json }},\n \"data\": {\n \"price_change_event_id\": {{ price_change_event_id | json }}\n }\n }\n {% endaction %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/price_changes/end\",\n \"task_id\": {{ task.id | json }},\n \"run_at\": {{ end_datetime_s | json }},\n \"data\": {\n \"price_change_event_id\": {{ price_change_event_id | json }}\n }\n }\n {% endaction %}\n\n {% capture email_subject %}New price change event scheduled ({{ price_change_event_id }}){% endcapture %}\n\n {% capture email_body %}\n A new price change event has been scheduled, from the {{ task_admin_link }} task within the Mechanic app.\n\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event[\"status\"] }}\n Event start: {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n\n Note: To cancel this event while it is still scheduled or ongoing, use the \"cancel\" keyword along with the price change event ID when running the task.\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n {% when \"list\" %}\n {% action \"echo\" price_change_events: price_change_events %}\n\n {% when \"email\" %}\n {% if price_change_events == blank %}\n {% assign email_body = \"There are currently no configured price change events\" %}\n\n {% else %}\n {% capture email_body %}\n Currently configured price change events, using the {{ task_admin_link }} task within the Mechanic app.\n\n {% for price_change_event in price_change_events -%}\n {%- assign price_change_event_id = price_change_event[0] -%}\n {%- assign price_change_event_data = price_change_event[1] -%}\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event_data[\"status\"] }}\n Event start: {{ price_change_event_data[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event_data[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event_data[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event_data[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event_data[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event_data[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event_data[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event_data[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n
\n {%- endfor %}\n {% endcapture %}\n {% endif %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": \"All price change events as of {{ \"now\" | date: \"%F\" }}\",\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n {% when \"reset\" %}\n {% action \"event\" %}\n {\n \"topic\": \"user/price_changes/reset\",\n \"task_id\": {{ task.id | json }}\n }\n {% endaction %}\n\n {% else %}\n {% if action_keyword contains \"cancel\" %}\n {% assign price_change_event_id = action_keyword | remove: \"cancel \" %}\n {% assign price_change_event = price_change_events[price_change_event_id] %}\n\n {% if price_change_event == blank %}\n {% error \"Keyword of 'cancel' entered with an invalid price change event ID. Run task with 'list' keyword to see a list of all configured price change events.\" %}\n {% break %}\n {% endif %}\n\n {% if price_change_event.status == \"scheduled\" %}\n {% comment %}\n -- price change event has not started, so can just update it to cancelled and when the start/end events run they will ignore this event\n {% endcomment %}\n\n {% assign price_change_event[\"status\"] = \"cancelled\" %}\n {% assign price_change_events[price_change_event_id] = price_change_event %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n value: {{ price_change_events | json | json }}\n type: \"json\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Shop {\n id\n name\n myshopifyDomain\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% capture email_subject %}Scheduled price change event has been cancelled ({{ price_change_event_id }}){% endcapture %}\n\n {% capture email_body %}\n A scheduled price change event has been cancelled, from the {{ task_admin_link }} task within the Mechanic app.\n\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event[\"status\"] }}\n Event start: {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n {% elsif price_change_event.status == \"ongoing\" %}\n {% comment %}\n -- price change event in progress, so we have to revert the price changes to properly cancel it\n {% endcomment %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/price_changes/end\",\n \"task_id\": {{ task.id | json }},\n \"data\": {\n \"price_change_event_id\": {{ price_change_event_id | json }},\n \"cancel\": true\n }\n }\n {% endaction %}\n\n {% else %}\n {% action \"echo\"\n message: \"Price change event cannot be cancelled because it does not have a status of 'scheduled' or 'ongoing'\",\n price_change_event: price_change_event\n %}\n {% endif %}\n\n {% else %}\n {% error\n message: \"Unrecognized action keyword. Action keyword must be one of 'schedule', 'list', 'email', or 'cancel'\",\n action_keyword: action_keyword\n %}\n {% endif %}\n {% endcase %}\n\n{% elsif event.topic == \"user/price_changes/start\" %}\n {% assign price_change_event_id = event.data.price_change_event_id %}\n {% assign price_change_event = price_change_events[price_change_event_id] %}\n\n {% if event.preview %}\n {% assign price_change_event_id = \"01234567-89ab-cdef\" %}\n\n {% capture price_change_event_json %}\n {\n \"status\": \"scheduled\",\n \"start\": {{ \"now + 1 day\" | date: \"%s\" }},\n \"end\": {{ \"now + 2 days\" | date: \"%s\" }},\n \"set_compare_at_prices\": false,\n \"collection_handles_and_discounts\": {\n \"alpha-beta-gamma\": \"10%\",\n \"sticks-and-stones\": \"-5.50\",\n \"lorem-ipsum\": \"3\"\n },\n \"skus_to_include\": [\n \"SKU-123\",\n \"SKU-456\"\n ],\n \"sku_discount\": \"15%\",\n \"skus_to_exclude\": [\n \"SKU-987\"\n ],\n \"exclude_products_tagged_with\": [\n \"do-not-discount\"\n ]\n }\n {% endcapture %}\n\n {% assign price_change_event = price_change_event_json | parse_json %}\n {% endif %}\n\n {% assign status = price_change_event.status %}\n {% assign set_compare_at_prices = price_change_event.set_compare_at_prices %}\n {% assign collection_handles_and_discounts = price_change_event.collection_handles_and_discounts %}\n {% assign skus_to_include = price_change_event.skus_to_include %}\n {% assign sku_discount = price_change_event.sku_discount %}\n {% assign exclude_products_tagged_with = price_change_event.exclude_products_tagged_with %}\n {% assign skus_to_exclude = price_change_event.skus_to_exclude %}\n\n {% if status != \"scheduled\" %}\n {% log\n message: \"This price change event does not have a status of scheduled, and thus will not start.\",\n price_change_event: price_change_event\n %}\n {% break %}\n {% endif %}\n\n {% comment %}\n -- get collection ids for products query\n {% endcomment %}\n\n {% assign in_collection_checks = array %}\n\n {% for keyval in collection_handles_and_discounts %}\n {% capture collection_query %}\n query {\n collectionByHandle(handle: {{ keyval[0] | json }}) {\n id\n handle\n }\n }\n {% endcapture %}\n\n {% assign collection_result = collection_query | shopify %}\n {% assign collection = collection_result.data.collectionByHandle %}\n\n {% if collection != blank %}\n {% capture in_collection_check -%}\n inCollection_{{ collection.handle | replace: \"-\", \"_\" }}: inCollection(id: {{ collection.id | json }})\n {%- endcapture %}\n\n {% assign in_collection_checks = in_collection_checks | push: in_collection_check %}\n {% endif %}\n {% endfor %}\n\n {% assign query_filter = \"gift_card:false\" %}\n\n {% for tag in exclude_products_tagged_with %}\n {% assign query_filter = tag | json | prepend: \" tag_not:\" | prepend: query_filter %}\n {% endfor %}\n\n {% log query_filter: query_filter %}\n\n {% assign products = array %}\n\n {% assign cursor = nil %}\n\n {% for n in (1..10000) %}\n {% capture products_query %}\n query {\n products(\n first: 4\n after: {{ cursor | json }}\n query: {{ query_filter | json }}\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n title\n tags\n variants(first: 100) {\n edges {\n node {\n id\n title\n sku\n price\n compareAtPrice\n metafield(\n namespace: {{ variant_metafield_namespace | json }}\n key: {{ variant_metafield_key | json }}\n ) {\n value\n }\n }\n }\n }\n {{ in_collection_checks | join: newline }}\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_query | shopify %}\n\n {% if event.preview %}\n {% capture products_result_json %}\n {\n \"data\": {\n \"products\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"variants\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/ProductVariant/1234567890\",\n \"sku\": \"ACME-BRICK-RED\",\n \"price\": \"10.00\",\n \"compareAtPrice\": \"15.00\",\n \"metafield\": null\n }\n }\n ]\n },\n \"inCollection_{{ collection_handles_and_discounts.first.first | replace: \"-\", \"_\" | default: \"sample\" }}\": true\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_result_json | parse_json %}\n {% endif %}\n\n {% assign products = products_result.data.products.edges | map: \"node\" %}\n\n {% for product in products %}\n {% comment %}\n -- For each product, match to a collection discount if applicable, then, if needed, loop through variants to see if there are any overrides\n {% endcomment %}\n\n {% assign product_level_discount = nil %}\n\n {% for keyval in collection_handles_and_discounts %}\n {% assign in_collection_label\n = \"inCollection_\"\n | append: keyval[0]\n | replace: \"-\", \"_\"\n %}\n\n {% if product[in_collection_label] %}\n {% assign product_level_discount = keyval[1] %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if product_level_discount == blank\n and skus_to_include == blank\n and skus_to_exclude == blank\n %}\n {% log\n message: \"This product is not within any configured collections, nor does the price change event have any sku inclusion or exclusion settings; skipping. \",\n product: product\n %}\n {% continue %}\n {% endif %}\n\n {% assign variants = product.variants.edges | map: \"node\" %}\n\n {% assign variant_updates = array %}\n\n {% for variant in variants %}\n {% if skus_to_exclude != blank and skus_to_exclude contains variant.sku %}\n {% log\n message: \"This variant was excluded by SKU\",\n variant: variant\n %}\n {% continue %}\n {% endif %}\n\n {% if variant.metafield != blank %}\n {% log\n message: \"This variant is already part of a price change event; skipping.\",\n variant: variant\n %}\n {% continue %}\n {% endif %}\n\n {% assign discount_to_apply = product_level_discount %}\n\n {% if skus_to_include != blank and skus_to_include contains variant.sku %}\n {% assign discount_to_apply = sku_discount %}\n {% endif %}\n\n {% log\n product_title: product.title,\n sku: variant.sku,\n skus_to_include: skus_to_include,\n sku_discount: sku_discount,\n discount_to_apply: discount_to_apply\n %}\n\n {% if discount_to_apply == blank %}\n {% log\n message: \"This variant's product is not within any configured collections, nor has this variant been specifically included by sku; skipping.\",\n variant: variant\n %}\n {% continue %}\n {% endif %}\n\n {% assign price_to_set = nil %}\n {% assign compare_at_price_to_set = nil %}\n\n {% if discount_to_apply contains \"%\" %}\n {% assign price_to_set\n = discount_to_apply\n | remove: \"%\"\n | minus: 100\n | abs\n | times: variant.price\n | divided_by: 100\n | round: 2\n %}\n\n {% elsif discount_to_apply contains \"-\" %}\n {% assign price_to_set\n = variant.price\n | plus: discount_to_apply\n | at_least: 0.0\n %}\n\n {% else %}\n {% assign price_to_set = discount_to_apply %}\n {% endif %}\n\n {% if set_compare_at_prices %}\n {% assign compare_at_price_to_set = variant.price %}\n {% endif %}\n\n {% log\n product: product,\n product_level_discount: product_level_discount,\n variant: variant,\n discount_to_apply: discount_to_apply,\n price_to_set: price_to_set,\n compare_at_price_to_set: compare_at_price_to_set\n %}\n\n {% capture variant_metafield_value %}\n {\n \"price_change_event_id\": {{ price_change_event_id | json }},\n \"discount_to_apply\": {{ discount_to_apply | json }},\n {% if set_compare_at_prices %}\n \"original_compare_at_price\": {{ variant.compareAtPrice | json }},\n \"compare_at_price_to_set\": {{ compare_at_price_to_set | json }},\n {% endif %}\n \"original_price\": {{ variant.price | json }},\n \"price_to_set\": {{ price_to_set | json }}\n }\n {% endcapture %}\n\n {% capture variant_update %}\n {\n id: {{ variant.id | json}}\n price: {{ price_to_set | json }}\n {% if set_compare_at_prices %}compareAtPrice: {{ variant.price | json }}{% endif %}\n metafields: [\n {\n key: {{ variant_metafield_key | json }}\n namespace: {{ variant_metafield_namespace | json }}\n type: \"json\"\n value: {{ variant_metafield_value | json }}\n }\n ]\n }\n {% endcapture %}\n\n {% assign variant_updates = variant_updates | push: variant_update %}\n {% endfor %}\n\n {% if variant_updates != blank %}\n {% action \"shopify\" %}\n mutation {\n productVariantsBulkUpdate(\n productId: {{ product.id | json }}\n variants: [\n {{ variant_updates | join: newline }}\n ]\n ) {\n product {\n id\n title\n tags\n }\n productVariants {\n id\n title\n sku\n price\n compareAtPrice\n metafield(\n namespace: {{ variant_metafield_namespace | json }}\n key: {{ variant_metafield_key | json }}\n ) {\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n\n {% if products_result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = products_result.data.products.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- update the price change event\n {% endcomment %}\n\n {% assign price_change_event[\"status\"] = \"ongoing\" %}\n {% assign price_change_events[price_change_event_id] = price_change_event %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n value: {{ price_change_events | json | json }}\n type: \"json\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Shop {\n id\n name\n myshopifyDomain\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% capture email_subject %}A scheduled price change event has started ({{ price_change_event_id }}){% endcapture %}\n\n {% capture email_body %}\n A scheduled price change event has started, using the {{ task_admin_link }} task within the Mechanic app.\n\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event[\"status\"] }}\n Event start: {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n\n Note: To cancel this event while it is ongoing, use the \"cancel\" keyword along with the price change event ID when running the task.\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n{% elsif event.topic == \"user/price_changes/end\" %}\n {% assign price_change_event_id = event.data.price_change_event_id %}\n {% assign price_change_event = price_change_events[price_change_event_id] %}\n\n {% if event.preview %}\n {% assign price_change_event_id = \"01234567-89ab-cdef\" %}\n {% assign price_change_event = hash %}\n {% assign price_change_event[\"status\"] = \"ongoing\" %}\n {% endif %}\n\n {% if event.data.cancel %}\n {% comment %}\n -- go ahead and cancel, as status was already checked to be \"scheduled\" prior to custom event call\n {% endcomment %}\n\n {% assign price_change_event[\"status\"] = \"cancelled\" %}\n\n {% else %}\n {% comment %}\n -- since this was a scheduled run, need to make sure the event status is \"ongoing\" before reverting changes\n {% endcomment %}\n\n {% if price_change_event.status != \"ongoing\" %}\n {% log\n message: \"This price change event does not have a status of 'ongoing', and thus will not be reverted.\",\n price_change_event: price_change_event\n %}\n {% break %}\n {% endif %}\n\n {% assign price_change_event[\"status\"] = \"completed\" %}\n {% endif %}\n\n {% comment %}\n -- To revert the price change event, check every variant in the shop to see if the metafield exists and contains this price change event ID\n -- Note: Query from the product level so that productVariantsBulkUpdate can be used, causing only one product update event to fire\n {% endcomment %}\n\n {% assign products = array %}\n\n {% assign cursor = nil %}\n\n {% for n in (1..10000) %}\n {% capture products_query %}\n query {\n products(\n first: 4\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n title\n tags\n variants(first: 100) {\n edges {\n node {\n id\n title\n sku\n price\n compareAtPrice\n metafield(\n namespace: {{ variant_metafield_namespace | json }}\n key: {{ variant_metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_query | shopify %}\n\n {% if event.preview %}\n {% capture products_result_json %}\n {\n \"data\": {\n \"products\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"variants\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/ProductVariant/1234567890\",\n \"sku\": \"ACME-BRICK-RED\",\n \"price\": \"7.50\",\n \"compareAtPrice\": \"10.00\",\n \"metafield\": {\n \"id\": \"gid://shopify/Metafield/9876543210\",\n \"value\": \"\\n{\\n \\\"price_change_event_id\\\": \\\"01234567-89ab-cdef\\\",\\n \\\"discount_to_apply\\\": \\\"25%\\\",\\n \\\"original_compare_at_price\\\": \\\"15.00\\\",\\n \\\"compare_at_price_to_set\\\": 7.50,\\n \\\"original_price\\\": \\\"10.00\\\",\\n \\\"price_to_set\\\": 7.50\\n}\\n\"\n }\n }\n }\n ]\n }\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_result_json | parse_json %}\n {% endif %}\n\n {% assign products = products_result.data.products.edges | map: \"node\" %}\n\n {% for product in products %}\n {% assign variant_updates = array %}\n {% assign metafields_to_delete = array %}\n\n {% assign variants_with_metafield = product.variants.edges | map: \"node\" | where: \"metafield\" %}\n\n {% for variant in variants_with_metafield %}\n {% assign metafield = variant.metafield.value | parse_json %}\n\n {% if metafield.price_change_event_id == price_change_event_id %}\n {% comment %}\n -- revert the prices on this variant and delete the metafield\n {% endcomment %}\n\n {% assign variant_update = hash %}\n {% assign variant_update[\"id\"] = variant.id %}\n {% assign variant_update[\"price\"] = metafield.original_price %}\n {% if metafield.original_compare_at_price %}\n {% assign variant_update[\"compareAtPrice\"] = metafield.original_compare_at_price %}\n {% endif %}\n {% assign variant_updates = variant_updates | push: variant_update %}\n\n {% assign metafield_to_delete = hash %}\n {% assign metafield_to_delete[\"ownerId\"] = variant.id %}\n {% assign metafield_to_delete[\"namespace\"] = variant_metafield_namespace | json %}\n {% assign metafield_to_delete[\"key\"] = variant_metafield_key | json %}\n {% assign metafields_to_delete = metafields_to_delete | push: metafield_to_delete %}\n {% endif %}\n {% endfor %}\n\n {% if metafields_to_delete != blank %}\n {% action \"shopify\" %}\n mutation {\n metafieldsDelete(\n metafields: {{ metafields_to_delete | graphql_arguments }}\n ) {\n deletedMetafields {\n ownerId\n namespace\n key\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% if variant_updates != blank %}\n {% action \"shopify\" %}\n mutation {\n productVariantsBulkUpdate(\n productId: {{ product.id | json }}\n variants: {{ variant_updates | graphql_arguments }}\n ) {\n product {\n id\n title\n }\n productVariants {\n id\n displayName\n sku\n price\n compareAtPrice\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n\n {% if products_result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = products_result.data.products.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- update the price change event\n {% endcomment %}\n\n {% assign price_change_events[price_change_event_id] = price_change_event %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n value: {{ price_change_events | json | json }}\n type: \"json\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Shop {\n id\n name\n myshopifyDomain\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% comment %}\n -- send email notification about the price change event status change\n {% endcomment %}\n\n {% capture email_subject %}A price change event has been {{ price_change_event[\"status\"] }} ({{ price_change_event_id }}){% endcapture %}\n\n {% capture email_body %}\n A price change event has been {{ price_change_event[\"status\"] }}, using the {{ task_admin_link }} task within the Mechanic app.\n\n Price change event ID: {{ price_change_event_id }}\n Status: {{ price_change_event[\"status\"] }}\n Event start: {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n Event end: {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n Set compare at price to original price during event: {{ price_change_event[\"set_compare_at_prices\"] }}\n Collection handles and discounts:\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n SKUs to include: {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n SKU discount: {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n SKUs to exclude: {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n Exclude products tagged with: {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n\n{% elsif event.topic == \"user/price_changes/reset\" %}\n {% comment %}\n -- To reset price change events, check every variant in the shop to see if the price change event metafield exists\n -- Note: Query from the product level so that productVariantsBulkUpdate can be used, causing only one product update event to fire\n {% endcomment %}\n\n {% assign products = array %}\n\n {% assign cursor = nil %}\n\n {% for n in (1..10000) %}\n {% capture products_query %}\n query {\n products(\n first: 4\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n title\n tags\n variants(first: 100) {\n edges {\n node {\n id\n title\n sku\n price\n compareAtPrice\n metafield(\n namespace: {{ variant_metafield_namespace | json }}\n key: {{ variant_metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_query | shopify %}\n\n {% if event.preview %}\n {% capture products_result_json %}\n {\n \"data\": {\n \"products\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"variants\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/ProductVariant/1234567890\",\n \"sku\": \"ACME-BRICK-RED\",\n \"price\": \"7.50\",\n \"compareAtPrice\": \"10.00\",\n \"metafield\": {\n \"id\": \"gid://shopify/Metafield/9876543210\",\n \"value\": \"\\n{\\n \\\"price_change_event_id\\\": \\\"01234567-89ab-cdef\\\",\\n \\\"discount_to_apply\\\": \\\"25%\\\",\\n \\\"original_compare_at_price\\\": \\\"15.00\\\",\\n \\\"compare_at_price_to_set\\\": 7.50,\\n \\\"original_price\\\": \\\"10.00\\\",\\n \\\"price_to_set\\\": 7.50\\n}\\n\"\n }\n }\n }\n ]\n }\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign products_result = products_result_json | parse_json %}\n {% endif %}\n\n {% assign products = products_result.data.products.edges | map: \"node\" %}\n\n {% for product in products %}\n {% assign variant_updates = array %}\n {% assign metafields_to_delete = array %}\n\n {% assign variants_with_metafield = product.variants.edges | map: \"node\" | where: \"metafield\" %}\n\n {% log\n product_id: product.id,\n variants_count: product.variants.edges.size,\n variants_with_metafield: variants_with_metafield.size\n %}\n\n {% for variant in variants_with_metafield %}\n {% assign metafield = variant.metafield.value | parse_json %}\n\n {% comment %}\n -- revert the prices on this variant and delete the metafield\n {% endcomment %}\n\n {% assign variant_update = hash %}\n {% assign variant_update[\"id\"] = variant.id %}\n {% assign variant_update[\"price\"] = metafield.original_price %}\n {% if metafield.original_compare_at_price %}\n {% assign variant_update[\"compareAtPrice\"] = metafield.original_compare_at_price %}\n {% endif %}\n {% assign variant_updates = variant_updates | push: variant_update %}\n\n {% assign metafield_to_delete = hash %}\n {% assign metafield_to_delete[\"ownerId\"] = variant.id %}\n {% assign metafield_to_delete[\"namespace\"] = variant_metafield_namespace | json %}\n {% assign metafield_to_delete[\"key\"] = variant_metafield_key | json %}\n {% assign metafields_to_delete = metafields_to_delete | push: metafield_to_delete %}\n {% endfor %}\n\n {% if metafields_to_delete != blank %}\n {% action \"shopify\" %}\n mutation {\n metafieldsDelete(\n metafields: {{ metafields_to_delete | graphql_arguments }}\n ) {\n deletedMetafields {\n ownerId\n namespace\n key\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% if variant_updates != blank %}\n {% action \"shopify\" %}\n mutation {\n productVariantsBulkUpdate(\n productId: {{ product.id | json }}\n variants: {{ variant_updates | graphql_arguments }}\n ) {\n product {\n id\n title\n }\n productVariants {\n id\n displayName\n sku\n price\n compareAtPrice\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n\n {% if products_result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = products_result.data.products.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- delete the price change events shop metafield\n {% endcomment %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsDelete(\n metafields: [\n {\n ownerId: {{ shop.id | json }}\n namespace: {{ shop_metafield_namespace | json }}\n key: {{ shop_metafield_key | json }}\n }\n ]\n ) {\n deletedMetafields {\n ownerId\n namespace\n key\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% comment %}\n -- send email notification about the price change events reset\n {% endcomment %}\n\n {% capture email_subject %}All price change events have been cleared{% endcapture %}\n\n {% capture email_body %}\n All price change events have been reverted and cleared, using the {{ task_admin_link }} task within the Mechanic app.\n {% endcapture %}\n\n {% action \"email\" %}\n {\n \"to\": {{ email_recipients | json }},\n \"subject\": {{ email_subject | json }},\n \"body\": {{ email_body | newline_to_br | json }},\n \"reply_to\": {{ shop.customer_email | json }},\n \"from_display_name\": {{ shop.name | json }}\n }\n {% endaction %}\n{% endif %}\n", "subscriptions": [ "mechanic/user/text", "user/price_changes/start", diff --git a/tasks/auto-remove-a-customer-tag-x-days-after-its-added.json b/tasks/auto-remove-a-customer-tag-x-days-after-its-added.json index 0025bef4..c96648ee 100644 --- a/tasks/auto-remove-a-customer-tag-x-days-after-its-added.json +++ b/tasks/auto-remove-a-customer-tag-x-days-after-its-added.json @@ -1,5 +1,5 @@ { - "docs": "Use this this task to monitor for the addition of a new customer tag, and to schedule the customer to be untagged some number of days later. Useful for granting temporary access to discounts, or other resources.\n\nThis task monitors new and updated customers, watching for the configured customer tag.\r\n\r\nAs soon as that tag is detected, the task will _add_ a second tag, indicating that the customer is scheduled to be untagged. (For example, if the task is configured to watch for the tag \"Approved\", the task will _add_ the tag \"Approved - will be auto-removed by Mechanic\".) The task will then schedule a followup event for the future, according to the configured number of days to wait. At that time, the task will remove both tags.\r\n\r\nImportant note: To _prevent_ the task from untagging the customer later, manually remove the task's additional tag (i.e. the \"will be auto-removed by Mechanic\" tag). If the additional tag is found missing, the task will leave the tag in place instead of auto-removing it.", + "docs": "Use this this task to monitor for the addition of a specific customer tag, and to schedule the customer to be untagged a configurable number of days later. Useful for granting temporary access to discounts, or other resources.\n\nAs soon as that tag is detected, the task will _add_ a second tag, indicating that the customer is scheduled to be untagged. (For example, if the task is configured to watch for the tag \"Approved\", the task will _add_ the tag \"Approved - will be auto-removed by Mechanic\".) The task will then schedule a followup event for the future, according to the configured number of days to wait. At that time, the task will remove both tags.\n\nImportant note: To _prevent_ the task from untagging the customer later, manually remove the task's additional tag (i.e. the \"will be auto-removed by Mechanic\" tag).", "halt_action_run_sequence_on_error": false, "name": "Auto-remove a customer tag x days after it's added", "online_store_javascript": null, @@ -10,7 +10,7 @@ "order_status_javascript": null, "perform_action_runs_in_sequence": false, "preview_event_definitions": [], - "script": "{% if event.topic == \"user/task/untag_customer\" %}\n {% assign customer_id = event.data.customer_id %}\n{% else %}\n {% assign customer_id = customer.admin_graphql_api_id %}\n{% endif %}\n\n{% assign tag = options.tag_to_monitor__required %}\n{% assign untag_flag_tag = tag | append: \" - will be auto-removed by Mechanic\" %}\n{% assign tag_removal_interval_s = options.days_to_wait_before_untagging__number_required | times: 24 | times: 60 | times: 60 | round %}\n{% assign now_s = \"now\" | date: \"%s\" | times: 1 %}\n{% assign metafield_key = task.id | sha256 | slice: 0, 7 %}\n\n{% capture query %}\n query {\n customer(id: {{ customer_id | json }}) {\n id\n tags\n metafield(\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n{% endcapture %}\n\n{% assign result = query | shopify %}\n\n{% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"customer\": {\n \"id\": \"gid://shopify/Customer/1234567890\",\n \"tags\": [{{ tag | json }}],\n \"metafield\": null\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n{% endif %}\n\n{% assign customer = result.data.customer %}\n\n{% if customer.tags contains tag %}\n {% assign time_to_remove_s = customer.metafield.value | times: 1 %}\n\n {% if customer.metafield == nil or time_to_remove_s == 0 %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ customer.id | json }}\n tags: {{ untag_flag_tag | json }}\n ) {\n node {\n ... on Customer {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n customerUpdate(\n input: {\n id: {{ customer.id | json }}\n metafields: [\n {\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n value: {{ now_s | plus: tag_removal_interval_s | append: \"\" | json }}\n type: \"number_integer\"\n }\n ]\n }\n ) {\n customer {\n metafield(\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n ) {\n id\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/task/untag_customer\",\n \"data\": {\n \"customer_id\": {{ customer.id | json }}\n },\n \"run_at\": {{ now_s | plus: tag_removal_interval_s | json }},\n \"task_id\": {{ task.id | json }}\n }\n {% endaction %}\n {% elsif customer.tags contains untag_flag_tag %}\n {% if now_s < time_to_remove_s %}\n {% log message: \"This customer is scheduled to be untagged, but it's not time yet. Skipping.\", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %}\n {% else %}\n {% log message: \"This customer is scheduled to be untagged, and that time is now.\", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %}\n {% action \"shopify\" %}\n mutation {\n tagsRemove(\n id: {{ customer.id | json }}\n tags: [{{ tag | json }}, {{ untag_flag_tag | json }}]\n ) {\n node {\n ... on Customer {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n metafieldDelete(\n input: {\n id: {{ customer.metafield.id | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% else %}\n {% log message: \"This customer has a tag removal time recorded, but the 'untag flag tag' has been removed. Skipping.\", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %}\n {% endif %}\n{% elsif event.topic == \"user/task/untag_customer\" %}\n {% log message: \"The tag auto-removal event has arrived, but the customer has already been untagged by someone/something else. Skipping.\", tag: tag, customer_tags: customer.tags %}\n{% endif %}", + "script": "{% assign tag_to_monitor = options.tag_to_monitor__required %}\n{% assign tag_removal_interval_s = options.days_to_wait_before_untagging__number_required | times: 24 | times: 60 | times: 60 | round %}\n\n{% assign untag_flag_tag = tag_to_monitor | append: \" - will be auto-removed by Mechanic\" %}\n{% assign now_s = \"now\" | date: \"%s\" | times: 1 %}\n{% assign metafield_key = task.id | sha256 | slice: 0, 7 %}\n\n{% if event.topic == \"user/task/untag_customer\" %}\n {% assign customer_id = event.data.customer_id %}\n{% else %}\n {% assign customer_id = customer.admin_graphql_api_id %}\n{% endif %}\n\n{% capture query %}\n query {\n customer(id: {{ customer_id | json }}) {\n id\n tags\n metafield(\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n{% endcapture %}\n\n{% assign result = query | shopify %}\n\n{% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"customer\": {\n \"id\": \"gid://shopify/Customer/1234567890\",\n \"tags\": {{ tag_to_monitor | json }}\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n{% endif %}\n\n{% assign customer = result.data.customer %}\n\n{% if customer.tags contains tag_to_monitor %}\n {% assign time_to_remove_s = customer.metafield.value | times: 1 %}\n\n {% if customer.metafield == blank or time_to_remove_s == 0 %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ customer.id | json }}\n tags: {{ untag_flag_tag | json }}\n ) {\n node {\n ... on Customer {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ customer.id | json }}\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n value: {{ now_s | plus: tag_removal_interval_s | append: \"\" | json }}\n type: \"number_integer\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Customer {\n id\n displayName\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/task/untag_customer\",\n \"data\": {\n \"customer_id\": {{ customer.id | json }}\n },\n \"run_at\": {{ now_s | plus: tag_removal_interval_s | json }},\n \"task_id\": {{ task.id | json }}\n }\n {% endaction %}\n\n {% elsif customer.tags contains untag_flag_tag %}\n {% if now_s < time_to_remove_s %}\n {% log\n message: \"This customer is scheduled to be untagged, but it's not time yet. Skipping.\",\n tag_to_monitor: tag_to_monitor,\n untag_flag_tag: untag_flag_tag,\n now_s: now_s,\n time_to_remove_s: time_to_remove_s\n %}\n\n {% else %}\n {% log\n message: \"This customer is scheduled to be untagged, and that time is now.\",\n tag_to_monitor: tag_to_monitor,\n untag_flag_tag: untag_flag_tag,\n now_s: now_s,\n time_to_remove_s: time_to_remove_s\n %}\n\n {% action \"shopify\" %}\n mutation {\n tagsRemove(\n id: {{ customer.id | json }}\n tags: {{ array | push: tag_to_monitor, untag_flag_tag | json }}\n ) {\n node {\n ... on Customer {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n metafieldsDelete(\n metafields: [\n {\n ownerId: {{ customer.id | json }}\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n }\n ]\n ) {\n deletedMetafields {\n ownerId\n namespace\n key\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% else %}\n {% log\n message: \"This customer has a tag removal time recorded, but the 'untag flag tag' has been removed. Skipping.\",\n tag_to_monitor: tag_to_monitor,\n untag_flag_tag: untag_flag_tag,\n now_s: now_s,\n time_to_remove_s: time_to_remove_s\n %}\n {% endif %}\n\n{% elsif event.topic == \"user/task/untag_customer\" %}\n {% log\n message: \"The tag auto-removal event has arrived, but the customer has already been untagged by someone/something else. Skipping.\",\n tag_to_monitor: tag_to_monitor,\n customer_tags: customer.tags\n %}\n{% endif %}\n", "subscriptions": [ "shopify/customers/update", "user/task/untag_customer" diff --git a/tasks/auto-remove-a-product-tag-x-days-after-its-added.json b/tasks/auto-remove-a-product-tag-x-days-after-its-added.json index e67a2df4..fb4973fd 100644 --- a/tasks/auto-remove-a-product-tag-x-days-after-its-added.json +++ b/tasks/auto-remove-a-product-tag-x-days-after-its-added.json @@ -1,5 +1,5 @@ { - "docs": "Use this this task to monitor for the addition of a new product tag, and to schedule the product to be untagged some number of days later. Useful for temporarily adding a product to a collection, or qualifying the product for some other temporary functionality.\n\nThis task monitors new and updated products, watching for the configured product tag.\r\n\r\nAs soon as that tag is detected, the task will _add_ a second tag, indicating that the product is scheduled to be untagged. (For example, if the task is configured to watch for the tag \"Approved\", the task will _add_ the tag \"Approved - will be auto-removed by Mechanic\".) The task will then schedule a followup event for the future, according to the configured number of days to wait. At that time, the task will remove both tags.\r\n\r\nImportant note: To _prevent_ the task from untagging the product later, manually remove the task's additional tag (i.e. the \"will be auto-removed by Mechanic\" tag). If the additional tag is found missing, the task will leave the tag in place instead of auto-removing it.", + "docs": "Use this this task to monitor for the addition of a specific product tag, and to schedule the product to be untagged a configurable number of days later. Useful for temporarily adding a product to a collection, or qualifying the product for some other temporary functionality.\n\nAs soon as that tag is detected, the task will _add_ a second tag, indicating that the product is scheduled to be untagged. (For example, if the task is configured to watch for the tag \"Approved\", the task will _add_ the tag \"Approved - will be auto-removed by Mechanic\".) The task will then schedule a followup event for the future, according to the configured number of days to wait. At that time, the task will remove both tags.\n\nImportant note: To _prevent_ the task from untagging the product later, manually remove the task's additional tag (i.e. the \"will be auto-removed by Mechanic\" tag).", "halt_action_run_sequence_on_error": false, "name": "Auto-remove a product tag x days after it's added", "online_store_javascript": null, @@ -10,7 +10,7 @@ "order_status_javascript": null, "perform_action_runs_in_sequence": false, "preview_event_definitions": [], - "script": "{% if event.topic == \"user/task/untag_product\" %}\n {% assign product_id = event.data.product_id %}\n{% else %}\n {% assign product_id = product.admin_graphql_api_id %}\n{% endif %}\n\n{% assign tag = options.tag_to_monitor__required %}\n{% assign untag_flag_tag = tag | append: \" - will be auto-removed by Mechanic\" %}\n{% assign tag_removal_interval_s = options.days_to_wait_before_untagging__number_required | times: 24 | times: 60 | times: 60 | round %}\n{% assign now_s = \"now\" | date: \"%s\" | times: 1 %}\n{% assign metafield_key = task.id | sha256 | slice: 0, 7 %}\n\n{% capture query %}\n query {\n product(id: {{ product_id | json }}) {\n id\n tags\n metafield(\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n{% endcapture %}\n\n{% assign result = query | shopify %}\n\n{% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"product\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"tags\": [{{ tag | json }}],\n \"metafield\": null\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n{% endif %}\n\n{% assign product = result.data.product %}\n\n{% if product.tags contains tag %}\n {% assign time_to_remove_s = product.metafield.value | times: 1 %}\n\n {% if product.metafield == nil or time_to_remove_s == 0 %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ product.id | json }}\n tags: {{ untag_flag_tag | json }}\n ) {\n node {\n ... on Product {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n productUpdate(\n input: {\n id: {{ product.id | json }}\n metafields: [\n {\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n value: {{ now_s | plus: tag_removal_interval_s | append: \"\" | json }}\n type: \"number_integer\"\n }\n ]\n }\n ) {\n product {\n metafield(\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n ) {\n id\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/task/untag_product\",\n \"data\": {\n \"product_id\": {{ product.id | json }}\n },\n \"run_at\": {{ now_s | plus: tag_removal_interval_s | json }},\n \"task_id\": {{ task.id | json }}\n }\n {% endaction %}\n {% elsif product.tags contains untag_flag_tag %}\n {% if now_s < time_to_remove_s %}\n {% log message: \"This product is scheduled to be untagged, but it's not time yet. Skipping.\", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %}\n {% else %}\n {% log message: \"This product is scheduled to be untagged, and that time is now.\", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %}\n {% action \"shopify\" %}\n mutation {\n tagsRemove(\n id: {{ product.id | json }}\n tags: [{{ tag | json }}, {{ untag_flag_tag | json }}]\n ) {\n node {\n ... on Product {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n metafieldDelete(\n input: {\n id: {{ product.metafield.id | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% else %}\n {% log message: \"This product has a tag removal time recorded, but the 'untag flag tag' has been removed. Skipping.\", tag: tag, untag_flag_tag: untag_flag_tag, now_s: now_s, time_to_remove_s: time_to_remove_s %}\n {% endif %}\n{% elsif event.topic == \"user/task/untag_product\" %}\n {% log message: \"The tag auto-removal event has arrived, but the product has already been untagged by someone/something else. Skipping.\", tag: tag, product_tags: product.tags %}\n{% endif %}", + "script": "{% assign tag_to_monitor = options.tag_to_monitor__required %}\n{% assign tag_removal_interval_s = options.days_to_wait_before_untagging__number_required | times: 24 | times: 60 | times: 60 | round %}\n\n{% assign untag_flag_tag = tag_to_monitor | append: \" - will be auto-removed by Mechanic\" %}\n{% assign now_s = \"now\" | date: \"%s\" | times: 1 %}\n{% assign metafield_key = task.id | sha256 | slice: 0, 7 %}\n\n{% if event.topic == \"user/task/untag_product\" %}\n {% assign product_id = event.data.product_id %}\n{% else %}\n {% assign product_id = product.admin_graphql_api_id %}\n{% endif %}\n\n{% capture query %}\n query {\n product(id: {{ product_id | json }}) {\n id\n tags\n metafield(\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n ) {\n id\n value\n }\n }\n }\n{% endcapture %}\n\n{% assign result = query | shopify %}\n\n{% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"product\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"tags\": {{ tag_to_monitor | json }}\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n{% endif %}\n\n{% assign product = result.data.product %}\n\n{% if product.tags contains tag_to_monitor %}\n {% assign time_to_remove_s = product.metafield.value | times: 1 %}\n\n {% if product.metafield == blank or time_to_remove_s == 0 %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ product.id | json }}\n tags: {{ untag_flag_tag | json }}\n ) {\n node {\n ... on Product {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ product.id | json }}\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n value: {{ now_s | plus: tag_removal_interval_s | append: \"\" | json }}\n type: \"number_integer\"\n }\n ]\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Product {\n id\n title\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/task/untag_product\",\n \"data\": {\n \"product_id\": {{ product.id | json }}\n },\n \"run_at\": {{ now_s | plus: tag_removal_interval_s | json }},\n \"task_id\": {{ task.id | json }}\n }\n {% endaction %}\n\n {% elsif product.tags contains untag_flag_tag %}\n {% if now_s < time_to_remove_s %}\n {% log\n message: \"This product is scheduled to be untagged, but it's not time yet. Skipping.\",\n tag_to_monitor: tag_to_monitor,\n untag_flag_tag: untag_flag_tag,\n now_s: now_s,\n time_to_remove_s: time_to_remove_s\n %}\n\n {% else %}\n {% log\n message: \"This product is scheduled to be untagged, and that time is now.\",\n tag_to_monitor: tag_to_monitor,\n untag_flag_tag: untag_flag_tag,\n now_s: now_s,\n time_to_remove_s: time_to_remove_s\n %}\n\n {% action \"shopify\" %}\n mutation {\n tagsRemove(\n id: {{ product.id | json }}\n tags: {{ array | push: tag_to_monitor, untag_flag_tag | json }}\n ) {\n node {\n ... on Product {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n metafieldsDelete(\n metafields: [\n {\n ownerId: {{ product.id | json }}\n namespace: \"mechanic\"\n key: {{ metafield_key | json }}\n }\n ]\n ) {\n deletedMetafields {\n ownerId\n namespace\n key\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% else %}\n {% log\n message: \"This product has a tag removal time recorded, but the 'untag flag tag' has been removed. Skipping.\",\n tag_to_monitor: tag_to_monitor,\n untag_flag_tag: untag_flag_tag,\n now_s: now_s,\n time_to_remove_s: time_to_remove_s\n %}\n {% endif %}\n\n{% elsif event.topic == \"user/task/untag_product\" %}\n {% log\n message: \"The tag auto-removal event has arrived, but the product has already been untagged by someone/something else. Skipping.\",\n tag_to_monitor: tag_to_monitor,\n product_tags: product.tags\n %}\n{% endif %}\n", "subscriptions": [ "shopify/products/update", "user/task/untag_product" diff --git a/tasks/auto-tag-new-products-by-back-in-stock-age.json b/tasks/auto-tag-new-products-by-back-in-stock-age.json index 2344b67f..7d0f5828 100644 --- a/tasks/auto-tag-new-products-by-back-in-stock-age.json +++ b/tasks/auto-tag-new-products-by-back-in-stock-age.json @@ -14,7 +14,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% assign back_in_stock_inventory_level = options.back_in_stock_inventory_level__number_required %}\n{% assign product_tags_and_maximum_age_in_days = options.product_tags_and_maximum_age_in_days__keyval_number_required %}\n\n{% assign now_s = \"now\" | date: \"%s\"%}\n\n{% if event.topic == \"mechanic/user/trigger\" or event.topic contains \"mechanic/scheduler\" %}\n {% comment %}\n -- run bulk op query for all products in the shop, to get inventory, tags, and metafield values\n {% endcomment %}\n\n {% capture bulk_operation_query %}\n query {\n products {\n edges {\n node {\n __typename\n id\n title\n tags\n totalInventory\n back_in_stock_metafield: metafield(\n namespace: \"mechanic\"\n key: \"back_in_stock_s\"\n ) {\n value\n }\n back_in_stock_first_seen_metafield: metafield(\n namespace: \"mechanic\"\n key: \"back_in_stock_first_seen_s\"\n ) {\n value\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture bulkOperation_objects_jsonl %}\n {\"id\":\"gid:\\/\\/shopify\\/Product\\/1234567890\",\"back_in_stock_metafield\":{\"value\": {{ now_s | json }}},\"back_in_stock_first_seen_metafield\":{\"value\": \"1\"}}\n {\"id\":\"gid:\\/\\/shopify\\/Product\\/2345678901\",\"tags\":[{{ product_tags_and_maximum_age_in_days.first.first | json }}],\"back_in_stock_first_seen_metafield\":{\"value\": \"1\"}}\n {\"id\":\"gid:\\/\\/shopify\\/Product\\/3456789012\",\"tags\":[{{ product_tags_and_maximum_age_in_days.first.first | json }}]}\n {% endcapture %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = bulkOperation_objects_jsonl | parse_jsonl %}\n {% endif %}\n\n {% comment %}\n -- loop through products and decide which tags should be added/removed based only on metafield values; inventory is not checked here\n {% endcomment %}\n\n {% assign first_seen_product_ids = array %}\n\n {% for product in bulkOperation.objects %}\n {% comment %}\n -- check if this product has been seen by this task before; if not, save the ID and move to next product\n {% endcomment %}\n\n {% if product.back_in_stock_first_seen_metafield == blank %}\n {% assign first_seen_product_ids = first_seen_product_ids | push: product.id %}\n {% continue %}\n {% endif %}\n\n {% assign back_in_stock_s = product.back_in_stock_metafield.value | times: 1 %}\n {% assign back_in_stock_first_seen_s = product.back_in_stock_first_seen_metafield.value | times: 1 %}\n\n {% assign do_not_add_tags = nil %}\n\n {% comment %}\n -- don't add tags unless the back in stock timestamp value is newer than the first seen timestamp\n {% endcomment %}\n\n {% unless back_in_stock_s > back_in_stock_first_seen_s %}\n {% assign do_not_add_tags = true %}\n {% endunless %}\n\n {% comment %}\n -- check the threshold times for each tag\n {% endcomment %}\n\n {% assign tags_to_add = array %}\n {% assign tags_to_remove = array %}\n\n {% for keyval in product_tags_and_maximum_age_in_days %}\n {% assign tag = keyval[0] %}\n {% assign maximum_age_s = keyval[1] | times: 86400 %}\n {% assign threshold_timestamp_s = now_s | minus: maximum_age_s %}\n\n {% if back_in_stock_s >= threshold_timestamp_s %}\n {% unless product.tags contains tag or do_not_add_tags %}\n {% assign tags_to_add[tags_to_add.size] = tag %}\n {% endunless %}\n\n {% else %}\n {% comment %}\n -- back in stock metafield doesn't exist or the value doesn't exceed the threshold; remove tag if it is present\n {% endcomment %}\n\n {% if product.tags contains tag %}\n {% assign tags_to_remove[tags_to_remove.size] = tag %}\n {% endif %}\n {% endif %}\n {% endfor %}\n\n {% if tags_to_add != blank or tags_to_remove != blank %}\n {% action \"shopify\" %}\n mutation {\n {% if tags_to_add != blank %}\n tagsAdd(\n id: {{ product.id | json }}\n tags: {{ tags_to_add | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n {% endif %}\n\n {% if tags_to_remove != blank %}\n tagsRemove(\n id: {{ product.id | json }}\n tags: {{ tags_to_remove | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n {% endif %}\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- set a metafield timestamp value of \"now\" on any products that were first seen on this run\n {% endcomment %}\n\n {% if first_seen_product_ids != blank %}\n {% log\n message: \"Products first seen on this task run, which will have a first seen metafield set, but will not have new tags added.\",\n first_seen_product_ids: first_seen_product_ids\n %}\n\n {% assign groups_of_product_ids = first_seen_product_ids | in_groups_of: 25, fill_with: false %}\n\n {% for group_of_product_ids in groups_of_product_ids %}\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {% for product_id in group_of_product_ids %}\n {\n ownerId: {{ product_id | json }}\n namespace: \"mechanic\"\n key: \"back_in_stock_first_seen_s\"\n type: \"number_integer\"\n value: {{ now_s | json }}\n }\n {% endfor %}\n ]\n ) {\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n {% endfor %}\n {% endif %}\n\n{% elsif event.topic contains \"shopify/inventory_levels/\" %}\n {% comment %}\n -- on inventory level changes, check the total inventory and set metafields as needed; no tagging decisions occur here\n {% endcomment %}\n\n {% capture query %}\n query {\n inventoryLevel(id: {{ inventory_level.admin_graphql_api_id | json }}) {\n item {\n variant {\n product {\n id\n totalInventory\n back_in_stock_metafield: metafield(\n namespace: \"mechanic\"\n key: \"back_in_stock_s\"\n ) {\n id\n value\n }\n back_in_stock_first_seen_metafield: metafield(\n namespace: \"mechanic\"\n key: \"back_in_stock_first_seen_s\"\n ) {\n id\n value\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"inventoryLevel\": {\n \"item\": {\n \"variant\": {\n \"product\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"totalInventory\": {{ back_in_stock_inventory_level }}\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign product = result.data.inventoryLevel.item.variant.product %}\n\n {% log product: product, back_in_stock_inventory_level: back_in_stock_inventory_level %}\n\n {% assign metafield_inputs = array %}\n\n {% comment %}\n -- save the first seen time in a metafield if it doesn't already exist\n {% endcomment %}\n\n {% if product.back_in_stock_first_seen_metafield == blank %}\n {% assign metafield_input = hash %}\n {% assign metafield_input[\"namespace\"] = \"mechanic\" %}\n {% assign metafield_input[\"key\"] = \"back_in_stock_first_seen_s\" %}\n {% assign metafield_input[\"type\"] = \"number_integer\" %}\n {% assign metafield_input[\"value\"] = now_s %}\n {% assign metafield_inputs = metafield_inputs | push: metafield_input %}\n {% endif %}\n\n {% if product.totalInventory >= back_in_stock_inventory_level %}\n {% comment %}\n -- product inventory meets the back in stock threshold; save the time in the metafield IF a value does not already exist\n {% endcomment %}\n\n {% if product.back_in_stock_metafield == blank %}\n {% assign metafield_input = hash %}\n {% assign metafield_input[\"namespace\"] = \"mechanic\" %}\n {% assign metafield_input[\"key\"] = \"back_in_stock_s\" %}\n {% assign metafield_input[\"type\"] = \"number_integer\" %}\n {% assign metafield_input[\"value\"] = now_s %}\n {% assign metafield_inputs = metafield_inputs | push: metafield_input %}\n {% endif %}\n\n {% elsif product.back_in_stock_metafield != blank %}\n {% comment %}\n -- product inventory is less than the back in stock threshold and the back in stock metafield exists; delete it\n {% endcomment %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldDelete(\n input: {\n id: {{ product.back_in_stock_metafield.id | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% if metafield_inputs != blank %}\n {% action \"shopify\" %}\n mutation {\n productUpdate(\n input: {\n id: {{ product.id | json }}\n metafields: {{ metafield_inputs | graphql_arguments }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n{% elsif event.topic contains \"shopify/products/create\" %}\n {% comment %}\n -- for new products, set the first seen metafield value using the product created date\n {% endcomment %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ product.admin_graphql_api_id | json }}\n namespace: \"mechanic\"\n key: \"back_in_stock_first_seen_s\"\n type: \"number_integer\"\n value: {{ product.created_at | date: \"%s\" | json }}\n }\n ]\n ) {\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n{% endif %}\n", + "script": "{% assign back_in_stock_inventory_level = options.back_in_stock_inventory_level__number_required %}\n{% assign product_tags_and_maximum_age_in_days = options.product_tags_and_maximum_age_in_days__keyval_number_required %}\n\n{% assign now_s = \"now\" | date: \"%s\"%}\n\n{% if event.topic == \"mechanic/user/trigger\" or event.topic contains \"mechanic/scheduler\" %}\n {% comment %}\n -- run bulk op query for all products in the shop, to get inventory, tags, and metafield values\n {% endcomment %}\n\n {% capture bulk_operation_query %}\n query {\n products {\n edges {\n node {\n __typename\n id\n title\n tags\n totalInventory\n back_in_stock_metafield: metafield(\n namespace: \"mechanic\"\n key: \"back_in_stock_s\"\n ) {\n value\n }\n back_in_stock_first_seen_metafield: metafield(\n namespace: \"mechanic\"\n key: \"back_in_stock_first_seen_s\"\n ) {\n value\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture bulkOperation_objects_jsonl %}\n {\"id\":\"gid:\\/\\/shopify\\/Product\\/1234567890\",\"back_in_stock_metafield\":{\"value\": {{ now_s | json }}},\"back_in_stock_first_seen_metafield\":{\"value\": \"1\"}}\n {\"id\":\"gid:\\/\\/shopify\\/Product\\/2345678901\",\"tags\":[{{ product_tags_and_maximum_age_in_days.first.first | json }}],\"back_in_stock_first_seen_metafield\":{\"value\": \"1\"}}\n {\"id\":\"gid:\\/\\/shopify\\/Product\\/3456789012\",\"tags\":[{{ product_tags_and_maximum_age_in_days.first.first | json }}]}\n {% endcapture %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = bulkOperation_objects_jsonl | parse_jsonl %}\n {% endif %}\n\n {% comment %}\n -- loop through products and decide which tags should be added/removed based only on metafield values; inventory is not checked here\n {% endcomment %}\n\n {% assign first_seen_product_ids = array %}\n\n {% for product in bulkOperation.objects %}\n {% comment %}\n -- check if this product has been seen by this task before; if not, save the ID and move to next product\n {% endcomment %}\n\n {% if product.back_in_stock_first_seen_metafield == blank %}\n {% assign first_seen_product_ids = first_seen_product_ids | push: product.id %}\n {% continue %}\n {% endif %}\n\n {% assign back_in_stock_s = product.back_in_stock_metafield.value | times: 1 %}\n {% assign back_in_stock_first_seen_s = product.back_in_stock_first_seen_metafield.value | times: 1 %}\n\n {% assign do_not_add_tags = nil %}\n\n {% comment %}\n -- don't add tags unless the back in stock timestamp value is newer than the first seen timestamp\n {% endcomment %}\n\n {% unless back_in_stock_s > back_in_stock_first_seen_s %}\n {% assign do_not_add_tags = true %}\n {% endunless %}\n\n {% comment %}\n -- check the threshold times for each tag\n {% endcomment %}\n\n {% assign tags_to_add = array %}\n {% assign tags_to_remove = array %}\n\n {% for keyval in product_tags_and_maximum_age_in_days %}\n {% assign tag = keyval[0] %}\n {% assign maximum_age_s = keyval[1] | times: 86400 %}\n {% assign threshold_timestamp_s = now_s | minus: maximum_age_s %}\n\n {% if back_in_stock_s >= threshold_timestamp_s %}\n {% unless product.tags contains tag or do_not_add_tags %}\n {% assign tags_to_add[tags_to_add.size] = tag %}\n {% endunless %}\n\n {% else %}\n {% comment %}\n -- back in stock metafield doesn't exist or the value doesn't exceed the threshold; remove tag if it is present\n {% endcomment %}\n\n {% if product.tags contains tag %}\n {% assign tags_to_remove[tags_to_remove.size] = tag %}\n {% endif %}\n {% endif %}\n {% endfor %}\n\n {% if tags_to_add != blank or tags_to_remove != blank %}\n {% action \"shopify\" %}\n mutation {\n {% if tags_to_add != blank %}\n tagsAdd(\n id: {{ product.id | json }}\n tags: {{ tags_to_add | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n {% endif %}\n\n {% if tags_to_remove != blank %}\n tagsRemove(\n id: {{ product.id | json }}\n tags: {{ tags_to_remove | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n {% endif %}\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- set a metafield timestamp value of \"now\" on any products that were first seen on this run\n {% endcomment %}\n\n {% if first_seen_product_ids != blank %}\n {% log\n message: \"Products first seen on this task run, which will have a first seen metafield set, but will not have new tags added.\",\n first_seen_product_ids: first_seen_product_ids\n %}\n\n {% assign groups_of_product_ids = first_seen_product_ids | in_groups_of: 25, fill_with: false %}\n\n {% for group_of_product_ids in groups_of_product_ids %}\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {% for product_id in group_of_product_ids %}\n {\n ownerId: {{ product_id | json }}\n namespace: \"mechanic\"\n key: \"back_in_stock_first_seen_s\"\n type: \"number_integer\"\n value: {{ now_s | json }}\n }\n {% endfor %}\n ]\n ) {\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n {% endfor %}\n {% endif %}\n\n{% elsif event.topic contains \"shopify/inventory_levels/\" %}\n {% comment %}\n -- on inventory level changes, check the total inventory and set metafields as needed; no tagging decisions occur here\n {% endcomment %}\n\n {% capture query %}\n query {\n inventoryLevel(id: {{ inventory_level.admin_graphql_api_id | json }}) {\n item {\n variant {\n product {\n id\n totalInventory\n back_in_stock_metafield: metafield(\n namespace: \"mechanic\"\n key: \"back_in_stock_s\"\n ) {\n id\n value\n }\n back_in_stock_first_seen_metafield: metafield(\n namespace: \"mechanic\"\n key: \"back_in_stock_first_seen_s\"\n ) {\n id\n value\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"inventoryLevel\": {\n \"item\": {\n \"variant\": {\n \"product\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"totalInventory\": {{ back_in_stock_inventory_level }}\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign product = result.data.inventoryLevel.item.variant.product %}\n\n {% log product: product, back_in_stock_inventory_level: back_in_stock_inventory_level %}\n\n {% assign metafields_to_set = array %}\n\n {% comment %}\n -- save the first seen time in a metafield if it doesn't already exist\n {% endcomment %}\n\n {% if product.back_in_stock_first_seen_metafield == blank %}\n {% assign metafield_to_set = hash %}\n {% assign metafield_to_set[\"namespace\"] = \"mechanic\" %}\n {% assign metafield_to_set[\"key\"] = \"back_in_stock_first_seen_s\" %}\n {% assign metafield_to_set[\"type\"] = \"number_integer\" %}\n {% assign metafield_to_set[\"value\"] = now_s %}\n {% assign metafields_to_set = metafields_to_set | push: metafield_to_set %}\n {% endif %}\n\n {% if product.totalInventory >= back_in_stock_inventory_level %}\n {% comment %}\n -- product inventory meets the back in stock threshold; save the time in the metafield IF a value does not already exist\n {% endcomment %}\n\n {% if product.back_in_stock_metafield == blank %}\n {% assign metafield_to_set = hash %}\n {% assign metafield_to_set[\"namespace\"] = \"mechanic\" %}\n {% assign metafield_to_set[\"key\"] = \"back_in_stock_s\" %}\n {% assign metafield_to_set[\"type\"] = \"number_integer\" %}\n {% assign metafield_to_set[\"value\"] = now_s %}\n {% assign metafields_to_set = metafields_to_set | push: metafield_to_set %}\n {% endif %}\n\n {% elsif product.back_in_stock_metafield != blank %}\n {% comment %}\n -- product inventory is less than the back in stock threshold and the back in stock metafield exists; delete it\n {% endcomment %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsDelete(\n metafields: [\n {\n ownerId: {{ product.id | json }}\n namespace: \"mechanic\"\n key: \"back_in_stock_s\"\n }\n ]\n ) {\n deletedMetafields {\n ownerId\n namespace\n key\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% if metafields_to_set != blank %}\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: {{ metafields_to_set | graphql_arguments }}\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Product {\n id\n title\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n{% elsif event.topic contains \"shopify/products/create\" %}\n {% comment %}\n -- for new products, set the first seen metafield value using the product created date\n {% endcomment %}\n\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: [\n {\n ownerId: {{ product.admin_graphql_api_id | json }}\n namespace: \"mechanic\"\n key: \"back_in_stock_first_seen_s\"\n type: \"number_integer\"\n value: {{ product.created_at | date: \"%s\" | json }}\n }\n ]\n ) {\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n{% endif %}\n", "subscriptions": [ "shopify/products/create", "shopify/inventory_levels/update", diff --git a/tasks/temporarily-enable-tax-exempt-status-when-a-customer-is-tagged.json b/tasks/temporarily-enable-tax-exempt-status-when-a-customer-is-tagged.json index 52c92a3e..c9b29609 100644 --- a/tasks/temporarily-enable-tax-exempt-status-when-a-customer-is-tagged.json +++ b/tasks/temporarily-enable-tax-exempt-status-when-a-customer-is-tagged.json @@ -9,7 +9,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% assign metafield_namespace = \"mechanic\" %}\n{% assign metafield_key = \"tax-exempt-expiry-scheduled\" %}\n{% assign expiry_event_topic = \"user/customers/expire_tax_exempt\" %}\n\n{% if event.topic contains \"shopify/customers/\" %}\n {% if event.preview %}\n {% capture customer_json %}\n {\n \"admin_graphql_api_id\": \"gid://shopify/Customer/1234567890\",\n \"tags\": {{ options.tax_exempt_tag__required | json }},\n \"metafields\": {\n {{ metafield_namespace | json }}: {\n {{ metafield_key | json }}: null\n }\n }\n }\n {% endcapture %}\n\n {% assign customer = customer_json | parse_json %}\n {% endif %}\n\n {% assign customer_qualifies = false %}\n {% assign customer_tags = customer.tags | split: \", \" %}\n {% if customer_tags contains options.tax_exempt_tag__required %}\n {% if customer.metafields[metafield_namespace][metafield_key] != nil %}\n {% log \"Customer already has their metafield set - skipping\" %}\n {% else %}\n {% assign customer_qualifies = true %}\n {% endif %}\n {% endif %}\n\n {% if customer_qualifies %}\n {% assign expiry_interval_s = 60 | times: 60 | times: 24 | times: options.days_before_removing_tax_exempt_status__number_required %}\n {% assign expiry_time = \"now\" | date: \"%s\" | plus: expiry_interval_s | round %}\n\n {% action \"event\" %}\n {\n \"topic\": {{ expiry_event_topic | json }},\n \"data\": {\n \"customer_id\": {{ customer.admin_graphql_api_id | json }},\n \"customer_tag\": {{ options.tax_exempt_tag__required | json }}\n },\n \"run_at\": {{ expiry_time | json }}\n }\n {% endaction %}\n\n {% action \"shopify\" %}\n mutation {\n customerUpdate(\n input: {\n id: {{ customer.admin_graphql_api_id | json }}\n taxExempt: true\n metafields: [\n {\n namespace: {{ metafield_namespace | json }}\n key: {{ metafield_key | json }}\n type: \"number_integer\"\n value: \"1\"\n }\n ]\n }\n ) {\n customer {\n id\n taxExempt\n metafield(namespace: {{ metafield_namespace | json }}, key: {{ metafield_key | json }}) {\n id\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n{% elsif event.topic == expiry_event_topic %}\n {% capture customer_query %}\n query {\n customer(id: {{ event.data.customer_id | json }}) {\n id\n tags\n metafield(namespace: {{ metafield_namespace | json }}, key: {{ metafield_key | json }}) {\n id\n value\n }\n }\n }\n {% endcapture %}\n\n {% assign customer_result = customer_query | shopify %}\n\n {% if event.preview %}\n {% capture customer_result_json %}\n {\n \"data\": {\n \"customer\": {\n \"id\": \"gid://shopify/Customer/1234567890\",\n \"tags\": [{{ options.tax_exempt_tag__required | json }}],\n \"metafield\": {\n \"id\": \"gid://shopify/Metafield/1234567890\",\n \"value\": \"1\"\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign customer_result = customer_result_json | parse_json %}\n {% endif %}\n\n {% assign customer = customer_result.data.customer %}\n\n {% assign customer_qualifies = false %}\n {% if customer.tags contains options.tax_exempt_tag__required and customer.metafield.value != nil %}\n {% assign customer_qualifies = true %}\n {% endif %}\n\n {% if event.preview or customer_qualifies %}\n {% action \"shopify\" %}\n mutation {\n customerUpdate(\n input: {\n id: {{ customer.id | json }}\n taxExempt: false\n }\n ) {\n userErrors {\n field\n message\n }\n }\n\n tagsRemove(\n id: {{ customer.id | json }}\n tags: {{ options.tax_exempt_tag__required | json }}\n ) {\n node {\n ... on Customer {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n metafieldDelete(\n input: {\n id: {{ customer_result.data.customer.metafield.id | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n{% endif %}", + "script": "{% assign metafield_namespace = \"mechanic\" %}\n{% assign metafield_key = \"tax-exempt-expiry-scheduled\" %}\n{% assign expiry_event_topic = \"user/customers/expire_tax_exempt\" %}\n\n{% if event.topic contains \"shopify/customers/\" %}\n {% if event.preview %}\n {% capture customer_json %}\n {\n \"admin_graphql_api_id\": \"gid://shopify/Customer/1234567890\",\n \"tags\": {{ options.tax_exempt_tag__required | json }},\n \"metafields\": {\n {{ metafield_namespace | json }}: {\n {{ metafield_key | json }}: null\n }\n }\n }\n {% endcapture %}\n\n {% assign customer = customer_json | parse_json %}\n {% endif %}\n\n {% assign customer_qualifies = false %}\n {% assign customer_tags = customer.tags | split: \", \" %}\n\n {% if customer_tags contains options.tax_exempt_tag__required %}\n {% if customer.metafields[metafield_namespace][metafield_key] != nil %}\n {% log \"Customer already has their metafield set - skipping\" %}\n {% else %}\n {% assign customer_qualifies = true %}\n {% endif %}\n {% endif %}\n\n {% if customer_qualifies %}\n {% assign expiry_interval_s = 60 | times: 60 | times: 24 | times: options.days_before_removing_tax_exempt_status__number_required %}\n {% assign expiry_time = \"now\" | date: \"%s\" | plus: expiry_interval_s | round %}\n\n {% action \"event\" %}\n {\n \"topic\": {{ expiry_event_topic | json }},\n \"data\": {\n \"customer_id\": {{ customer.admin_graphql_api_id | json }},\n \"customer_tag\": {{ options.tax_exempt_tag__required | json }}\n },\n \"run_at\": {{ expiry_time | json }}\n }\n {% endaction %}\n\n {% action \"shopify\" %}\n mutation {\n customerUpdate(\n input: {\n id: {{ customer.admin_graphql_api_id | json }}\n taxExempt: true\n metafields: [\n {\n namespace: {{ metafield_namespace | json }}\n key: {{ metafield_key | json }}\n type: \"number_integer\"\n value: \"1\"\n }\n ]\n }\n ) {\n customer {\n id\n taxExempt\n metafield(namespace: {{ metafield_namespace | json }}, key: {{ metafield_key | json }}) {\n id\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n{% elsif event.topic == expiry_event_topic %}\n {% capture customer_query %}\n query {\n customer(id: {{ event.data.customer_id | json }}) {\n id\n tags\n metafield(namespace: {{ metafield_namespace | json }}, key: {{ metafield_key | json }}) {\n id\n value\n }\n }\n }\n {% endcapture %}\n\n {% assign customer_result = customer_query | shopify %}\n\n {% if event.preview %}\n {% capture customer_result_json %}\n {\n \"data\": {\n \"customer\": {\n \"id\": \"gid://shopify/Customer/1234567890\",\n \"tags\": [{{ options.tax_exempt_tag__required | json }}],\n \"metafield\": {\n \"id\": \"gid://shopify/Metafield/1234567890\",\n \"value\": \"1\"\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign customer_result = customer_result_json | parse_json %}\n {% endif %}\n\n {% assign customer = customer_result.data.customer %}\n\n {% assign customer_qualifies = false %}\n {% if customer.tags contains options.tax_exempt_tag__required and customer.metafield.value != nil %}\n {% assign customer_qualifies = true %}\n {% endif %}\n\n {% if event.preview or customer_qualifies %}\n {% action \"shopify\" %}\n mutation {\n customerUpdate(\n input: {\n id: {{ customer.id | json }}\n taxExempt: false\n }\n ) {\n userErrors {\n field\n message\n }\n }\n\n tagsRemove(\n id: {{ customer.id | json }}\n tags: {{ options.tax_exempt_tag__required | json }}\n ) {\n node {\n ... on Customer {\n tags\n }\n }\n userErrors {\n field\n message\n }\n }\n\n metafieldsDelete(\n metafields: [\n {\n ownerId: {{ customer.id | json }}\n namespace: {{ metafield_namespace | json }}\n key: {{ metafield_key | json }}\n }\n ]\n ) {\n deletedMetafields {\n ownerId\n namespace\n key\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n{% endif %}\n", "subscriptions": [ "shopify/customers/update", "user/customers/expire_tax_exempt" diff --git a/tasks/unpublish-products-that-have-been-out-of-stock-for-x-days.json b/tasks/unpublish-products-that-have-been-out-of-stock-for-x-days.json index e61526b7..727c1b68 100644 --- a/tasks/unpublish-products-that-have-been-out-of-stock-for-x-days.json +++ b/tasks/unpublish-products-that-have-been-out-of-stock-for-x-days.json @@ -13,7 +13,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% comment %}\n 1. Watch for updates to inventory levels.\n 2. Look up the associated product, when an update comes in.\n 3. If the product's total inventory is 0, record the current time in a metafield. If the\n total inventory is not 0, delete that metafield, if it exists.\n 4. Scan all products, on a schedule, retrieving each product's out-of-stock time metafield.\n For products found with a total inventory of 0, with a recorded time that's at least a\n configurable distance in days from the current time, unpublish the product.\n{% endcomment %}\n\n{% assign number_of_days_to_wait_before_unpublishing = options.number_of_days_to_wait_before_unpublishing__number_required %}\n{% assign sales_channel_names = options.sales_channel_names__required_array %}\n{% assign only_include_products_matching_this_search_query = options.only_include_products_matching_this_search_query %}\n{% assign test_mode = options.test_mode__boolean %}\n\n{% assign time_format = \"%FT%T%z\" %}\n\n{% if event.topic == \"shopify/inventory_levels/update\" %}\n {% capture query %}\n query {\n inventoryLevel(\n id: {{ inventory_level.admin_graphql_api_id | json }}\n ) {\n id\n item {\n variant {\n product {\n id\n title\n totalInventory\n metafield(\n namespace: \"mechanic\"\n key: \"out_of_stock_at\"\n ) {\n id\n value\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"inventoryLevel\": {\n \"id\": \"gid://shopify/InventoryLevel/1234567890?inventory_item_id=1234567890\",\n \"item\": {\n \"variant\": {\n \"product\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"title\": \"Short sleeve t-shirt\",\n \"totalInventory\": 0,\n \"metafield\": null\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign product = result.data.inventoryLevel.item.variant.product %}\n\n {% if product.totalInventory <= 0 and product.metafield == nil %}\n {% if test_mode %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock. Its out-of-stock time should be recorded.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n\n {% else %}\n {% action \"shopify\" %}\n mutation {\n productUpdate(\n input: {\n id: {{ product.id | json }}\n metafields: [\n {\n namespace: \"mechanic\"\n key: \"out_of_stock_at\"\n type: \"date_time\"\n value: {{ \"now\" | date: time_format | json }}\n }\n ]\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% elsif product.totalInventory > 0 and product.metafield != nil %}\n {% if test_mode %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is back in stock. Its out-of-stock time should be cleared.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n\n {% else %}\n {% action \"shopify\" %}\n mutation {\n metafieldDelete(\n input: {\n id: {{ product.metafield.id | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endif %}\n\n{% elsif event.topic contains \"mechanic/scheduler/\" or event.topic == \"mechanic/user/trigger\" %}\n {% assign now_s = \"now\" | date: \"%s\" | times: 1 %}\n {% assign minimum_out_of_stock_at_distance_s = number_of_days_to_wait_before_unpublishing | times: 24 | times: 60 | times: 60 %}\n\n {% capture query %}\n query {\n publications(first: 250) {\n nodes {\n id\n name\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% assign publications = array %}\n {% assign metafields_to_set = array %}\n\n {% for publication in result.data.publications.nodes %}\n {% if sales_channel_names contains publication.name %}\n {% assign publications[publications.size] = publication %}\n {% endif %}\n {% endfor%}\n\n {% if event.preview %}\n {% assign publications[0] = hash %}\n {% assign publications[0][\"id\"] = \"gid://shopify/Publication/1234567890\" %}\n\n {% elsif publications.size != sales_channel_names.size %}\n {% log\n publications_named: sales_channel_names,\n publications_available: result.data.publications.nodes,\n publications_matched: publications\n %}\n {% error \"Unable to find all named publications. Double-check your task configuration.\" %}\n {% endif %}\n\n {% assign search_query = \"inventory_total:<=0\" %}\n\n {% if only_include_products_matching_this_search_query != blank %}\n {% capture search_query -%}\n {{ search_query }} AND ({{ only_include_products_matching_this_search_query }})\n {%- endcapture %}\n {% log search_query: search_query %}\n {% endif %}\n\n {% assign cursor = nil %}\n\n {% for n in (0..100) %}\n {% capture query %}\n query {\n products(\n first: 250\n query: {{ search_query | json }}\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n title\n totalInventory\n {% for publication in publications %}\n publishedOnPublication{{ forloop.index }}: publishedOnPublication(publicationId: {{ publication.id | json }})\n {% endfor %}\n metafield(\n namespace: \"mechanic\"\n key: \"out_of_stock_at\"\n ) {\n id\n value\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"title\": \"Short sleeve t-shirt\",\n \"totalInventory\": 0,\n {% for publication in publications %}\n {{ \"publishedOnPublication\" | append: forloop.index | json }}: true,\n {% endfor %}\n \"metafield\": {\n \"value\": {{ now_s | minus: minimum_out_of_stock_at_distance_s | minus: 1 | append: \"\" | json }}\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% for product in result.data.products.nodes %}\n {% if product.metafield == nil %}\n {% log %}\n {% capture message %}Product {{ product.id }} is out of stock, but its out-of-stock time was not recorded. This product will not be unpublished now, but its out-of-stock time will be set to the current time.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n\n {% assign metafield_to_set = hash %}\n {% assign metafield_to_set[\"ownerId\"] = product.id %}\n {% assign metafield_to_set[\"namespace\"] = \"mechanic\" %}\n {% assign metafield_to_set[\"key\"] = \"out_of_stock_at\" %}\n {% assign metafield_to_set[\"type\"] = \"date_time\" %}\n {% assign metafield_to_set[\"value\"] = \"now\" | date: time_format %}\n {% assign metafields_to_set = metafields_to_set | push: metafield_to_set %}\n\n {% continue %}\n {% endif %}\n\n {% assign out_of_stock_at_s = product.metafield.value | date: \"%s\" | times: 1 %}\n {% assign out_of_stock_at_distance_s = now_s | minus: out_of_stock_at_s %}\n\n {% assign unpublishings = array %}\n\n {% for publication in publications %}\n {% assign key = \"publishedOnPublication\" | append: forloop.index %}\n {% if product[key] %}\n {% assign unpublishing = array %}\n {% assign unpublishing[0] = product.id %}\n {% assign unpublishing[1] = publication.id %}\n {% assign unpublishings[unpublishings.size] = unpublishing %}\n {% endif %}\n {% endfor %}\n\n {% if unpublishings == empty %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) has been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s), but is not published - nothing to do.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n {% continue %}\n {% endif %}\n\n {% if out_of_stock_at_distance_s < minimum_out_of_stock_at_distance_s %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock, and is published, but has only been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s).{% endcapture %}\n {{ message | json }}\n {% endlog %}\n {% continue %}\n {% endif %}\n\n {% if test_mode %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock, and has been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s). It should be unpublished.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n\n {% else %}\n {% action \"shopify\" %}\n mutation {\n {% for unpublishing in unpublishings %}\n publishableUnpublish{{ forloop.index }}: publishableUnpublish(\n id: {{ unpublishing[0] | json }}\n input: {\n publicationId: {{ unpublishing[1] | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n {% endfor %}\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n\n {% if result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n{% endif %}\n\n{% if metafields_to_set != blank %}\n {% if test_mode %}\n {% log %}\n \"Found {{ metafields_to_set.size }} out of stock products which do not yet have their out of stock time recorded. This task has test mode enabled, so these metafields will not be set on this task run.\"\n {% endlog %}\n\n {% break %}\n {% endif %}\n\n {% assign groups_of_metafields_to_set = metafields_to_set | in_groups_of: 25, fill_with: false %}\n\n {% for group_of_metafields_to_set in groups_of_metafields_to_set %}\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: {{ group_of_metafields_to_set | graphql_arguments }}\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Product {\n id\n title\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n {% endfor %}\n{% endif %}\n", + "script": "{% comment %}\n 1. Watch for updates to inventory levels.\n 2. Look up the associated product, when an update comes in.\n 3. If the product's total inventory is <=0, record the current time in a metafield. If the\n total inventory is not 0, delete that metafield, if it exists.\n 4. Scan all products, on a schedule, retrieving each product's out-of-stock time metafield.\n For products found with a total inventory of <=0, with a recorded time that's at least a\n configurable distance in days from the current time, unpublish the product.\n{% endcomment %}\n\n{% assign number_of_days_to_wait_before_unpublishing = options.number_of_days_to_wait_before_unpublishing__number_required %}\n{% assign sales_channel_names = options.sales_channel_names__required_array %}\n{% assign only_include_products_matching_this_search_query = options.only_include_products_matching_this_search_query %}\n{% assign test_mode = options.test_mode__boolean %}\n\n{% assign time_format = \"%FT%T%z\" %}\n\n{% if event.topic == \"shopify/inventory_levels/update\" %}\n {% capture query %}\n query {\n inventoryLevel(\n id: {{ inventory_level.admin_graphql_api_id | json }}\n ) {\n id\n item {\n variant {\n product {\n id\n title\n totalInventory\n metafield(\n namespace: \"mechanic\"\n key: \"out_of_stock_at\"\n ) {\n id\n value\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"inventoryLevel\": {\n \"id\": \"gid://shopify/InventoryLevel/1234567890?inventory_item_id=1234567890\",\n \"item\": {\n \"variant\": {\n \"product\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"title\": \"Short sleeve t-shirt\",\n \"totalInventory\": 0,\n \"metafield\": null\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign product = result.data.inventoryLevel.item.variant.product %}\n\n {% if product.totalInventory <= 0 and product.metafield == nil %}\n {% if test_mode %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock. Its out-of-stock time should be recorded.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n\n {% else %}\n {% action \"shopify\" %}\n mutation {\n productUpdate(\n input: {\n id: {{ product.id | json }}\n metafields: [\n {\n namespace: \"mechanic\"\n key: \"out_of_stock_at\"\n type: \"date_time\"\n value: {{ \"now\" | date: time_format | json }}\n }\n ]\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n {% elsif product.totalInventory > 0 and product.metafield != nil %}\n {% if test_mode %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is back in stock. Its out-of-stock time should be cleared.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n\n {% else %}\n {% action \"shopify\" %}\n mutation {\n metafieldsDelete(\n metafields: [\n {\n ownerId: {{ product.id | json }}\n namespace: \"mechanic\"\n key: \"out_of_stock_at\"\n }\n ]\n ) {\n deletedMetafields {\n ownerId\n namespace\n key\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endif %}\n\n{% elsif event.topic contains \"mechanic/scheduler/\" or event.topic == \"mechanic/user/trigger\" %}\n {% assign now_s = \"now\" | date: \"%s\" | times: 1 %}\n {% assign minimum_out_of_stock_at_distance_s = number_of_days_to_wait_before_unpublishing | times: 24 | times: 60 | times: 60 %}\n\n {% capture query %}\n query {\n publications(first: 250) {\n nodes {\n id\n name\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% assign publications = array %}\n {% assign metafields_to_set = array %}\n\n {% for publication in result.data.publications.nodes %}\n {% if sales_channel_names contains publication.name %}\n {% assign publications[publications.size] = publication %}\n {% endif %}\n {% endfor%}\n\n {% if event.preview %}\n {% assign publications[0] = hash %}\n {% assign publications[0][\"id\"] = \"gid://shopify/Publication/1234567890\" %}\n\n {% elsif publications.size != sales_channel_names.size %}\n {% log\n publications_named: sales_channel_names,\n publications_available: result.data.publications.nodes,\n publications_matched: publications\n %}\n {% error \"Unable to find all named publications. Double-check your task configuration.\" %}\n {% endif %}\n\n {% assign search_query = \"inventory_total:<=0\" %}\n\n {% if only_include_products_matching_this_search_query != blank %}\n {% capture search_query -%}\n {{ search_query }} AND ({{ only_include_products_matching_this_search_query }})\n {%- endcapture %}\n {% log search_query: search_query %}\n {% endif %}\n\n {% assign cursor = nil %}\n\n {% for n in (0..100) %}\n {% capture query %}\n query {\n products(\n first: 250\n query: {{ search_query | json }}\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n title\n totalInventory\n {% for publication in publications %}\n publishedOnPublication{{ forloop.index }}: publishedOnPublication(publicationId: {{ publication.id | json }})\n {% endfor %}\n metafield(\n namespace: \"mechanic\"\n key: \"out_of_stock_at\"\n ) {\n id\n value\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"title\": \"Short sleeve t-shirt\",\n \"totalInventory\": 0,\n {% for publication in publications %}\n {{ \"publishedOnPublication\" | append: forloop.index | json }}: true,\n {% endfor %}\n \"metafield\": {\n \"value\": {{ now_s | minus: minimum_out_of_stock_at_distance_s | minus: 1 | append: \"\" | json }}\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% for product in result.data.products.nodes %}\n {% if product.metafield == nil %}\n {% log %}\n {% capture message %}Product {{ product.id }} is out of stock, but its out-of-stock time was not recorded. This product will not be unpublished now, but its out-of-stock time will be set to the current time.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n\n {% assign metafield_to_set = hash %}\n {% assign metafield_to_set[\"ownerId\"] = product.id %}\n {% assign metafield_to_set[\"namespace\"] = \"mechanic\" %}\n {% assign metafield_to_set[\"key\"] = \"out_of_stock_at\" %}\n {% assign metafield_to_set[\"type\"] = \"date_time\" %}\n {% assign metafield_to_set[\"value\"] = \"now\" | date: time_format %}\n {% assign metafields_to_set = metafields_to_set | push: metafield_to_set %}\n\n {% continue %}\n {% endif %}\n\n {% assign out_of_stock_at_s = product.metafield.value | date: \"%s\" | times: 1 %}\n {% assign out_of_stock_at_distance_s = now_s | minus: out_of_stock_at_s %}\n\n {% assign unpublishings = array %}\n\n {% for publication in publications %}\n {% assign key = \"publishedOnPublication\" | append: forloop.index %}\n {% if product[key] %}\n {% assign unpublishing = array %}\n {% assign unpublishing[0] = product.id %}\n {% assign unpublishing[1] = publication.id %}\n {% assign unpublishings[unpublishings.size] = unpublishing %}\n {% endif %}\n {% endfor %}\n\n {% if unpublishings == empty %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) has been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s), but is not published - nothing to do.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n {% continue %}\n {% endif %}\n\n {% if out_of_stock_at_distance_s < minimum_out_of_stock_at_distance_s %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock, and is published, but has only been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s).{% endcapture %}\n {{ message | json }}\n {% endlog %}\n {% continue %}\n {% endif %}\n\n {% if test_mode %}\n {% log %}\n {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock, and has been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s). It should be unpublished.{% endcapture %}\n {{ message | json }}\n {% endlog %}\n\n {% else %}\n {% action \"shopify\" %}\n mutation {\n {% for unpublishing in unpublishings %}\n publishableUnpublish{{ forloop.index }}: publishableUnpublish(\n id: {{ unpublishing[0] | json }}\n input: {\n publicationId: {{ unpublishing[1] | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n {% endfor %}\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n\n {% if result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n{% endif %}\n\n{% if metafields_to_set != blank %}\n {% if test_mode %}\n {% log %}\n \"Found {{ metafields_to_set.size }} out of stock products which do not yet have their out of stock time recorded. This task has test mode enabled, so these metafields will not be set on this task run.\"\n {% endlog %}\n\n {% break %}\n {% endif %}\n\n {% assign groups_of_metafields_to_set = metafields_to_set | in_groups_of: 25, fill_with: false %}\n\n {% for group_of_metafields_to_set in groups_of_metafields_to_set %}\n {% action \"shopify\" %}\n mutation {\n metafieldsSet(\n metafields: {{ group_of_metafields_to_set | graphql_arguments }}\n ) {\n metafields {\n id\n namespace\n key\n type\n value\n owner {\n ... on Product {\n id\n title\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n {% endfor %}\n{% endif %}\n", "subscriptions": [ "shopify/inventory_levels/update", "mechanic/scheduler/hourly",