-
Notifications
You must be signed in to change notification settings - Fork 35
/
Copy pathmove-out-of-stock-products-to-the-end-of-a-collection.json
26 lines (26 loc) · 15 KB
/
move-out-of-stock-products-to-the-end-of-a-collection.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
{
"docs": "This task re-sorts your collections, beginning with the sort order of your choice (alphabetically, best selling first, etc), and then moving all out-of-stock products to the very end of the collection.\n\nRun this task manually to re-sort your collections on demand. Optionally, configure this task to run hourly or nightly as well.\n\nBy default, this task will run against **ALL** of your collections. Alternatively, you may configure this task to only _include_ certain collections using each collection's handle, or its ID. [Learn how to find the collection IDs.](https://learn.mechanic.dev/techniques/finding-a-resource-id)\n\nConversely, you may configure this task to _exclude_ certain collections using each collection's handle, or its ID, in which case it will run against all collections except the ones in this list. [Note: if there are any collections entered into the inclusion list, then the exclusion list will be ignored.]\n\nThe combination of inclusion and exclusion options _can_ allow multiple copies of this task to run (to use different base sorting for instance), provided they are configured properly.\n\nThis task will skip any collections it encounters if the collection sorting is not already set to manual. Check the \"Force manual sorting on collections\" option to have the task update those collections to the manual sorting required by this task.\n\nYou may use any of these options for the base sort order:\n\n* MANUAL\n* ALPHA_ASC\n* ALPHA_DESC\n* BEST_SELLING\n* CREATED\n* CREATED_DESC\n* PRICE_ASC\n* PRICE_DESC\n\n__Note__: To function correctly, the \"Perform action runs in sequence\" option should stay enabled in the task's advanced settings.",
"halt_action_run_sequence_on_error": false,
"name": "Move out-of-stock products to the end of a collection",
"online_store_javascript": null,
"options": {
"base_sort_order__required": "ALPHA_ASC",
"collection_handles_or_ids_to_include__array": null,
"collection_handles_or_ids_to_exclude__array": null,
"force_manual_sorting_on_collections__boolean": false,
"run_hourly__boolean": false,
"run_daily__boolean": false
},
"order_status_javascript": null,
"perform_action_runs_in_sequence": true,
"script": "{% assign base_sort_order = options.base_sort_order__required %}\n{% assign collection_handles_or_ids_to_include = options.collection_handles_or_ids_to_include__array %}\n{% assign collection_handles_or_ids_to_exclude = options.collection_handles_or_ids_to_exclude__array %}\n{% assign force_manual_sorting_on_collections = options.force_manual_sorting_on_collections__boolean %}\n\n{% assign allowed_base_sort_orders = \"MANUAL,BEST_SELLING,ALPHA_ASC,ALPHA_DESC,PRICE_DESC,PRICE_ASC,CREATED_DESC,CREATED\" | split: \",\" %}\n\n{% unless allowed_base_sort_orders contains base_sort_order %}\n {% error %}\n {{ allowed_base_sort_orders | join: \", \" | prepend: \"Base sort order must be one of: \" | json }}\n {% enderror %}\n{% endunless %}\n\n{% log %}\n {{ base_sort_order | prepend: \"Base sort order for this task run: \" | json }}\n{% endlog %}\n\n{% assign product_sort_order = base_sort_order %}\n{% assign reverse_sort = nil %}\n\n{% case product_sort_order %}\n {% when \"ALPHA_ASC\" %}\n {% assign product_sort_order = \"TITLE\" %}\n\n {% when \"ALPHA_DESC\" %}\n {% assign product_sort_order = \"TITLE\" %}\n {% assign reverse_sort = true %}\n\n {% when \"CREATED_DESC\" %}\n {% assign product_sort_order = \"CREATED\" %}\n {% assign reverse_sort = true %}\n\n {% when \"PRICE_ASC\" %}\n {% assign product_sort_order = \"PRICE\" %}\n\n {% when \"PRICE_DESC\" %}\n {% assign product_sort_order = \"PRICE\" %}\n {% assign reverse_sort = true %}\n{% endcase %}\n\n{% comment %}\n -- query all collections in the shop; do not use a query filter for IDs or handles here since those configuration fields are optional\n{% endcomment %}\n\n{% assign cursor = nil %}\n{% assign collections = array %}\n\n{% for n in (1..100) %}\n {% capture query %}\n query {\n collections(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n legacyResourceId\n title\n handle\n sortOrder\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"collections\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Collection/1234567890\",\n \"legacyResourceId\": {{ collection_handles_or_ids_to_include.first | default: \"1234567890\" | json }},\n \"title\": \"Samples\",\n \"handle\": {{ collection_handles_or_ids_to_include.first | json }},\n \"sortOrder\": \"MANUAL\"\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign collections = collections | concat: result.data.collections.nodes %}\n\n {% if result.data.collections.pageInfo.hasNextPage %}\n {% assign cursor = result.data.collections.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}\n\n{% comment %}\n -- loop through collections and filter by ID and/or handle as configured\n{% endcomment %}\n\n{% for collection in collections %}\n {% if collection_handles_or_ids_to_include != blank %}\n {% unless collection_handles_or_ids_to_include contains collection.legacyResourceId\n or collection_handles_or_ids_to_include contains collection.handle %}\n {% continue %}\n {% endunless %}\n\n {% elsif collection_handles_or_ids_to_exclude != blank %}\n {% if collection_handles_or_ids_to_exclude contains collection.legacyResourceId\n or collection_handles_or_ids_to_exclude contains collection.handle %}\n {% continue %}\n {% endif %}\n {% endif %}\n\n {% comment %}\n -- make sure collection is configured for manual sorting, and optionally update it if not\n {% endcomment %}\n\n {% if collection.sortOrder != \"MANUAL\" %}\n {% if force_manual_sorting_on_collections %}\n {% action \"shopify\" %}\n mutation {\n collectionUpdate(\n input: {\n id: {{ collection.id | json }}\n sortOrder: MANUAL\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% else %}\n {% log %}\n {{ collection.title | json | append: \" is not configured for manual sorting; skipping.\" | json }}\n {% endlog %}\n\n {% continue %}\n {% endif %}\n {% endif %}\n\n {% comment %}\n -- get all product IDs in this collection using the current sort order\n {% endcomment %}\n\n {% assign all_product_ids_current_sort = array %}\n {% assign cursor = nil %}\n\n {% for n in (1..100) %}\n {% capture query %}\n query {\n collection(id: {{ collection.id | json }}) {\n products(\n sortKey: COLLECTION_DEFAULT\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% assign product_ids_batch = result.data.collection.products.nodes | map: \"id\" %}\n {% assign all_product_ids_current_sort = all_product_ids_current_sort | concat: product_ids_batch %}\n\n {% if result.data.collection.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.collection.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- get all products in this collection using the configured sort order; get variants later if needed in order to support 2K variant limit\n {% endcomment %}\n\n {% assign in_stock_product_ids = array %}\n {% assign out_of_stock_product_ids = array %}\n {% assign cursor = nil %}\n\n {% for n in (1..100) %}\n {% capture query %}\n query {\n collection(id: {{ collection.id | json }}) {\n products(\n sortKey: {{ product_sort_order }}\n reverse: {{ reverse_sort | json }}\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n tracksInventory\n hasOutOfStockVariants\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 \"collection\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"tracksInventory\": true,\n \"hasOutOfStockVariants\": true\n },\n {\n \"id\": \"gid://shopify/Product/2345678901\",\n \"tracksInventory\": true,\n \"hasOutOfStockVariants\": false\n },\n {\n \"id\": \"gid://shopify/Product/3456789012\",\n \"tracksInventory\": false\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% comment %}\n -- split products into in-stock and out-of-stock buckets, keeping the configured sort order\n {% endcomment %}\n\n {% for product in result.data.collection.products.nodes %}\n {% assign has_in_stock_variant = nil %}\n\n {% unless product.tracksInventory and product.hasOutOfStockVariants %}\n {% assign has_in_stock_variant = true %}\n\n {% else %}\n {% comment %}\n -- query up to 2K variants to see if any of them are in stock; can break as soon as one is found\n {% endcomment %}\n\n {% assign variants_cursor = nil %}\n\n {% for n in (1..8) %}\n {% capture query %}\n query {\n product(id: {{ product.id | json }}) {\n variants(\n first: 250\n after: {{ variants_cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n inventoryPolicy\n inventoryQuantity\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign variants_result = query | shopify %}\n\n {% if event.preview %}\n {% capture variants_result_json %}\n {\n \"data\": {\n \"product\": {\n \"variants\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/ProductVariant/1234567890\",\n \"inventoryPolicy\": \"DENY\",\n \"inventoryQuantity\": 0\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign variants_result = variants_result_json | parse_json %}\n {% endif %}\n\n {% for variant in variants_result.data.product.variants.nodes %}\n {% if variant.inventoryPolicy == \"CONTINUE\" or variant.inventoryQuantity > 0 %}\n {% assign has_in_stock_variant = true %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if variants_result.data.product.variants.pageInfo.hasNextPage and has_in_stock_variant != true %}\n {% assign variants_cursor = variants_result.data.product.variants.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n {% endunless %}\n\n {% if has_in_stock_variant %}\n {% assign in_stock_product_ids = in_stock_product_ids | push: product.id %}\n {% else %}\n {% assign out_of_stock_product_ids = out_of_stock_product_ids | push: product.id %}\n {% endif %}\n {% endfor %}\n\n {% if result.data.collection.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.collection.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- combine product IDs back into single array with all out of stock variants following the in stock ones, but otherwise keeping the configured sort order\n {% endcomment %}\n\n {% assign all_product_ids = in_stock_product_ids | concat: out_of_stock_product_ids %}\n\n {% comment %}\n -- determine which product IDs need to be moved by comparing to original sort order\n {% endcomment %}\n\n {% assign moves = array %}\n\n {% for product_id in all_product_ids %}\n {% if all_product_ids_current_sort[forloop.index0] != product_id %}\n {% assign move = hash %}\n {% assign move[\"id\"] = product_id %}\n {% assign move[\"newPosition\"] = \"\" | append: forloop.index0 %}\n {% assign moves = moves | push: move %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- move to next collection if no moves are necessary\n {% endcomment %}\n\n {% if moves == blank %}\n {% log\n message: \"No position moves necessary for this collection, everything is already in its appropriate sort order.\",\n collection: collection.title\n %}\n {% continue %}\n {% endif %}\n\n {% log\n message: \"Scheduling job(s) to reorder products for this collection.\",\n collection: collection.title,\n moves_count: moves.size\n %}\n\n {% comment %}\n -- using reverse filter below due to a bug in the collectionReorderProducts mutation\n -- this filter will NOT affect the sort order determined above\n {% endcomment %}\n\n {% assign move_groups = moves | reverse | in_groups_of: 250, fill_with: false %}\n\n {% for move_group in move_groups %}\n {% action \"shopify\" %}\n mutation {\n collectionReorderProducts(\n id: {{ collection.id | json }}\n moves: {{ move_group | graphql_arguments }}\n ) {\n job {\n id\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endfor %}\n{% endfor %}\n",
"subscriptions": [
"mechanic/user/trigger"
],
"subscriptions_template": "mechanic/user/trigger\n{% if options.run_hourly__boolean %}\n mechanic/scheduler/hourly\n{% elsif options.run_daily__boolean %}\n mechanic/scheduler/daily\n{% endif %}",
"tags": [
"Collections",
"Out of Stock",
"Sort"
]
}