forked from lightward/mechanic-tasks
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathadvanced-scheduled-price-changes.json
35 lines (35 loc) · 54 KB
/
advanced-scheduled-price-changes.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"docs": "This advanced task allows you to schedule price change events for your store, useful for seasonal and flash sales. To use this task, configure a price change event using the task options, save the task and run it manually, and then enter a keyword of **schedule** in the dialog that appears. An email summary with the price change event ID will be sent upon successful scheduling.\n\n#### This task supports these keyword actions:\n\n- **schedule** - Validates and schedules a new price change event using the current task configuration.\n- **list** - Logs out (in Mechanic) all configured price change events (of any status).\n- **email** - Emails a summary of all configured price change events (of any status).\n- **cancel** - Cancels a specific scheduled or ongoing price change event. The cancel keyword must be followed by a space and the _price change event ID_.\n- **reset** - Clears all price change events (of any status) from the shop metafield, and reverts the prices on any variants that have a price change event applied.\n\n#### Key features of this task:\n\n- Ability to schedule overlapping price change events\n- Ability to cancel an ongoing event\n- Ability to add collections that contain more than 1000 products\n- Ability to discount more than 20k products\n- Supports percentage, fixed price, and fixed discount options for products AND collections\n- Option to include specific SKUs in a list with a specific discount for the whole list\n- Option to exclude SKUs from any discount qualification\n- Option to exclude products by tag from any discount qualification\n- Option to have (or not have) compare at prices set to original prices for the duration of an event\n\n#### Price change event configuration notes\n\n- The event start and end datetimes must be valid dates in the form of _YYYY-MM-DD_ or _YYYY-MM-DD HH:MM_ (24 hour clock).\n- Collection handles and discounts are entered as key value pairs, with the collection handle on the left and the discount for it on the right.\n- If any SKUs are specifically included, then a SKU discount must be set to apply to them.\n- Discount formats:\n - Percentage - This must be an integer between 1 and 99, followed immediately by % (e.g. 20%)\n - Fixed price - Any positive integer or float will be considered the fixed price that all applicable items will be discounted to (e.g. 123.45)\n - Fixed discount - Any negative integer or float will be considered the discount to be substracted from the price of all applicable items (e.g. -25)\n- Priority of discount assignment when a variant qualifies for multiple discounts within a price change event:\n - Exclusions (by SKU or product tag) will always override inclusion\n - SKU inclusion list\n - Collection membership in order of configuration\n\n#### Important Notes:\n\n- An email notification will be sent for every price change event scheduled, started, completed, and cancelled. If an email is not received as expected, then the task run logs should be reviewed.\n- Price change events that affect a very large number of variants will take some time to process all of the price updates. The notification emails are sent after all variant prices have been updated.\n- Just saving the task configuration will not schedule (or validate) a price change event. This must be done with the \"schedule\" keyword when run manually.\n- This task only manages variant prices. Product and theme publishing are not handled by this task.\n- Variants that are already part of an ongoing price change event will not be modified if targeted by a new price change event.",
"halt_action_run_sequence_on_error": false,
"name": "Advanced: Scheduled Price Changes",
"online_store_javascript": null,
"options": {
"notification_email_recipients__array_required": null,
"event_start_datetime__required": null,
"event_end_datetime__required": null,
"set_compare_at_prices_to_original_price_during_event__boolean": false,
"collection_handles_and_discounts__keyval": null,
"skus_to_include__array": null,
"sku_discount": null,
"exclude_products_tagged_with__array": null,
"skus_to_exclude__array": null
},
"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<a href=\"https://{{ shop.myshopify_domain }}/admin/apps/mechanic/~/tasks/edit/{{ task.id }}\">{{ task.name | remove: \"ADVANCED: \" }}</a>\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 <strong>Price change event ID:</strong> {{ price_change_event_id }}\n <strong>Status:</strong> {{ price_change_event[\"status\"] }}\n <strong>Event start:</strong> {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n <strong>Event end:</strong> {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n <strong>Set compare at price to original price during event:</strong> {{ price_change_event[\"set_compare_at_prices\"] }}\n <strong>Collection handles and discounts:</strong>\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n <strong>SKUs to include:</strong> {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n <strong>SKU discount:</strong> {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n <strong>SKUs to exclude:</strong> {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n <strong>Exclude products tagged with:</strong> {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n\n <em><strong>Note:</strong> 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.</em>\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 <strong>Price change event ID:</strong> {{ price_change_event_id }}\n <strong>Status:</strong> {{ price_change_event_data[\"status\"] }}\n <strong>Event start:</strong> {{ price_change_event_data[\"start\"] | date: \"%F %H:%M %z\" }}\n <strong>Event end:</strong> {{ price_change_event_data[\"end\"] | date: \"%F %H:%M %z\" }}\n <strong>Set compare at price to original price during event:</strong> {{ price_change_event_data[\"set_compare_at_prices\"] }}\n <strong>Collection handles and discounts:</strong>\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 <strong>SKUs to include:</strong> {{ price_change_event_data[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n <strong>SKU discount:</strong> {{ price_change_event_data[\"sku_discount\"] | default: \"n/a\" }},\n <strong>SKUs to exclude:</strong> {{ price_change_event_data[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n <strong>Exclude products tagged with:</strong> {{ price_change_event_data[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n <hr />\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 <strong>Price change event ID:</strong> {{ price_change_event_id }}\n <strong>Status:</strong> {{ price_change_event[\"status\"] }}\n <strong>Event start:</strong> {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n <strong>Event end:</strong> {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n <strong>Set compare at price to original price during event:</strong> {{ price_change_event[\"set_compare_at_prices\"] }}\n <strong>Collection handles and discounts:</strong>\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n <strong>SKUs to include:</strong> {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n <strong>SKU discount:</strong> {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n <strong>SKUs to exclude:</strong> {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n <strong>Exclude products tagged with:</strong> {{ 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 <strong>Price change event ID:</strong> {{ price_change_event_id }}\n <strong>Status:</strong> {{ price_change_event[\"status\"] }}\n <strong>Event start:</strong> {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n <strong>Event end:</strong> {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n <strong>Set compare at price to original price during event:</strong> {{ price_change_event[\"set_compare_at_prices\"] }}\n <strong>Collection handles and discounts:</strong>\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n <strong>SKUs to include:</strong> {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n <strong>SKU discount:</strong> {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n <strong>SKUs to exclude:</strong> {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n <strong>Exclude products tagged with:</strong> {{ price_change_event[\"exclude_products_tagged_with\"] | join: \", \" | default: \"n/a\" }}\n\n <em><strong>Note:</strong> To cancel this event while it is ongoing, use the \"cancel\" keyword along with the price change event ID when running the task.</em>\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 <strong>Price change event ID:</strong> {{ price_change_event_id }}\n <strong>Status:</strong> {{ price_change_event[\"status\"] }}\n <strong>Event start:</strong> {{ price_change_event[\"start\"] | date: \"%F %H:%M %z\" }}\n <strong>Event end:</strong> {{ price_change_event[\"end\"] | date: \"%F %H:%M %z\" }}\n <strong>Set compare at price to original price during event:</strong> {{ price_change_event[\"set_compare_at_prices\"] }}\n <strong>Collection handles and discounts:</strong>\n {% for keyval in price_change_event[\"collection_handles_and_discounts\"] -%}\n - {{ keyval[0] }}: {{ keyval[1] }}\n {% else -%}\n n/a\n {%- endfor %}\n <strong>SKUs to include:</strong> {{ price_change_event[\"skus_to_include\"] | join: \", \" | default: \"n/a\" }}\n <strong>SKU discount:</strong> {{ price_change_event[\"sku_discount\"] | default: \"n/a\" }},\n <strong>SKUs to exclude:</strong> {{ price_change_event[\"skus_to_exclude\"] | join: \", \" | default: \"n/a\" }}\n <strong>Exclude products tagged with:</strong> {{ 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 %}",
"subscriptions": [
"mechanic/user/text",
"user/price_changes/start",
"user/price_changes/end",
"user/price_changes/reset"
],
"subscriptions_template": "mechanic/user/text\nuser/price_changes/start\nuser/price_changes/end\nuser/price_changes/reset",
"tags": [
"Advanced",
"Discounts",
"Price",
"Products",
"Sale",
"Schedule"
]
}