Partially auto-capture payments as orders are fulfilled, with Mechanic.

Mechanic is a development and ecommerce automation platform for Shopify. :)

Partially auto-capture payments as orders are fulfilled

Running when an order is updated, this task captures payment in proportion to the order value that has been fulfilled. For example, for an order with a subtotal of $10 and a total of $15 with shipping/taxes/discounts, this task will capture $7.50 when $5 of the order's value has been fulfilled.

Runs Occurs whenever an order is updated. Configuration includes payment gateway names.

15-day free trial – unlimited tasks

Documentation

Running when an order is updated, this task captures payment in proportion to the order value that has been fulfilled. For example, for an order with a subtotal of $10 and a total of $15 with shipping/taxes/discounts, this task will capture $7.50 when $5 of the order's value has been fulfilled.

This task only works with payment gateways that support multiple captures against an authorization, which includes Shopify Payments. However, this task will not process orders that use multiple payment gateways on the same order.

Note: To find the payment gateway names, you will need to check the payment_gateway_names field on any order that uses the gateway(s) you wish to configure in this task. One method of doing this is to add .json to the end of the order admin page address, and searching for payment_gateway_names (e.g. "https://admin.shopify.com/store/my-shop/orders/1234567890.json").

Developer details

Mechanic is designed to benefit everybody: merchants, customers, developers, agencies, Shopifolks, everybody.

That’s why we make it easy to configure automation without code, why we make it easy to tweak the underlying code once tasks are installed, and why we publish it all here for everyone to learn from.

(By the way, have you seen our documentation? Have you joined the Slack community?)

Open source
View on GitHub to contribute to this task
Subscriptions
shopify/orders/updated
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
payment gateway names (array, required)
Code
{% assign payment_gateway_names = options.payment_gateway_names__array_required %}

{% if event.preview %}
  {% assign order = hash %}
  {% assign order["admin_graphql_api_id"] = "gid://shopify/Order/1234567890" %}
  {% assign order["financial_status"] = "partially_paid" %}
{% endif %}

{% if order.financial_status != "authorized" and order.financial_status != "partially_paid" %}
  {% log "Order is not either authorized or partially_paid." %}

{% else %}
  {% capture query %}
    query {
      order(id: {{ order.admin_graphql_api_id | json }}) {
        id
        paymentGatewayNames
        transactions(capturable: true) {
          id
          amountSet {
            presentmentMoney {
              amount
              currencyCode
            }
          }
        }
        totalPriceSet {
          presentmentMoney {
            amount
          }
        }
        subtotalPriceSet {
          presentmentMoney {
            amount
          }
        }
        totalCapturableSet {
          presentmentMoney {
            amount
          }
        }
        fulfillments {
          name
          fulfillmentLineItems(first: 250) {
            pageInfo {
              hasNextPage
            }
            nodes {
              discountedTotalSet {
                presentmentMoney {
                  amount
                }
              }
            }
          }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "order": {
            "paymentGatewayNames": [
              {{ payment_gateway_names.first | json }}
            ],
            "transactions": [
              {
                "id": "gid://shopify/OrderTransaction/1234567890",
                "amountSet": {
                  "presentmentMoney": {
                    "amount": "16.99",
                    "currencyCode": "USD"
                  }
                }
              }
            ],
            "totalPriceSet": {
              "presentmentMoney": {
                "amount": "16.99"
              }
            },
            "subtotalPriceSet": {
              "presentmentMoney": {
                "amount": "10.0"
              }
            },
            "totalCapturableSet": {
              "presentmentMoney": {
                "amount": "16.54"
              }
            },
            "fulfillments": [
              {
                "name": "#1234-F1",
                "fulfillmentLineItems": {
                  "nodes": [
                    {
                      "discountedTotalSet": {
                        "presentmentMoney": {
                          "amount": "5.0"
                        }
                      }
                    }
                  ]
                }
              }
            ]
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% endif %}

  {% assign order = result.data.order %}

  {% unless order.paymentGatewayNames.size == 1 and payment_gateway_names contains order.paymentGatewayNames.first %}
    {% log
      message: "This order did not use one of the configured gateways or there are multiple gateways on the order; skipping.",
      configured_payment_gateway_names: payment_gateway_names,
      order: order
    %}
    {% break %}
  {% endunless %}

  {% assign total = order.totalPriceSet.presentmentMoney.amount | times: 1.0 %}
  {% assign total_captured = total | minus: order.totalCapturableSet.presentmentMoney.amount %}
  {% assign subtotal = order.subtotalPriceSet.presentmentMoney.amount | times: 1.0 %}
  {% assign subtotal_fulfilled = 0.0 %}

  {% for fulfillment in order.fulfillments %}
    {% if fulfillment.fulfillmentLineItems.pageInfo.hasNextPage %}
      {% error "This order has a fulfillment with more than 250 line items in it. This is not supported by this task." %}
      {% break %}
    {% endif %}

    {% for fulfillment_line_item in fulfillment.fulfillmentLineItems.nodes %}
      {% assign subtotal_fulfilled = subtotal_fulfilled | plus: fulfillment_line_item.discountedTotalSet.presentmentMoney.amount %}
    {% endfor %}
  {% endfor %}

  {% assign desired_total_captured = subtotal_fulfilled | divided_by: subtotal | times: total %}
  {% assign amount_to_capture = desired_total_captured | minus: total_captured | times: 100 | round | divided_by: 100.0 %}
  {% assign parent_transaction = order.transactions.first %}

  {% log
    total: total,
    total_captured: total_captured,
    subtotal: subtotal,
    subtotal_fulfilled: subtotal_fulfilled,
    desired_total_captured: desired_total_captured,
    amount_to_capture: amount_to_capture,
    parent_transaction: parent_transaction
  %}

  {% if order.capturable == false %}
    {% log "This order is not capturable." %}

  {% elsif amount_to_capture == 0 %}
    {% log "Nothing to capture at this time." %}

  {% else %}
    {% action "shopify" %}
      mutation {
        orderCapture(
          input: {
            id: {{ order.id | json }}
            parentTransactionId: {{ parent_transaction.id | json }}
            amount: {{ amount_to_capture | append: "" | json }}
            currency: {{ parent_transaction.amountSet.presentmentMoney.currencyCode }}
          }
        ) {
          transaction {
            id
            status
          }
          userErrors {
            field
            message
          }
        }
      }
    {% endaction %}
  {% endif %}
{% endif %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more