ACH Direct Debit

ActionKit supports transfers from US bank accounts via Braintree's implementation of ACH Direct Debit. ACH payments typically have lower transaction fees, and people change banks less frequently than credit cards. This makes them attractive for recurring donations.

How It Works

The implementation in ActionKit is similar to Braintree's credit card processing in that the bank information is never sent to our servers. Instead it's sent to Braintree with a JavaScript request and a one-time-use token from Braintree is the only thing submitted to us.

Users can provide their bank account information directly on your donation form. They will need to enter:

  • their bank's routing number
  • their account number
  • the type of account, checking or savings
  • the ownership of the account, personal or business
  • the business name if it's a business account

Alternatively, they can provide their bank login and password using a pre-built UI from Plaid. This method is more foolproof, but users may be reluctant to provide their bank login to a third-party service. Note that Plaid is a separate service from ActionKit and Braintree that you would need to sign up for before using.

Either method results in a one-time-use token that gets sent to ActionKit. We use the key to create a sale transaction with Braintree. During this process, Braintree will verify the account using their network check method. The account must pass this check before a sale can be created. (If you have a donor with a bank account that can't be verified in this way but you want to accept it anyway, there is a workaround below.)

After a sale is successfully created, both the order and transaction in ActionKit will have a status of pending. Determining if the transfer is successful can take up to three business days. Braintree has a more detailed timeline for settlements and disbursement of funds here. ActionKit's mail targeting and reports will not count pending donations as successful.

When the transaction settles or fails, Braintree notifies ActionKit with a POST to our servers. The order and transaction status will be updated to complete or failed within ten minutes of receiving this notification. Transactions can fail due to mismatches in the bank information or disputes from the account owner.

Note

If an ACH transactions fails settlement because it is returned by the bank, Braintree will charge you a fee. Make sure you understand the risks before using ACH.

It is possible to refund or partially refund an ACH order, but not until the transaction has settled.

Recurring Donation Support

Because Braintree doesn't support recurring donations with ACH, the recurring billing is implemented on the ActionKit side. It supports the same recurring periods as card orders on Braintree.

When a recurring ACH donation is created, ActionKit will create a customer for the user in your Braintree vault, and add the bank account as a payment method. Then a daily job will check for accounts that should be charged and create new sale transactions for them. These transactions will need to settle, just like one-time donations do. If any sale transaction for a recurring donation fails to settle, the profile will be canceled and the status will be set to canceled_by_failure. This is due to the fees involved in failed ACH transactions.

You can cancel or update recurring ACH orders using the standard management tools.

Setting Up Your Account

The first step in setting up your account for ACH is to sign up for Braintree's ACH beta program. You can read more about their eligibility requirements and how to sign up here.

If you want to use the Plaid bank login UI, you will need to sign up with Plaid and then contact Braintree support to link your Plaid account to your Braintree account.

Whether or not you use Plaid, you will need to configure the webhooks in your Braintree account to deliver notifications about ACH settlements to ActionKit.

  • Log into your Braintree console.
  • Click on Settings (the gear icon).
  • Select "API", then select "Webhooks".
  • Click the "Create New Webhook" button.
  • For "Destination URL" enter this URL: https://docs.actionkit.com/webhooks/braintree/transaction
  • Under "TRANSACTION" check the boxes for "Transaction Settlement Declined" and "Transaction Settled" and click the "Create Webhook" button.
  • That should set up the webhook. Click the "Check URL" link to send a test notification to our servers and then check with support to make sure that we receieved it.

When that's all done, ask ActionKit support to enable ACH for your Braintree account.

Updating Your Donation Templates

The Original templateset has been updated to support ACH, so if you create new templates based on Original they will have ACH support. If you want to add ACH support to an existing templateset you will need to make some changes. The changes to accept ACH donations are all in donate.html. If you want to also allow users to update recurring ACH donations themselves, you will need to make additional channges to the recurring_update.html and recurring_info.html templates.

The first change is that ACH requires separate first and last name fields for verification of the bank account. In the Original templates we handle it like this:

{% if page.accept_ach %}
<div>
  <label for="id_first_name">First Name</label>
  <input id="id_first_name" type="text" name="first_name">
  <input type="hidden" name="required" value="first_name">
</div>
<div>
  <label for="id_last_name">Last Name</label>
  <input id="id_last_name" type="text" name="last_name">
  <input type="hidden" name="required" value="last_name">
</div>
{% else %}
<div>
  <label for="id_name">Name</label>
  <input id="id_name" type="text" name="name">
</div>
{% endif %}

Next, there are several new fields for bank information. First there's a field named payment_method which indicates whether to use ACH or a credit card for payment. This can be a radio button to let people choose, or if you want to make a templateset that is specifically for ACH you can hard-code this in a hidden input:

<input type="hidden" name="payment_method" value="ach">

Here's how it looks in a radio input:

{% if page.accept_ach %}
  <input type="radio" id="ak_payment_method_cc" name="payment_method" value="cc" checked>
  <label for="ak_payment_method_cc">Credit Card</label>
  <br>
  <input type="radio" id="ak_payment_method_ach" name="payment_method" value="ach">
  <label for="ak_payment_method_ach">US Bank Transfer</label>
{% endif %}

If you leave this field out, credit card (or PayPal) is assumed.

Next, there's a similar field to choose between bank login (via Plaid) or account numbers for identifying the bank account. The field is ach_method and can also be hard-coded in a hidden input if you don't want to support both methods:

<input type="hidden" name="ach_method" value="account_number">

Here it is in a radio input:

{% if page.accept_ach %}
<input type="radio" id="ak_ach_method_account_number" name="ach_method" value="account_number" checked>
<label for="ak_ach_method_account_number">Account Number</label>
<br>
<input type="radio" id="ak_ach_method_bank_login" name="ach_method" value="bank_login">
<label for="ak_ach_method_bank_login">Bank Login</label>
{% endif %}

If you leave this out and payment_method has the value ach, then the account number method will be assumed.

For the account number method, you will need some additional inputs: routing_number, bank_account, and account_type. Here's how they look in Original. Note that the IDs on these are required.

<div class="ak-err-below">
  <div id="ak-fieldbox-routing_number">
    <label for="ak-routing_number">Routing Number</label>
    <input id="ak-routing_number" type="text" name="routing_number" size="9">
  </div>
</div>

<div class="ak-err-below">
  <div id="ak-fieldbox-bank_account">
    <label for="ak-bank_account">Bank Account</label>
    <input id="ak-bank_account" type="text" name="bank_account" size="17">
  </div>
</div>

<div class="ak-err-below">
  <div id="ak-fieldbox-account_type">
    <label for="ak-account_type">Account Type</label>
    <select id="ak-account_type" type="text" name="account_type">
      <option value="checking">checking</option>
      <option value="savings">savings</option>
    </select>
  </div>
</div>

For either the account number method or bank login, you'll need to ask for ownership information about the account. Here's how that looks in Original:

<div class="ak-err-below">
  <div id="ak-fieldbox-ownership_type">
    <label for="ak-ownership_type">Ownership Type</label>
    <select id="ak-ownership_type" type="text" name="ownership_type">
      <option value="personal">personal</option>
      <option value="business">business</option>
    </select>
  </div>
</div>

<div class="ak-err-below">
  <div id="ak-fieldbox-business_name">
    <label for="ak-business_name">Business Name</label>
    <input id="ak-business_name" type="text" name="business_name">
  </div>
</div>

In the Original template, there's JavaScript code to show the business_name input only when needed.

Finally, you need to show the required authorization language aka the mandate text. The details of this text are provided by Braintree here. You must display this text as part of your checkout flow, at or near the final submit button. It needs to be inside a div with the ak-mandate ID in order for the JavaScript to find it and pass it to Braintree.

<div id="ak-mandate">By clicking "Donate", I authorize Braintree, a service of PayPal, on behalf of <span id="ak-client-name">{% filter ak_text:"org_name" %}{% client_name %}{% endfilter %}</span> (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account.</div>

There's some additional JavaScript to handle showing and hiding these new fields. This relies on names of classes on enclosing divs in Original. It may not be relevant for you if you're not providing a choice of payment options in one page.

$('input[type=radio][name=payment_method]').change(function() {
    if (this.value == 'ach') {
        $('.ach_payment_options').show();
        $('.cc_payment_options').hide();
    } else {
        $('.ach_payment_options').hide();
        $('.cc_payment_options').show();
    }
});
$('input[type=radio][name=ach_method]').change(function() {
    if (this.value == 'account_number') {
        $('.account_number_options').show();
    } else {
        $('.account_number_options').hide();
    }
});
$('#ak-ownership_type').change(function() {
    if (this.value == 'business') {
        $('#ak-fieldbox-business_name').show();
    } else {
        $('#ak-fieldbox-business_name').hide();
    }
});

There's also new JavaScript to handle the validation of the new fields. The function step_3_validation has been largely rewritten:

function step_3_validation() {
    var step_has_errors = false;
    if (!actionkit.errors) {
        actionkit.errors = {};
    }

    var payment_method = $('input[name="payment_method"]:checked').val();
    if (payment_method && payment_method == 'ach') {
        if ($('#ak-ownership_type').val() == 'business') {
            if (!$('#ak-business_name').val()) {
                actionkit.errors['business_name:missing'] = actionkit.forms.errorMessage('business_name:missing');
                step_has_errors = true;
            }
        }
        var ach_method = $('input[name="ach_method"]:checked').val();
        if (ach_method && ach_method != 'bank_login') {
            var bank_account = $('#ak-bank_account').val();
            if (!bank_account) {
                actionkit.errors['bank_account:missing'] = actionkit.forms.errorMessage('bank_account:missing');
                step_has_errors = true;
            } else if (!valid_bank_account_number(bank_account)) {
                actionkit.errors['bank_account:invalid'] = actionkit.forms.errorMessage('bank_account:invalid');
                step_has_errors = true;
            }
            var routing_number = $('#ak-routing_number').val();
            if (!routing_number) {
                actionkit.errors['routing_number:missing'] = actionkit.forms.errorMessage('routing_number:missing');
                step_has_errors = true;
            } else if (!valid_bank_routing_number(routing_number)) {
                actionkit.errors['routing_number:invalid'] = actionkit.forms.errorMessage('routing_number:invalid');
                step_has_errors = true;
            }
        }

    } else {
        if (!do_validate_credit_card()) {
            return step_has_errors;
        }

        if (!valid_credit_card($('#ak-card_num').val())) {
            actionkit.errors['card_num:invalid'] = actionkit.forms.errorMessage('card_num:invalid');
            step_has_errors = true;
        }

        if (!valid_credit_card_code($('#ak-card_code').val())) {
            actionkit.errors['card_code:invalid'] = actionkit.forms.errorMessage('card_code:invalid');
            step_has_errors = true;
        }
    }

    if (step_has_errors) {
        actionkit.forms.onValidationErrors(actionkit.errors);
    }

    return step_has_errors;
}

The following validation functions were also added:

function valid_bank_account_number(value) {
    return /^\d{4,17}$/.test(value);
}

function valid_bank_routing_number(value) {
    value = value.replace(/\D/g,'');

    if (value.length != 9) { return false; }

    var checksum = 0;
    for (var i = 0; i < value.length; i += 3) {
        checksum += parseInt(value.charAt(i), 10) * 3
                 +  parseInt(value.charAt(i + 1), 10) * 7
                 +  parseInt(value.charAt(i + 2), 10);
    }

    if (checksum == 0 || checksum % 10 != 0) { return false; }

    return true;
}

The initialization code at the end of the page also needs to pass ach: true.

    submitOnEmpty: function() {
        return $('#ak-pay-by-paypal').val() == 1;
    },
-   submit: form.querySelector(".ak-submit-button")
+   submit: form.querySelector(".ak-submit-button"),
+   {% if page.accept_ach %}ach: true,{% endif %}
},
toRemove = ["#ak-card_num", "#ak-card_code", "#ak-exp_date_month",
            "#ak-exp_date_year", "#ak-card_num-required",

To allow users to manage their recurring ACH donations, you will also need to make updates to recurring_update.html and recurring_info.html. (The Original templates have been updated with these changes, so this is only necessary if you want to update a customized templateset.) This is the diff for recurring_update.html:

@@ -37,7 +37,7 @@
     function ak_recurring_change_card(profile_id) {
         var profile_el = $('#change_profile_' + profile_id);
         profile_el.find('.ak-show-cc').hide();
-        profile_el.find('.ak-change-cc').fadeIn();
+        profile_el.find('.ak-change-cc').not('.ak-business_name').fadeIn();
         profile_el.find('.ak-change-address').fadeIn();
         profile_el.find('.ak-change-submit').fadeIn();
         profile_el.find(':input').prop('disabled', false);
@@ -56,10 +56,11 @@
     $(document).ready(function() {
         var match = /profile_id=(\d+)/.exec(window.location.search);
         if (match) {
-            profile_id = match[1];
+            var profile_id = match[1];
             if (/error_card_num/.test(window.location.search) ||
-                    /error_address1/.test(window.location.search) ||
-                    /error_city/.test(window.location.search)) {
+                /error_address1/.test(window.location.search) ||
+                /error_city/.test(window.location.search) ||
+                /error_profile_id/.test(window.location.search)) {
                 ak_recurring_change_card(profile_id);
             } else if (/amount=/.test(window.location.search)) {
                 ak_recurring_change_amount(profile_id);
@@ -69,12 +70,30 @@
 </script>

 {% for profile in active %}
+
+{% if profile.payment_processor_information.use_accept %}
+{% once %}
+<script src="/resources/ak_authnet_accept.js"></script>
+{% authnet_js_libs %}
+<script>
+$(function() {
+    var form = $("#change_profile_{{ profile.id }}").get(0);
+    options = {
+            form: form,
+            submit: $("#change_profile_{{ profile.id }} .ak-change-submit button").get(0),
+    };
+    actionkit.authnet.initClient('{{ profile.payment_processor_information.login }}', '{{ profile.payment_processor_information.public_key }}', options);
+});
+</script>
+{% endonce %}
+{% endif %}
+
 {% if profile.payment_processor_information.use_vzero %}
 {% once %}
 {% braintree_js_libs %}
 <script src="/resources/ak_braintree_vzero.js"></script>
 <script>
-function initVZeroForForm(form_id, clientToken) {
+function initVZeroForForm(form_id, clientToken, is_ach) {
     var form = document.querySelector(form_id),
         options = {
             form: form,
@@ -111,13 +130,18 @@ function initVZeroForForm(form_id, clientToken) {
         toRemove = ["input[name=card_num]", "input[name=card_code]",
                     "input[name=exp_date]"];

-    toRemove.forEach(function(el) {
-        form.querySelector(el).remove();
-    });
-    Object.keys(options.fields).forEach(function(key) {
-        var field = options.fields[key];
-        document.querySelector(field.selector).classList.add('hosted-field');
-    });
+    if (is_ach) {
+        options['fields'] = {};
+        options['ach'] = true;
+    } else {
+        toRemove.forEach(function(el) {
+            form.querySelector(el).remove();
+        });
+        Object.keys(options.fields).forEach(function(key) {
+            var field = options.fields[key];
+            document.querySelector(field.selector).classList.add('hosted-field');
+        });
+    }

     actionkit.donations.initClient(clientToken, options);
 }
@@ -190,6 +214,64 @@ function onStripeResponse(status, response) {
 {% endif %}
 {% endfor %}

+<script type="text/javascript">
+function valid_bank_account_number(value) {
+    return /^\d{4,17}$/.test(value);
+}
+
+function valid_bank_routing_number(value) {
+    value = value.replace(/\D/g,'');
+
+    if (value.length != 9) { return false; }
+
+    var checksum = 0;
+    for (var i = 0; i < value.length; i += 3) {
+        checksum += parseInt(value.charAt(i), 10) * 3
+                 +  parseInt(value.charAt(i + 1), 10) * 7
+                 +  parseInt(value.charAt(i + 2), 10);
+    }
+
+    if (checksum == 0 || checksum % 10 != 0) { return false; }
+
+    return true;
+}
+
+function validate_business_name(input) {
+    form = input.form;
+    if (input.value == 'business' && !form['business_name'].value) {
+        return "Business name is required for business accounts.";
+    }
+
+    return true;
+}
+
+function ach_validation(form) {
+    actionkit.forms.setForm(form);
+
+    // allow changing amount separately
+    if ($(form['bank_account']).is(':hidden')) { return true; }
+
+    // clear_errors will delete these inputs from other
+    // forms if we don't remove the error class from them here
+    $(':input.ak-error, label.ak-error').removeClass('ak-error');
+
+    // address is required for ACH changes, even for logged in users
+    var saved_state = actionkit.forms.alwaysRequireUserFields;
+    actionkit.forms.alwaysRequireUserFields = true;
+    var is_valid = actionkit.forms.validate();
+    actionkit.forms.alwaysRequireUserField = saved_state;
+
+    return is_valid;
+}
+
+/* This prevents the invisible inputs from being submitted. Needed
+ because reflectCountryChange() messes with the disabled prop. */
+function disable_invisibles(form) {
+    $(form).find(':input:hidden[type!="hidden"]').prop('disabled', true);
+    return true;
+}
+</script>
+
 {% endblock %}

 {% block content %}

And this is the diff for the changes to recurring_info.html:

@@ -1,13 +1,15 @@
 {% with profile.payment_processor_information as pp %}
-<form id="change_profile_{{ profile.id }}" {% if pp.processor == "stripe" %}data-stripe-pub-key="{{ pp.pub_key }}"{% endif %} class="action_form" name="act" method="POST" action="/act/" accept-charset="utf-8">
+<form id="change_profile_{{ profile.id }}" {% if pp.processor == "stripe" %}data-stripe-pub-key="{{ pp.pub_key }}"{% endif %} class="action_form" name="act_{{ profile.id }}" method="POST" action="/act/" accept-charset="utf-8" onsubmit="return true;">
     <input type="hidden" name="page" value="{{ page.name }}">
     <input type="hidden" name="profile_id" value="{{ profile.id }}">
     {% comment %} akid is needed to tell javascript required field checking we have a user, but the view requires a user to be logged in. {% endcomment %}
     <input type="hidden" name="akid" value="{{ logged_in_user.token }}">

     <div class="ak-field-box">
-        <div class="ak-styled-fields ak-labels-before {{templateset.custom_fields.field_errors_class|default:"ak-errs-below"}}">
-
+        <div class="ak-styled-fields ak-labels-before {{templateset.custom_fields.field_errors_class|default:"ak-err-above"}}">
+{% once %}<ul id="ak-errors"></ul>{% endonce %}
+          <ul class="ak-errors"></ul>
+
         <div>
             <label>
                 Next Payment
@@ -95,8 +97,74 @@
         </div>

         {% endif %}
+
+        {% if profile.is_ach %}
+        <input type="hidden" name="payment_method" value="ach">
+        <input type="hidden" name="first_name" value="{{ user.first_name }}">
+        <input type="hidden" name="last_name" value="{{ user.last_name }}">

-        {% if profile.order.payment_method == "cc" %}
+        <div class="ak-show-cc">
+            <label>
+                Bank Account
+            </label>
+            <div class="ak-readonly-value">
+                <div>
+                  {% if profile.card_num %}
+                  Account ending in {{ profile.card_num }}
+                  {% endif %}
+                </div>
+                <div>
+                   {%if not profile.is_import_stub %} <a href="#" onclick="return ak_recurring_change_card('{{ profile.id }}');">Change bank information</a>{% endif %}
+                </div>
+            </div>
+        </div>
+
+        <div class="ak-change-cc" style="display: none">
+            <label for="ak-routing_number_{{ profile.id }}">
+                Routing Number
+            </label>
+            <input id="ak-routing_number_{{ profile.id }}" type="text" name="routing_number" size="9" style="max-width: 33%" onvalidate="return valid_bank_routing_number(this.value)" disabled=true>
+            <input type="hidden" name="required" value="routing_number"/>
+        </div>
+
+        <div class="ak-change-cc" style="display: none">
+            <label for="ak-bank_account_{{ profile.id }}">
+                Bank Account
+            </label>
+            <input id="ak-bank_account_{{ profile.id }}" type="text" name="bank_account" size="17" style="max-width: 33%" onvalidate="return valid_bank_account_number(this.value)" disabled=true>
+            <input type="hidden" name="required" value="bank_account"/>
+        </div>
+
+        <div class="ak-change-cc" style="display: none">
+            <label for="ak-account_type_{{ profile.id }}">
+                Account Type
+            </label>
+            <select id="ak-account_type_{{ profile.id }}" type="text" name="account_type" style="max-width: 33%" disabled=true>
+              <option value="checking">checking</option>
+              <option value="savings">savings</option>
+            </select>
+        </div>
+
+        <div class="ak-change-cc" style="display: none">
+            <label for="ak-ownership_type_{{ profile.id }}">
+                Ownership Type
+            </label>
+            <select id="ak-ownership_type_{{ profile.id }}" type="text" name="ownership_type" style="max-width: 33%" onvalidate="return validate_business_name(this)" disabled=true>
+              <option value="personal">personal</option>
+              <option value="business">business</option>
+            </select>
+        </div>
+
+        <div class="ak-change-cc ak-business_name" style="display: none">
+            <label for="ak-business_name_{{ profile.id }}">
+                Business Name
+            </label>
+            <input id="ak-business_name_{{ profile.id }}" type="text" name="business_name" disabled=true>
+        </div>
+
+        {% endif %}
+
+        {% if profile.order.payment_method == "cc" or profile.is_ach %}
             <div class="ak-show-address" style="display: none">
                 <label>
                     Address
@@ -114,14 +182,19 @@
             {% if profile.payment_processor.recurring_update_supports_address %}

             <div class="ak-change-address" style="display: none">
+              {% if not profile.is_ach %}
                 <div style="text-align: center">
                     Optionally also enter your credit card billing address:
                 </div>
+              {% endif %}
                 <div>
                     <label for="id_address1_{{ profile.id }}">
                         Street Address
                     </label>
                     <input name="address1" id="id_address1_{{ profile.id }}" type="text" value="{{ profile.order.user_detail.address1 }}" disabled=true>
+                    {% if profile.is_ach %}
+                    <input type="hidden" name="required" value="address1"/>
+                    {% endif %}
                 </div>

                 <div>
@@ -129,6 +202,9 @@
                         City
                     </label>
                     <input name="city" id="id_city_{{ profile.id }}" type="text" value="{{ profile.order.user_detail.city }}" disabled=true>
+                    {% if profile.is_ach %}
+                    <input type="hidden" name="required" value="city"/>
+                    {% endif %}
                 </div>

                 <div class="state_select_box">
@@ -136,6 +212,9 @@
                         State
                     </label>
                     {% include "./state_select.html" %}
+                    {% if profile.is_ach %}
+                    <input type="hidden" name="required" value="state"/>
+                    {% endif %}
                 </div>

                 <div>
@@ -143,8 +222,11 @@
                         ZIP Code
                     </label>
                     <input name="zip" id="id_zip_{{ profile.id }}" type="text" value="{{ profile.order.user_detail.zip }}" disabled=true>
+                    {% if profile.is_ach %}
+                    <input type="hidden" name="required" value="zip"/>
+                    {% endif %}
                 </div>
-
+                {% if not profile.is_ach %}
                 <div>
                     <label for="id_region_{{ profile.id }}">
                         Region
@@ -163,18 +245,19 @@
                     <label>
                         Country
                     </label>
-                    {% include "./country_select.html" %}
+                    {% include "./country_select.html" with onchange="actionkit.forms.setForm(this.form); actionkit.forms.reflectCountryChange();" %}
                 </div>
-
+                {% endif %}
                 <script>
                   $(function () {
+                     var profile_el = $("#change_profile_{{ profile.id }}");
                      {% if profile.order.user_detail.country %}
-                       $("#change_profile_{{ profile.id }} .country_select_box select").val("{{ profile.order.user_detail.country }}");
+                       profile_el.find(".country_select_box select").val("{{ profile.order.user_detail.country }}");
                      {% endif %}
                      {% if profile.order.user_detail.state %}
-                       $("#change_profile_{{ profile.id }} .state_select_box select").val("{{ profile.order.user_detail.state }}");
+                       profile_el.find(".state_select_box select").val("{{ profile.order.user_detail.state }}");
                      {% endif %}
-                     actionkit.forms.reflectCountryChange();
+                     actionkit.forms.initForm('act_{{ profile.id }}');
                   });
                 </script>
             </div>
@@ -182,18 +265,34 @@
             {% endif %}
         {% endif %}

+        {% if profile.is_ach %}
+        <div class="ak-change-cc" style="display: none">
+          <div class="ak-mandate">By clicking "Save Changes", I authorize Braintree, a service of PayPal, on behalf of <span class="ak-client-name">{% filter ak_text:"org_name" %}{% client_name %}{% endfilter %}</span> (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account.</div>
+        </div>
+        {% endif %}

         <div class="ak-change-submit ak-align-right" style="display: none">
-            <button name="submit_form" type="submit">Save Changes</button>
+            <button name="submit_form" type="submit" {% if profile.is_ach %}onclick="return ach_validation(this.form);"{% else %}onclick="return disable_invisibles(this.form);"{% endif %}">Save Changes</button>
         </div>

         </div>
     </div>

 </form>
-{% if pp.use_vzero and profile.order.payment_method == "cc" %}
+{% if pp.use_vzero %}
 <script>
-initVZeroForForm('#change_profile_{{ profile.id }}', '{{ pp.client_token }}');
+initVZeroForForm('#change_profile_{{ profile.id }}', '{{ pp.client_token }}', {% if profile.is_ach %}true{% else %}false{% endif %});
+
+var ownership_type_menu = $('#change_profile_{{ profile.id }} [name=ownership_type]');
+if (ownership_type_menu.length) {
+    ownership_type_menu.change(function() {
+        if (this.value == 'business') {
+            $(this.form).find('.ak-business_name').show();
+        } else {
+            $(this.form).find('.ak-business_name').hide();
+        }
+    });
+}
 </script>
 {% endif %}

Notifying Users Whose Donations Fail

Because ACH donations can take days to settle, the user is no longer around to tell if settlement fails. You may want to notify users whose donations fail by email. This can be done with a recurring mailing series.

A new report named failed_onetime_ach_donations has been added to help idenitify these users. This report is suitable for use as a merge query in mailings and provides the details of the failed donation to use in the mailing text.

Here are the steps to set this up to notify users:

  • Create a new mailing with a subject like "Your direct debit donation did not complete successfully".
  • Select the failed_onetime_ach_donations as a merge query on the Compose screen.
  • In your text, you may now use query.amount, query.date, and query.url. An example is shown below.
<p>Dear {{ user.first_name|default:"Friend" }},</p>
<p> </p>
<p>Thanks for your donation of ${{ query.amount }} on {{ query.date }}. Unfortunately, the direct debit was declined by your bank. If you'd like to try again with a different type of payment, the page is here: {% client_domain_url %}/donate/{{ query.url }}/</p>
<p> </p>
<p>{% client_name %}</p>
  • On the Target screen, check the box under Special Criteria to only target users returned by the merge query.
  • Go to the Proof and Send screen and under Send or Schedule choose to "make recurring".
  • Create a new Recurring Series to use and schedule it for daily at 8am. Sending daily, a few hours after the end of the day, allows us to avoid missing donations or seeing the same ones twice.
  • Make a note of your Recurring Series ID and go back to the Targeting step to exclude people who have received this series in the last day. This is a good idea whenever you create a recurring mailing to avoid any chance of sending a duplicate mailing to any user. Under Excludes, add Query Library -> "Users Who Recently Received a Recurring Series" and specify your Recurring Series ID and 1 day.

Overriding Bank Account Verification

The network check method of verification that Braintree provides is quick enough to give immediate feedback, but it is not supported by all banks. If a user's bank is not covered by this method, they will see a message that "The bank account you entered could not be verified" and the row in core_transaction will have a message like this in the failure_message column: "ACH failed network_check verification. status: processor_declined, response: No Data Found - Try Another Verification Method".

If you have a bank account that you know is good (i.e. one that worked in another ACH billing setup), you can override the network check by telling ActionKit to use the independent check instead. The independent check just says that you are taking responsibility for verifying the account and don't want any further checking done before trying to charge it. Since this could be abused to enter fraudulent bank accounts that would result in fees from Braintree, this will only work for staff who have the "Users - plus Donation Management" permission and are currently logged in to the ActionKit admin.

You will need to make a new templateset for this use and add the following HTML to your donation form:

{% if request.user.is_authenticated %}
<div id="ak-fieldbox-ach_independent_check">
  <label for="ak-ach_independent_check">Override Network Check</label>
  <input type="checkbox" id="ak-ach_independent_check" name="ach_independent_check" value="1">
</div>
{% endif %}

This will send ach_independent_check=1 in your form submission when checked. When submitted by a logged-in staff member with the "Users - plus Donation Management" permission, that will override the network check. The check for request.user.is_authenticated is not required, but can be helpful. It hides the checkbox from people who stumble across the page but are not staff users.