diff --git a/docs/README.md b/docs/README.md index c9d52bfd..724944ce 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,7 @@ This directory is built automatically. Each task's documentation is generated fr * [Auto-add phone numbers to unfulfilled orders, when the customer is updated](./auto-add-phone-numbers-to-unfulfilled-orders-when-the-customer-is-updated) * [Auto-add products to a custom collection when tagged](./auto-add-products-to-a-custom-collection-when-tagged) * [Auto-add the draft order to a new order's attributes](./auto-add-the-draft-order-id-to-an-orders-attributes) +* [Auto-approve return requests](./auto-approve-return-requests) * [Auto-archive orders after fulfillment](./auto-archive-orders-after-fulfillment) * [Auto-associate products with a delivery profile, by product tag](./auto-associate-products-with-a-delivery-profile-by-product-tag) * [Auto-associate variants with a delivery profile, by metafield value](./auto-associate-variants-with-a-delivery-profile-by-metafield-value) @@ -1507,6 +1508,7 @@ This directory is built automatically. Each task's documentation is generated fr ### Returns +* [Auto-approve return requests](./auto-approve-return-requests) * [Send email notification when items are returned](./send-email-notification-when-items-are-returned) ### Reviews diff --git a/docs/auto-approve-return-requests/README.md b/docs/auto-approve-return-requests/README.md new file mode 100644 index 00000000..aee0b1a8 --- /dev/null +++ b/docs/auto-approve-return-requests/README.md @@ -0,0 +1,55 @@ +# Auto-approve return requests + +Tags: Returns + +Use this task to auto-approve return requests, typically made by customers using self-serve from their account. Choose to limit the auto-approval to orders with any of a set of tags, products with any of a set of tags, or specific product categories or types. Optionally, configure email recipients to be notified when any auto-approval occurs. + +* View in the task library: [tasks.mechanic.dev/auto-approve-return-requests](https://tasks.mechanic.dev/auto-approve-return-requests) +* Task JSON, for direct import: [task.json](../../tasks/auto-approve-return-requests.json) +* Preview task code: [script.liquid](./script.liquid) + +## Default options + +```json +{ + "limit_to_orders_with_any_of_these_tags__array": null, + "limit_to_products_with_any_of_these_tags__array": null, + "limit_to_these_product_categories_or_types__array": null, + "notification_email_recipients__array": null +} +``` + +[Learn about task options in Mechanic](https://learn.mechanic.dev/core/tasks/options) + +## Subscriptions + +```liquid +shopify/returns/request +mechanic/actions/perform +``` + +[Learn about event subscriptions in Mechanic](https://learn.mechanic.dev/core/tasks/subscriptions) + +## Documentation + +Use this task to auto-approve return requests, typically made by customers using self-serve from their account. Choose to limit the auto-approval to orders with any of a set of tags, products with any of a set of tags, or specific product categories or types. Optionally, configure email recipients to be notified when any auto-approval occurs. + +More info on Shopify self-serve returns [here](https://help.shopify.com/en/manual/orders/refunds-returns/self-serve-returns). + +**Important:** +- Adding multiple limit conditions means the return request must meet ALL of the conditions in order to be auto-approved. +- More specifically, if any product conditions are configured, then ALL products on the return must meet those conditions. +- Return requests that are auto-approved **cannot** later be used to exchange items. +- Approval of return requests **cannot** be reverted; instead, the return request may be cancelled if needed. + +## Installing this task + +Find this task [in the library at tasks.mechanic.dev](https://tasks.mechanic.dev/auto-approve-return-requests), and use the "Try this task" button. Or, import [this task's JSON export](../../tasks/auto-approve-return-requests.json) – see [Importing and exporting tasks](https://learn.mechanic.dev/core/tasks/import-and-export) to learn how imports work. + +## Contributions + +Found a bug? Got an improvement to add? Start here: [../../CONTRIBUTING.md](../../CONTRIBUTING.md). + +## Task requests + +Submit your [task requests](https://mechanic.canny.io/task-requests) for consideration by the Mechanic community, and they may be chosen for development and inclusion in the [task library](https://tasks.mechanic.dev/)! diff --git a/docs/auto-approve-return-requests/script.liquid b/docs/auto-approve-return-requests/script.liquid new file mode 100644 index 00000000..69bd9d22 --- /dev/null +++ b/docs/auto-approve-return-requests/script.liquid @@ -0,0 +1,232 @@ +{% assign limit_order_tags = options.limit_to_orders_with_any_of_these_tags__array %} +{% assign limit_product_tags = options.limit_to_products_with_any_of_these_tags__array %} +{% assign limit_product_categories_or_types = options.limit_to_these_product_categories_or_types__array %} +{% assign notification_email_recipients = options.notification_email_recipients__array %} + +{% if event.topic == "shopify/returns/request" %} + {% comment %} + -- get additional return and product data not available in the webhook + {% endcomment %} + + {% capture query %} + query { + return(id: {{ return.admin_graphql_api_id | json }}) { + id + name + status + order { + name + tags + } + returnLineItems(first: 100) { + nodes { + fulfillmentLineItem { + lineItem { + product { + category { + name + } + productType + tags + } + } + } + } + } + } + } + {% endcapture %} + + {% assign result = query | shopify %} + + {% if event.preview %} + {% capture result_json %} + { + "data": { + "return": { + "id": "gid://shopify/Return/1234567890", + "name": "#PREVIEW-R1", + "status": "REQUESTED", + "order": { + "name": "#PREVIEW", + "tags": {{ limit_order_tags.first | json }} + }, + "returnLineItems": { + "nodes": [ + { + "fulfillmentLineItem": { + "lineItem": { + "product": { + "category": { + "name": {{ limit_product_categories_or_types.first | json }} + }, + "productType": {{ limit_product_categories_or_types.first | json }}, + "tags": {{ limit_product_tags.first | json }} + } + } + } + } + ] + } + } + } + } + {% endcapture %} + + {% assign result = result_json | parse_json %} + {% endif %} + + {% assign return = result.data.return %} + {% assign order = return.order %} + {% assign return_products + = return.returnLineItems.nodes + | map: "fulfillmentLineItem" + | map: "lineItem" + | map: "product" + %} + + {% if return.status != "REQUESTED" %} + {% log "This return request does not have a status of 'REQUESTED' and likely has already been acted on; skipping." %} + {% break %} + {% endif %} + + {% comment %} + -- assume the return request will be approved unless it doesn't meet any configured tag, category, or type limits + {% endcomment %} + + {% assign return_qualifies = true %} + + {% if limit_order_tags != blank %} + {% comment %} + -- make sure the order has one of the configured tags + {% endcomment %} + + {% assign has_qualifying_order_tag = nil %} + + {% for limit_order_tag in limit_order_tags %} + {% if order.tags contains limit_order_tag %} + {% assign has_qualifying_order_tag = true %} + {% break %} + {% endif %} + {% endfor %} + + {% unless has_qualifying_order_tag %} + {% assign return_qualifies = false %} + {% endunless %} + {% endif %} + + {% if limit_product_tags != blank %} + {% comment %} + -- make sure each returned product has one of the configured tags + {% endcomment %} + + {% assign all_products_qualify = true %} + + {% for product in return_products %} + {% assign product_qualifies = nil %} + + {% for limit_product_tag in limit_product_tags %} + {% if product.tags contains limit_product_tag %} + {% assign product_qualifies = true %} + {% break %} + {% endif %} + {% endfor %} + + {% unless product_qualifies %} + {% assign all_products_qualify = false %} + {% break %} + {% endunless %} + {% endfor %} + + {% unless all_products_qualify %} + {% assign return_qualifies = false %} + {% endunless %} + {% endif %} + + {% if limit_product_categories_or_types != blank %} + {% comment %} + -- make sure each returned product is one of the configured categories or types + {% endcomment %} + + {% assign all_products_qualify = true %} + + {% for product in return_products %} + {% assign product_qualifies = nil %} + + {% for limit_product_category_or_type in limit_product_categories_or_types %} + {% if product.productType == limit_product_category_or_type or product.category.name == limit_product_category_or_type %} + {% assign product_qualifies = true %} + {% break %} + {% endif %} + {% endfor %} + + {% unless product_qualifies %} + {% assign all_products_qualify = false %} + {% break %} + {% endunless %} + {% endfor %} + + {% unless all_products_qualify %} + {% assign return_qualifies = false %} + {% endunless %} + {% endif %} + + {% if return_qualifies %} + {% action "shopify" %} + mutation { + returnApproveRequest(input: {id: {{ return.id | json }}}) { + return { + id + name + status + order { + legacyResourceId + customer { + displayName + } + } + } + userErrors { + code + field + message + } + } + } + {% endaction %} + {% endif %} + +{% elsif event.topic == "mechanic/actions/perform" %} + {% unless action.type == "shopify" and action.run.ok and notification_email_recipients != blank %} + {% break %} + {% endunless %} + + {% comment %} + -- if any notification recipients are configured, send them an email when a return request is auto-approved + {% endcomment %} + + {% assign return = action.run.result.data.returnApproveRequest.return %} + + {%- capture email_subject -%} + Return request {{ return.name }} was auto-approved + {%- endcapture -%} + + {%- capture email_body -%} + Return request {{ return.name }}, made by {{ return.order.customer.displayName }}, was auto-approved. + + Review the return details and take further action on the order admin page. + + Thanks, + - Mechanic, for {{ shop.name }} + {%- endcapture -%} + + {% action "email" %} + { + "to": {{ notification_email_recipients | json }}, + "subject": {{ email_subject | json }}, + "body": {{ email_body | newline_to_br | json }}, + "reply_to": {{ shop.customer_email | json }}, + "from_display_name": {{ shop.name | json }} + } + {% endaction %} +{% endif %} diff --git a/tasks/auto-approve-return-requests.json b/tasks/auto-approve-return-requests.json new file mode 100644 index 00000000..de44b3fa --- /dev/null +++ b/tasks/auto-approve-return-requests.json @@ -0,0 +1,53 @@ +{ + "docs": "Use this task to auto-approve return requests, typically made by customers using self-serve from their account. Choose to limit the auto-approval to orders with any of a set of tags, products with any of a set of tags, or specific product categories or types. Optionally, configure email recipients to be notified when any auto-approval occurs.\n\nMore info on Shopify self-serve returns [here](https://help.shopify.com/en/manual/orders/refunds-returns/self-serve-returns).\n\n**Important:**\n- Adding multiple limit conditions means the return request must meet ALL of the conditions in order to be auto-approved.\n- More specifically, if any product conditions are configured, then ALL products on the return must meet those conditions.\n- Return requests that are auto-approved **cannot** later be used to exchange items.\n- Approval of return requests **cannot** be reverted; instead, the return request may be cancelled if needed.", + "halt_action_run_sequence_on_error": false, + "name": "Auto-approve return requests", + "online_store_javascript": null, + "options": { + "limit_to_orders_with_any_of_these_tags__array": null, + "limit_to_products_with_any_of_these_tags__array": null, + "limit_to_these_product_categories_or_types__array": null, + "notification_email_recipients__array": null + }, + "order_status_javascript": null, + "perform_action_runs_in_sequence": false, + "preview_event_definitions": [ + { + "description": "Return approve request", + "event_attributes": { + "topic": "mechanic/actions/perform", + "data": { + "type": "shopify", + "run": { + "ok": true, + "result": { + "data": { + "returnApproveRequest": { + "return": { + "name": "#PREVIEW-R1", + "status": "OPEN", + "order": { + "legacyResourceId": "1234567890", + "customer": { + "displayName": "Jean Deaux" + } + } + } + } + } + } + } + } + } + } + ], + "script": "{% assign limit_order_tags = options.limit_to_orders_with_any_of_these_tags__array %}\n{% assign limit_product_tags = options.limit_to_products_with_any_of_these_tags__array %}\n{% assign limit_product_categories_or_types = options.limit_to_these_product_categories_or_types__array %}\n{% assign notification_email_recipients = options.notification_email_recipients__array %}\n\n{% if event.topic == \"shopify/returns/request\" %}\n {% comment %}\n -- get additional return and product data not available in the webhook\n {% endcomment %}\n\n {% capture query %}\n query {\n return(id: {{ return.admin_graphql_api_id | json }}) {\n id\n name\n status\n order {\n name\n tags\n }\n returnLineItems(first: 100) {\n nodes {\n fulfillmentLineItem {\n lineItem {\n product {\n category {\n name\n }\n productType\n tags\n }\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 \"return\": {\n \"id\": \"gid://shopify/Return/1234567890\",\n \"name\": \"#PREVIEW-R1\",\n \"status\": \"REQUESTED\",\n \"order\": {\n \"name\": \"#PREVIEW\",\n \"tags\": {{ limit_order_tags.first | json }}\n },\n \"returnLineItems\": {\n \"nodes\": [\n {\n \"fulfillmentLineItem\": {\n \"lineItem\": {\n \"product\": {\n \"category\": {\n \"name\": {{ limit_product_categories_or_types.first | json }}\n },\n \"productType\": {{ limit_product_categories_or_types.first | json }},\n \"tags\": {{ limit_product_tags.first | json }}\n }\n }\n }\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign return = result.data.return %}\n {% assign order = return.order %}\n {% assign return_products\n = return.returnLineItems.nodes\n | map: \"fulfillmentLineItem\"\n | map: \"lineItem\"\n | map: \"product\"\n %}\n\n {% if return.status != \"REQUESTED\" %}\n {% log \"This return request does not have a status of 'REQUESTED' and likely has already been acted on; skipping.\" %}\n {% break %}\n {% endif %}\n\n {% comment %}\n -- assume the return request will be approved unless it doesn't meet any configured tag, category, or type limits\n {% endcomment %}\n\n {% assign return_qualifies = true %}\n\n {% if limit_order_tags != blank %}\n {% comment %}\n -- make sure the order has one of the configured tags\n {% endcomment %}\n\n {% assign has_qualifying_order_tag = nil %}\n\n {% for limit_order_tag in limit_order_tags %}\n {% if order.tags contains limit_order_tag %}\n {% assign has_qualifying_order_tag = true %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% unless has_qualifying_order_tag %}\n {% assign return_qualifies = false %}\n {% endunless %}\n {% endif %}\n\n {% if limit_product_tags != blank %}\n {% comment %}\n -- make sure each returned product has one of the configured tags\n {% endcomment %}\n\n {% assign all_products_qualify = true %}\n\n {% for product in return_products %}\n {% assign product_qualifies = nil %}\n\n {% for limit_product_tag in limit_product_tags %}\n {% if product.tags contains limit_product_tag %}\n {% assign product_qualifies = true %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% unless product_qualifies %}\n {% assign all_products_qualify = false %}\n {% break %}\n {% endunless %}\n {% endfor %}\n\n {% unless all_products_qualify %}\n {% assign return_qualifies = false %}\n {% endunless %}\n {% endif %}\n\n {% if limit_product_categories_or_types != blank %}\n {% comment %}\n -- make sure each returned product is one of the configured categories or types\n {% endcomment %}\n\n {% assign all_products_qualify = true %}\n\n {% for product in return_products %}\n {% assign product_qualifies = nil %}\n\n {% for limit_product_category_or_type in limit_product_categories_or_types %}\n {% if product.productType == limit_product_category_or_type or product.category.name == limit_product_category_or_type %}\n {% assign product_qualifies = true %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% unless product_qualifies %}\n {% assign all_products_qualify = false %}\n {% break %}\n {% endunless %}\n {% endfor %}\n\n {% unless all_products_qualify %}\n {% assign return_qualifies = false %}\n {% endunless %}\n {% endif %}\n\n {% if return_qualifies %}\n {% action \"shopify\" %}\n mutation {\n returnApproveRequest(input: {id: {{ return.id | json }}}) {\n return {\n id\n name\n status\n order {\n legacyResourceId\n customer {\n displayName\n }\n }\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n{% elsif event.topic == \"mechanic/actions/perform\" %}\n {% unless action.type == \"shopify\" and action.run.ok and notification_email_recipients != blank %}\n {% break %}\n {% endunless %}\n\n {% comment %}\n -- if any notification recipients are configured, send them an email when a return request is auto-approved\n {% endcomment %}\n\n {% assign return = action.run.result.data.returnApproveRequest.return %}\n\n {%- capture email_subject -%}\n Return request {{ return.name }} was auto-approved\n {%- endcapture -%}\n\n {%- capture email_body -%}\n Return request {{ return.name }}, made by {{ return.order.customer.displayName }}, was auto-approved.\n\n Review the return details and take further action on the order admin page.\n\n Thanks,\n - Mechanic, for {{ shop.name }}\n {%- endcapture -%}\n\n {% action \"email\" %}\n {\n \"to\": {{ notification_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": [ + "shopify/returns/request", + "mechanic/actions/perform" + ], + "subscriptions_template": "shopify/returns/request\nmechanic/actions/perform", + "tags": [ + "Returns" + ] +}