forked from lightward/mechanic-tasks
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathauto-sort-collections-by-inventory-levels.json
28 lines (28 loc) · 13.6 KB
/
auto-sort-collections-by-inventory-levels.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
{
"docs": "This task re-sorts your collections, beginning with the sort order of your choice (alphabetically, best selling first, etc), and then by total inventory in descending order. Products whose inventory is not tracked (i.e. limitless inventory) will be moved to the top and out-of-stock products (inventory <= 0) will be moved to the bottom. Optionally, choose to sort by the sellable online quantity (i.e. locations enabled for online order fulfillment) instead of the total inventory for a product across all locations.\n\nRun this task manually to re-sort your collections on demand. It may be configured 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": "Auto-sort collections by inventory levels",
"online_store_javascript": null,
"options": {
"base_sort_order__required": "ALPHA_ASC",
"collection_handles_or_ids_to_include__array": [],
"collection_handles_or_ids_to_exclude__array": [],
"force_manual_sorting_on_collections__boolean": false,
"use_sellable_online_quantity_instead_of_total_inventory__boolean": false,
"run_hourly__boolean": false,
"run_daily__boolean": false
},
"order_status_javascript": null,
"perform_action_runs_in_sequence": true,
"preview_event_definitions": [],
"script": "{% 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 options.base_sort_order__required %}\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 {{ options.base_sort_order__required | prepend: \"Base sort order for this task run: \" | json }}\n{% endlog %}\n\n{% assign product_sort_order = options.base_sort_order__required %}\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{% 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{% assign use_sellable_online_quantity = options.use_sellable_online_quantity_instead_of_total_inventory__boolean %}\n\n{% assign collections = shop.collections %}\n\n{% if event.preview %}\n {% capture collections_json %}\n [\n {\n \"id\": {{ collection_handles_or_ids_to_include.first | default: \"1234567890\" | json }},\n \"admin_graphql_api_id\": \"gid://shopify/Collection/1234567890\"\n }\n ]\n {% endcapture %}\n\n {% assign collections = collections_json | parse_json %}\n{% endif %}\n\n{% for collection in collections %}\n {% assign collection_id_string = \"\" | append: collection.id %}\n\n {% if collection_handles_or_ids_to_include != blank %}\n {% unless collection_handles_or_ids_to_include contains collection_id_string\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_id_string\n or collection_handles_or_ids_to_exclude contains collection.handle %}\n {% continue %}\n {% endif %}\n {% endif %}\n\n {% if collection.sort_order != \"manual\" %}\n {% if force_manual_sorting_on_collections or event.preview %}\n {% action \"shopify\" %}\n mutation {\n collectionUpdate(\n input: {\n id: {{ collection.admin_graphql_api_id | json }}\n sortOrder: MANUAL\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% else %}\n {% log\n message: \"Collection is not configured for manual sorting; skipping.\",\n collection_title: collection.title,\n collection_handle: collection.handle,\n collection_id: collection.id\n %}\n\n {% continue %}\n {% endif %}\n {% endif %}\n\n {% assign all_product_ids_current_sort = array %}\n {% assign cursor = nil %}\n\n {% for n in (0..100) %}\n {% capture query %}\n query {\n collection(id: {{ collection.admin_graphql_api_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 {% assign in_stock_products_and_quantities = array %}\n {% assign out_of_stock_products = array %}\n {% assign untracked_products = array %}\n\n {% assign cursor = nil %}\n\n {% for n in (0..3000) %}\n {% capture query %}\n query {\n collection(id: {{ collection.admin_graphql_api_id | json }}) {\n products(\n sortKey: {{ product_sort_order }}\n reverse: {{ reverse_sort | json }}\n first: 9\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n tracksInventory\n totalInventory\n {% if use_sellable_online_quantity %}\n variants(first: 100) {\n nodes {\n sellableOnlineQuantity\n }\n }\n {% endif %}\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 \"totalInventory\": 1\n },\n {\n \"id\": \"gid://shopify/Product/2345678901\",\n \"tracksInventory\": true,\n \"totalInventory\": 0\n },\n {\n \"id\": \"gid://shopify/Product/3456789012\",\n \"tracksInventory\": false,\n \"totalInventory\": null\n },\n {\n \"id\": \"gid://shopify/Product/4567890123\",\n \"tracksInventory\": true,\n \"totalInventory\": 2,\n \"variants\": {\n \"nodes\": [\n {\n \"sellableOnlineQuantity\": 1\n },\n {\n \"sellableOnlineQuantity\": 0\n }\n ]\n }\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% comment %}\n NOTE: first sort products into 3 groups\n - untracked (to preserve base collection sort, and push to top, i.e. unlimited stock)\n - in stock (to be sorted by quantity descending)\n - out of stock (to preserve base collection sort, and append to bottom)\n {% endcomment %}\n\n {% for product in result.data.collection.products.nodes %}\n {% unless product.tracksInventory %}\n {% assign untracked_products = untracked_products | push: product.id %}\n {% continue %}\n {% endunless %}\n\n {% assign product_and_quantity = hash %}\n {% assign product_and_quantity[\"id\"] = product.id %}\n\n {% if use_sellable_online_quantity %}\n {% assign sellable_online_quantity\n = product.variants.nodes\n | map: \"sellableOnlineQuantity\"\n | sum\n %}\n\n {% if sellable_online_quantity > 0 %}\n {% assign product_and_quantity[\"quantity\"] = sellable_online_quantity %}\n {% assign in_stock_products_and_quantities = in_stock_products_and_quantities | push: product_and_quantity %}\n\n {% else %}\n {% assign out_of_stock_products = out_of_stock_products | push: product.id %}\n {% endif %}\n\n {% elsif product.totalInventory > 0 %}\n {% assign product_and_quantity[\"quantity\"] = product.totalInventory %}\n {% assign in_stock_products_and_quantities = in_stock_products_and_quantities | push: product_and_quantity %}\n\n {% else %}\n {% assign out_of_stock_products = out_of_stock_products | 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 NOTE: reverse before and after sort by quantity to preserve base collection sort since Liquid sorts numbers ascending\n {% endcomment %}\n\n {% assign sorted_in_stock_products\n = in_stock_products_and_quantities\n | reverse\n | sort: \"quantity\"\n | reverse\n | map: \"id\"\n %}\n\n {% log\n collection_title: collection.title,\n all_product_ids_current_sort: all_product_ids_current_sort,\n untracked_products: untracked_products,\n sorted_in_stock_products: sorted_in_stock_products,\n out_of_stock_products: out_of_stock_products\n %}\n\n {% assign all_product_ids_new_sort\n = untracked_products\n | concat: sorted_in_stock_products\n | concat: out_of_stock_products\n %}\n\n {% assign moves = array %}\n\n {% for product_id in all_product_ids_new_sort %}\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 -- 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.admin_graphql_api_id | json }}\n moves: {{ move_group | graphql_arguments }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% else %}\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 {% 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",
"Inventory",
"Sort"
]
}