diff --git a/civicrm.php b/civicrm.php
index b9787f8b900150642afc2bbedcb6b18aa8d8429d..23e7cdd85d34441ee05b8b5bcc5b4f5878781cc0 100644
--- a/civicrm.php
+++ b/civicrm.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: CiviCRM
  * Description: CiviCRM - Growing and Sustaining Relationships
- * Version: 5.59.3
+ * Version: 5.59.4
  * Requires at least: 4.9
  * Requires PHP:      7.3
  * Author: CiviCRM LLC
@@ -36,7 +36,7 @@ if (!defined('ABSPATH')) {
 }
 
 // Set version here: changing it forces Javascript and CSS to reload.
-define('CIVICRM_PLUGIN_VERSION', '5.59.3');
+define('CIVICRM_PLUGIN_VERSION', '5.59.4');
 
 // Store reference to this file.
 if (!defined('CIVICRM_PLUGIN_FILE')) {
diff --git a/civicrm/CRM/Case/Form/Case.php b/civicrm/CRM/Case/Form/Case.php
index ec036cebc441de6ee2a4ff74a0fbd1406de5adc3..76800a435858fd1dfcadb0e4fd72819b39cb85e9 100644
--- a/civicrm/CRM/Case/Form/Case.php
+++ b/civicrm/CRM/Case/Form/Case.php
@@ -200,7 +200,7 @@ class CRM_Case_Form_Case extends CRM_Core_Form {
    */
   public function setDefaultValues() {
     if ($this->_action & CRM_Core_Action::DELETE || $this->_action & CRM_Core_Action::RENEW) {
-      return TRUE;
+      return [];
     }
     $className = "CRM_Case_Form_Activity_{$this->_activityTypeFile}";
     $defaults = $className::setDefaultValues($this);
diff --git a/civicrm/CRM/Contribute/BAO/ContributionSoft.php b/civicrm/CRM/Contribute/BAO/ContributionSoft.php
index 2e0115bec9742f932e520f1481f1b515f451a254..8c7160f944b9b822c7b7097738d46465262a5fa2 100644
--- a/civicrm/CRM/Contribute/BAO/ContributionSoft.php
+++ b/civicrm/CRM/Contribute/BAO/ContributionSoft.php
@@ -391,10 +391,10 @@ class CRM_Contribute_BAO_ContributionSoft extends CRM_Contribute_DAO_Contributio
     // This is necessary for dataTables sorting.
     $dataTableMapping = [
       'sct_label' => 'soft_credit_type_id:label',
-      'contributor_name' => 'contact.sort_name',
+      'contributor_name' => 'contact_id.sort_name',
       'financial_type' => 'contribution_id.financial_type_id:label',
       'contribution_status' => 'contribution_id.contribution_status_id:label',
-      'receive_date' => 'contribution.receive_date',
+      'receive_date' => 'contribution_id.receive_date',
       'pcp_title' => 'pcp_id.title',
       'amount' => 'amount',
     ];
diff --git a/civicrm/CRM/Contribute/Form/ContributionBase.php b/civicrm/CRM/Contribute/Form/ContributionBase.php
index c31e6ee9afb8e9a1265d9a0e13c367da8e1b8a4e..024e262c0b4a6fbe3d2dc8834d89d40b8712a85c 100644
--- a/civicrm/CRM/Contribute/Form/ContributionBase.php
+++ b/civicrm/CRM/Contribute/Form/ContributionBase.php
@@ -779,10 +779,10 @@ class CRM_Contribute_Form_ContributionBase extends CRM_Core_Form {
 
     // The concept of contributeMode is deprecated.
     // The payment processor object can provide info about the fields it shows.
-    if ($isMonetary && is_a($this->_paymentProcessor['object'], 'CRM_Core_Payment')) {
-      /** @var  \CRM_Core_Payment $paymentProcessorObject */
+    if ($isMonetary && $this->_paymentProcessor['object'] instanceof \CRM_Core_Payment) {
       $paymentProcessorObject = $this->_paymentProcessor['object'];
-
+      $this->assign('paymentAgreementTitle', $paymentProcessorObject->getText('agreementTitle', []));
+      $this->assign('paymentAgreementText', $paymentProcessorObject->getText('agreementText', []));
       $paymentFields = $paymentProcessorObject->getPaymentFormFields();
       foreach ($paymentFields as $index => $paymentField) {
         if (!isset($this->_params[$paymentField])) {
diff --git a/civicrm/CRM/Core/Payment.php b/civicrm/CRM/Core/Payment.php
index 84e47800bcd89ff3e237706d2e99830329b87a3b..10afed4e59dcc2543c65413a3762ac5ecd0495b1 100644
--- a/civicrm/CRM/Core/Payment.php
+++ b/civicrm/CRM/Core/Payment.php
@@ -619,6 +619,22 @@ abstract class CRM_Core_Payment {
         }
         return '';
 
+      case 'agreementTitle':
+        if ($this->getPaymentTypeName() !== 'direct_debit' || $this->_paymentProcessor['billing_mode'] != 1) {
+          return '';
+        }
+        // @todo - 'encourage' processors to override...
+        // CRM_Core_Error::deprecatedWarning('Payment processors should override getText for agreement text');
+        return ts('Agreement');
+
+      case 'agreementText':
+        if ($this->getPaymentTypeName() !== 'direct_debit' || $this->_paymentProcessor['billing_mode'] != 1) {
+          return '';
+        }
+        // @todo - 'encourage' processors to override...
+        // CRM_Core_Error::deprecatedWarning('Payment processors should override getText for agreement text');
+        return ts('Your account data will be used to charge your bank account via direct debit. While submitting this form you agree to the charging of your bank account via direct debit.');
+
     }
     CRM_Core_Error::deprecatedFunctionWarning('Calls to getText must use a supported method');
     return '';
diff --git a/civicrm/CRM/Dedupe/Merger.php b/civicrm/CRM/Dedupe/Merger.php
index 2c02b615e191bd93a575811e28f92b2e7c1a50b6..5e352409b0214ec94372ed2e32a86c62b6119d82 100644
--- a/civicrm/CRM/Dedupe/Merger.php
+++ b/civicrm/CRM/Dedupe/Merger.php
@@ -2122,9 +2122,9 @@ ORDER BY civicrm_custom_group.weight,
       if (strpos($key, 'custom_') === 0) {
         $fieldID = (int) substr($key, 7);
         if (empty($cFields[$fieldID])) {
-          $htmlType = $cFields[$fieldID]['attributes']['html_type'];
+          $htmlType = (string) $cFields[$fieldID]['attributes']['html_type'];
           $isSerialized = CRM_Core_BAO_CustomField::isSerialized($cFields[$fieldID]['attributes']);
-          $isView = $cFields[$fieldID]['attributes']['is_view'];
+          $isView = (bool) $cFields[$fieldID]['attributes']['is_view'];
           $submitted = self::processCustomFields($mainId, $key, $submitted, $value, $fieldID, $isView, $htmlType, $isSerialized);
 
         }
diff --git a/civicrm/CRM/Event/BAO/Event.php b/civicrm/CRM/Event/BAO/Event.php
index 23d26017feb7a8029451902f1ebf6ad87556c6c7..ce018aadf52a3d3dcea47e4e1f57713492cfc99f 100644
--- a/civicrm/CRM/Event/BAO/Event.php
+++ b/civicrm/CRM/Event/BAO/Event.php
@@ -108,7 +108,12 @@ class CRM_Event_BAO_Event extends CRM_Event_DAO_Event implements \Civi\Core\Hook
       if (!empty($params['template_id'])) {
         $copy = self::copy($params['template_id']);
         $params['id'] = $copy->id;
+
         unset($params['template_id']);
+        // unless we are explicitly trying to create a new template
+        // we want to set the `is_template` flag on the clone to false
+        // so that the copy is a new event rather than a new template
+        $params['is_template'] = $params['is_template'] ?? 0;
 
         //fix for api from template creation bug
         civicrm_api4('ActionSchedule', 'update', [
diff --git a/civicrm/civicrm-version.php b/civicrm/civicrm-version.php
index b08926b6d46de96cbdb9bca51230bb0b2f1aa414..4b9c6850effd8df192312ba4d33c4768cf879d4e 100644
--- a/civicrm/civicrm-version.php
+++ b/civicrm/civicrm-version.php
@@ -1,7 +1,7 @@
 <?php
 /** @deprecated */
 function civicrmVersion( ) {
-  return array( 'version'  => '5.59.3',
+  return array( 'version'  => '5.59.4',
                 'cms'      => 'Wordpress',
                 'revision' => '' );
 }
diff --git a/civicrm/ext/afform/admin/info.xml b/civicrm/ext/afform/admin/info.xml
index 4979591a570939649a418e595cdf10878b5f9eed..72a3c53f90abe3559081643a9ac979a8a27b033c 100644
--- a/civicrm/ext/afform/admin/info.xml
+++ b/civicrm/ext/afform/admin/info.xml
@@ -13,7 +13,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-01-09</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>beta</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/afform/core/info.xml b/civicrm/ext/afform/core/info.xml
index 7baa13bdcb388fa5763ada860da82a0c0ac8aa7c..dd0a196c11997800d1cf9616196bc92078edb357 100644
--- a/civicrm/ext/afform/core/info.xml
+++ b/civicrm/ext/afform/core/info.xml
@@ -13,7 +13,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-01-09</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>beta</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/afform/html/info.xml b/civicrm/ext/afform/html/info.xml
index fb592924c199af7497c9fd0336dccd3d9226aad4..c360e03868c26b71e86197f17b54457c4f7197d7 100644
--- a/civicrm/ext/afform/html/info.xml
+++ b/civicrm/ext/afform/html/info.xml
@@ -13,7 +13,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-01-09</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/afform/mock/info.xml b/civicrm/ext/afform/mock/info.xml
index e3248cf419d58c9be1d21a5ceae14a23fc0977f3..4337a37ed2ddf9ac7719fbd9816cebd9c9519bcd 100644
--- a/civicrm/ext/afform/mock/info.xml
+++ b/civicrm/ext/afform/mock/info.xml
@@ -12,7 +12,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-01-09</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/authx/info.xml b/civicrm/ext/authx/info.xml
index c900047955dbd33b11ea0090dd1f172d3d27a22b..358aca262daa56948ea5693f756db9760b3accac 100644
--- a/civicrm/ext/authx/info.xml
+++ b/civicrm/ext/authx/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2021-02-11</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/civicrm_admin_ui/info.xml b/civicrm/ext/civicrm_admin_ui/info.xml
index 634f5d155e4b0f4ec0909bc44e18dc150e4c52c5..1cf065a4c31207cf8e3b2183a12476f37650dad7 100644
--- a/civicrm/ext/civicrm_admin_ui/info.xml
+++ b/civicrm/ext/civicrm_admin_ui/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2022-01-02</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/civigrant/info.xml b/civicrm/ext/civigrant/info.xml
index b88e1be01af63b78a5a97352fb27d3edf5454e4d..17a488eb54b7e96dd2893a3877047a783432b03f 100644
--- a/civicrm/ext/civigrant/info.xml
+++ b/civicrm/ext/civigrant/info.xml
@@ -13,7 +13,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2021-11-11</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/civiimport/info.xml b/civicrm/ext/civiimport/info.xml
index f0de9fa3874cd6d042c5f6cae36eb92ebd96c26d..1097ce32bea768aa1fb5337c42c4e399617309ac 100644
--- a/civicrm/ext/civiimport/info.xml
+++ b/civicrm/ext/civiimport/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2022-08-11</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/ckeditor4/info.xml b/civicrm/ext/ckeditor4/info.xml
index d6b7352471bbe5f646d4583160d4308f09fc850a..af8a9d240b106b27be82c38b28e06a950647ab8e 100644
--- a/civicrm/ext/ckeditor4/info.xml
+++ b/civicrm/ext/ckeditor4/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">https://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2021-05-23</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/contributioncancelactions/info.xml b/civicrm/ext/contributioncancelactions/info.xml
index 1cec3c0ca31d1c9ff2a8574709e35e9fdcd8e9a5..f40b8c2d328c097b2b507c74dc316fb9fe3d8566 100644
--- a/civicrm/ext/contributioncancelactions/info.xml
+++ b/civicrm/ext/contributioncancelactions/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-10-12</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/elavon/info.xml b/civicrm/ext/elavon/info.xml
index 55b7e2fb5dfaffe4d45f6a1d02c1aa8732dcb0b0..cf49eca08d4d6a486fbf62af5281ea285b61c09e 100644
--- a/civicrm/ext/elavon/info.xml
+++ b/civicrm/ext/elavon/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2022-08-05</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/eventcart/info.xml b/civicrm/ext/eventcart/info.xml
index cbcfbfffcf5b8e9e199cc5e747fd15fe07abef84..11f7c5485053ec907d9cc75d8f6d774e8b3aaf91 100644
--- a/civicrm/ext/eventcart/info.xml
+++ b/civicrm/ext/eventcart/info.xml
@@ -13,7 +13,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-08-03</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/ewaysingle/info.xml b/civicrm/ext/ewaysingle/info.xml
index b2e3597b7814214c498dc37f188f5db74e1d1b61..644b6c005df5f4e1ce62c718eaaccece9307f687 100644
--- a/civicrm/ext/ewaysingle/info.xml
+++ b/civicrm/ext/ewaysingle/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-10-07</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/financialacls/info.xml b/civicrm/ext/financialacls/info.xml
index f7aa50b2a83ee062e5b1b0c67ea729f8cb139235..f79347bbbf62e91a45e9483d0b93d7d171e3b307 100644
--- a/civicrm/ext/financialacls/info.xml
+++ b/civicrm/ext/financialacls/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-08-27</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/flexmailer/info.xml b/civicrm/ext/flexmailer/info.xml
index f3c8a564f2beacbff42290288ecfa60926c65a66..8abc677e8f4f8577936701152eb4697c46999125 100644
--- a/civicrm/ext/flexmailer/info.xml
+++ b/civicrm/ext/flexmailer/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-08-05</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <comments>
     FlexMailer is an email delivery engine which replaces the internal guts
diff --git a/civicrm/ext/greenwich/info.xml b/civicrm/ext/greenwich/info.xml
index a1c8efd52b26d11cab55450ad386129e116fbef1..4d92444446e676b334c6987b1940f560356ef445 100644
--- a/civicrm/ext/greenwich/info.xml
+++ b/civicrm/ext/greenwich/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-07-21</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/iatspayments/CRM/Core/Payment/Faps.php b/civicrm/ext/iatspayments/CRM/Core/Payment/Faps.php
index 5719b25bcf215400739fdec4498c6e02aa354f67..d72b13c58e83c8cf09da9e08143fecdb1cb10cbd 100644
--- a/civicrm/ext/iatspayments/CRM/Core/Payment/Faps.php
+++ b/civicrm/ext/iatspayments/CRM/Core/Payment/Faps.php
@@ -191,7 +191,7 @@ class CRM_Core_Payment_Faps extends CRM_Core_Payment {
     $markup = '<link type="text/css" rel="stylesheet" href="'.$cryptoCss.'" media="all" />'; // <script type="text/javascript" src="'.$cryptojs.'"></script>';
     CRM_Core_Region::instance('billing-block')->add(array(
       'markup' => $markup,
-    )); 
+    ));
     // the cryptojs above is the one on the 1pay server, now I load and invoke the extension's crypto.js
     $myCryptoJs = $resources->getUrl('com.iatspayments.civicrm', 'js/crypto.js');
     // after manually doing what addVars('iats', $jsVariables) would normally do
@@ -268,18 +268,17 @@ class CRM_Core_Payment_Faps extends CRM_Core_Payment {
    */
   public function doPayment(&$params, $component = 'contribute') {
     // CRM_Core_Error::debug_var('doPayment params', $params);
+    if (empty($params['amount'])) {
+      return _iats_payment_status_complete();
+    }
 
-    // Check for valid currency [todo: we have C$ support, but how do we check,
-    // or should we?]
-    if (
-        'USD' != $params['currencyID']
-     && 'CAD' != $params['currencyID']
-    ) {
-      return self::error('Invalid currency selection: ' . $params['currencyID']);
+    $isRecur = CRM_Utils_Array::value('is_recur', $params);
+    if ($isRecur && empty($params['contributionRecurID'])) {
+      return self::error('Invalid call to doPayment with is_recur and no contributionRecurID');
     }
-    $isRecur = CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID'];
     $usingCrypto = !empty($params['cryptogram']);
-    $ipAddress = (function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']);
+    // FAPS only allows ipv4 addresses
+    $ipAddress = CRM_Iats_Transaction::remote_ip_address(FILTER_FLAG_IPV4);
     $credentials = array(
       'merchantKey' => $this->_paymentProcessor['signature'],
       'processorId' => $this->_paymentProcessor['user_name']
diff --git a/civicrm/ext/iatspayments/CRM/Core/Payment/FapsACH.php b/civicrm/ext/iatspayments/CRM/Core/Payment/FapsACH.php
index ebb1bec2fefcb1623ff81163c80d1808563c5b05..7b9a7f1a525f38414e1b4a27d2df3632d56fa2d4 100644
--- a/civicrm/ext/iatspayments/CRM/Core/Payment/FapsACH.php
+++ b/civicrm/ext/iatspayments/CRM/Core/Payment/FapsACH.php
@@ -125,14 +125,16 @@ class CRM_Core_Payment_FapsACH extends CRM_Core_Payment_Faps {
    */
   public function doPayment(&$params, $component = 'contribute') {
     // CRM_Core_Error::debug_var('doPayment params', $params);
+    if (empty($params['amount'])) {
+      return _iats_payment_status_complete();
+    }
 
-    // Check for valid currency
-    $currency = $params['currencyID'];
-    if (('USD' != $currency) && ('CAD' != $currency)) {
-      return self::error('Invalid currency selection: ' . $currency);
+    $isRecur = CRM_Utils_Array::value('is_recur', $params);
+    if ($isRecur && empty($params['contributionRecurID'])) {
+      return self::error('Invalid call to doPayment with is_recur and no contributionRecurID');
     }
-    $isRecur = CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID'];
-    $ipAddress = (function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']);
+    // FAPS only allows ipv4 addresses
+    $ipAddress = CRM_Iats_Transaction::remote_ip_address($FILTER_FLAG_IPV4);
     $credentials = array(
       'merchantKey' => $this->_paymentProcessor['signature'],
       'processorId' => $this->_paymentProcessor['user_name']
diff --git a/civicrm/ext/iatspayments/CRM/Core/Payment/iATSService.php b/civicrm/ext/iatspayments/CRM/Core/Payment/iATSService.php
index d1948eba9b7e32eae67fcef0b63f9ab4a9128419..95169d269db091b18e2d54ae8380c32af7454dc9 100644
--- a/civicrm/ext/iatspayments/CRM/Core/Payment/iATSService.php
+++ b/civicrm/ext/iatspayments/CRM/Core/Payment/iATSService.php
@@ -170,16 +170,22 @@ class CRM_Core_Payment_iATSService extends CRM_Core_Payment {
    */
   public function doPayment(&$params, $component = 'contribute') {
 
+    if (empty($params['amount'])) {
+      return _iats_payment_status_complete();
+    }
     if (!$this->_profile) {
       return self::error('Unexpected error, missing profile');
     }
     // Use the iATSService object for interacting with iATS. Recurring contributions go through a more complex process.
-    $isRecur = CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID'];
+    $isRecur = CRM_Utils_Array::value('is_recur', $params);
+    if ($isRecur && empty($params['contributionRecurID'])) {
+      return self::error('Invalid call to doPayment with is_recur and no contributionRecurID');
+    }
     $methodType = $isRecur ? 'customer' : 'process';
     $method = $isRecur ? 'create_credit_card_customer' : 'cc';
-    $iats = new CRM_Iats_iATSServiceRequest(array('type' => $methodType, 'method' => $method, 'iats_domain' => $this->_profile['iats_domain'], 'currencyID' => $params['currencyID']));
+    $iats = new CRM_Iats_iATSServiceRequest(array('type' => $methodType, 'method' => $method, 'iats_domain' => $this->_profile['iats_domain'], 'currency' => $params['currency']));
     $request = $this->convertParams($params, $method);
-    $request['customerIPAddress'] = (function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']);
+    $request['customerIPAddress'] = CRM_Iats_Transaction::remote_ip_address();
     $credentials = array(
       'agentCode' => $this->_paymentProcessor['user_name'],
       'password'  => $this->_paymentProcessor['password'],
@@ -272,7 +278,7 @@ class CRM_Core_Payment_iATSService extends CRM_Core_Payment {
         }
         else {
           // run the (first) transaction immediately
-          $iats = new CRM_Iats_iATSServiceRequest(array('type' => 'process', 'method' => 'cc_with_customer_code', 'iats_domain' => $this->_profile['iats_domain'], 'currencyID' => $params['currencyID']));
+          $iats = new CRM_Iats_iATSServiceRequest(array('type' => 'process', 'method' => 'cc_with_customer_code', 'iats_domain' => $this->_profile['iats_domain'], 'currency' => $params['currency']));
           $request = array('invoiceNum' => $params['invoiceID']);
           $request['total'] = sprintf('%01.2f', CRM_Utils_Rule::cleanMoney($params['amount']));
           $request['customerCode'] = $customer_code;
@@ -462,17 +468,8 @@ class CRM_Core_Payment_iATSService extends CRM_Core_Payment {
    */
   public function updateSubscriptionBillingInfo(&$message = '', $params = array()) {
 
-    // Fix billing form update bug https://github.com/iATSPayments/com.iatspayments.civicrm/issues/252 by getting crid from _POST
-    if (empty($params['crid'])) {
-      $params['crid'] = !empty($_POST['crid']) ? (int) $_POST['crid'] : (!empty($_GET['crid']) ? (int) $_GET['crid'] : 0);
-      if (empty($params['crid']) && !empty($params['entryURL'])) {
-        $components = parse_url($params['entryURL']);
-        parse_str(html_entity_decode($components['query']), $entryURLquery);
-        $params['crid'] = $entryURLquery['crid'];
-      }
-    }
-    // updatedBillingInfo array changed sometime after 4.7.27
-    $crid = !empty($params['crid']) ? $params['crid'] : $params['recur_id'];
+    // updatedBillingInfo array has changed a few times, we'll try a few different keys to pull the contribution recurring id
+    $crid = !empty($params['contributionRecurID']) ? $params['contributionRecurID'] : (!empty($params['crid']) ? $params['crid'] : $params['recur_id']);
     if (empty($crid)) {
       $alert = ts('This system is unable to perform self-service updates to credit cards. Please contact the administrator of this site.');
       throw new Exception($alert);
diff --git a/civicrm/ext/iatspayments/CRM/Core/Payment/iATSServiceACHEFT.php b/civicrm/ext/iatspayments/CRM/Core/Payment/iATSServiceACHEFT.php
index 5332d44cca0d45eefe508ce0d2e696f964832c99..4c1765deab63695ef5542ca70dd72cf278268567 100644
--- a/civicrm/ext/iatspayments/CRM/Core/Payment/iATSServiceACHEFT.php
+++ b/civicrm/ext/iatspayments/CRM/Core/Payment/iATSServiceACHEFT.php
@@ -203,17 +203,23 @@ class CRM_Core_Payment_iATSServiceACHEFT extends CRM_Core_Payment_iATSService {
    */
   public function doPayment(&$params, $component = 'contribute') {
 
+    if (empty($params['amount'])) {
+      return _iats_payment_status_complete();
+    }
     if (!$this->_profile) {
       return self::error('Unexpected error, missing profile');
     }
     // Use the iATSService object for interacting with iATS, mostly the same for recurring contributions.
     // We handle both one-time and recurring ACH/EFT
-    $isRecur = CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID'];
+    $isRecur = CRM_Utils_Array::value('is_recur', $params);
+    if ($isRecur && empty($params['contributionRecurID'])) {
+      return self::error('Invalid call to doPayment with is_recur and no contributionRecurID');
+    }
     $methodType = $isRecur ? 'customer' : 'process';
     $method = $isRecur ? 'create_acheft_customer_code' : 'acheft';
-    $iats = new CRM_Iats_iATSServiceRequest(array('type' => $methodType, 'method' => $method, 'iats_domain' => $this->_profile['iats_domain'], 'currencyID' => $params['currencyID']));
+    $iats = new CRM_Iats_iATSServiceRequest(array('type' => $methodType, 'method' => $method, 'iats_domain' => $this->_profile['iats_domain'], 'currency' => $params['currency']));
     $request = $this->convertParams($params, $method);
-    $request['customerIPAddress'] = (function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']);
+    $request['customerIPAddress'] = CRM_Iats_Transaction::remote_ip_address();
     $credentials = array(
       'agentCode' => $this->_paymentProcessor['user_name'],
       'password'  => $this->_paymentProcessor['password'],
@@ -313,7 +319,7 @@ class CRM_Core_Payment_iATSServiceACHEFT extends CRM_Core_Payment_iATSService {
           return $params;
         }
         else {
-          $iats = new CRM_Iats_iATSServiceRequest(array('type' => 'process', 'method' => 'acheft_with_customer_code', 'iats_domain' => $this->_profile['iats_domain'], 'currencyID' => $params['currencyID']));
+          $iats = new CRM_Iats_iATSServiceRequest(array('type' => 'process', 'method' => 'acheft_with_customer_code', 'iats_domain' => $this->_profile['iats_domain'], 'currency' => $params['currency']));
           $request = array('invoiceNum' => $params['invoiceID']);
           $request['total'] = sprintf('%01.2f', CRM_Utils_Rule::cleanMoney($params['amount']));
           $request['customerCode'] = $customer_code;
diff --git a/civicrm/ext/iatspayments/CRM/Iats/Form/Settings.php b/civicrm/ext/iatspayments/CRM/Iats/Form/Settings.php
index ff46b3bfb6bb539cf4ca45224ce229fbcd6e9c12..2805404e67e233a319a46018b537b68207792253 100644
--- a/civicrm/ext/iatspayments/CRM/Iats/Form/Settings.php
+++ b/civicrm/ext/iatspayments/CRM/Iats/Form/Settings.php
@@ -16,7 +16,13 @@ class CRM_Iats_Form_Settings extends CRM_Core_Form {
       'email_recurring_failure_report',
       ts('Email Recurring Contribution failure reports to this Email address')
     );
+    $this->add(
+      'email',
+      'bcc_email_recurring_failure_report',
+      ts('BCC Email Recurring Contribution failure reports to this Email address.')
+    );
     $this->addRule('email_recurring_failure_report', ts('Email address is not a valid format.'), 'email');
+    $this->addRule('bcc_email_recurring_failure_report', ts('Email address is not a valid format.'), 'email');
     $this->add(
       'text',
       'recurring_failure_threshhold',
@@ -30,6 +36,12 @@ class CRM_Iats_Form_Settings extends CRM_Core_Form {
       ts('Email receipt for a Contribution in a Recurring Series'),
       $receipt_recurring_options
     );
+    
+    $this->add(
+      'checkbox',
+      'email_failure_contribution_receipt',
+        ts('Email receipt for a Contribution if Recurring payment fails - with error message')
+    );
 
     $this->add(
       'checkbox',
diff --git a/civicrm/ext/iatspayments/CRM/Iats/Transaction.php b/civicrm/ext/iatspayments/CRM/Iats/Transaction.php
index 32857cce6ecdaaf307e7f2ca2527b1ec9181740d..f0a84aebc3f55d377b2aa158a6dc6faea5e7303e 100644
--- a/civicrm/ext/iatspayments/CRM/Iats/Transaction.php
+++ b/civicrm/ext/iatspayments/CRM/Iats/Transaction.php
@@ -80,7 +80,7 @@ class CRM_Iats_Transaction {
    *   Used in the Iatsrecurringcontributions.php job and the one-time ('card on file') form.
    *   
    */
-  static function process_contribution_payment(&$contribution, $paymentProcessor, $payment_token) {
+  static function process_contribution_payment(&$contribution, $paymentProcessor, $payment_token, $contributionRecurUpdate = []) {
     // By default, don't use repeattransaction
     $use_repeattransaction = FALSE;
     $is_recurrence = !empty($contribution['original_contribution_id']);
@@ -93,7 +93,7 @@ class CRM_Iats_Transaction {
     // Handle any case of a failure of some kind, either the card failed, or the system failed.
     if (!$success) {
       $error_message = $payment_result['message'];
-      /* set the failed transaction status, or pending if I had a server issue */
+      /* set the failed transaction status (=4), or pending (= 2) if I had a server issue */
       $contribution['contribution_status_id'] = empty($auth_code) ? 2 : 4;
       /* and include the reason in the source field */
       $contribution['source'] .= ' ' . $error_message;
@@ -108,6 +108,34 @@ class CRM_Iats_Transaction {
       // 2. if we don't already have a contribution id
       $use_repeattransaction = $is_recurrence && empty($contribution['id']);
     }
+    // Update the recurring contribution record with the next scheduled contribution date
+    // Note: applies if we have come from the iats recurring contribution job.
+    if (!empty($contributionRecurUpdate)) {
+      /* by default, just set the failure count back to 0 */
+      /* special handling for confirmed, hopefully transient, card failures: try again at next opportunity if we haven't failed too often */
+      if (4 == $contribution['contribution_status_id']) {
+        $contributionRecurUpdate['failure_count'] = $contributionRecurUpdate['failure_count'] + 1;
+        /* if it has failed and the failure threshold will not be reached with this failure, set the next sched contribution date to what it was */
+        if ($contributionRecurUpdate['failure_count'] < $contributionRecurUpdate['failure_threshold']) {
+          // Override/set the next_sched_contribution_date to it's current value so we can try again.
+          $contributionRecurUpdate['next_sched_contribution_date'] = $contributionRecurUpdate['current_sched_contribution_date'];
+        }
+        // otherwise, we'll keep the next collection date as passed in from the caller
+      }
+      elseif (!empty($auth_code)) {
+        // set the failure count back to zero unless I had a server issue
+        $contributionRecurUpdate['failure_count'] = 0;
+      }
+      // unset some of the passed in values that were useful but not part of the recurring record
+      unset($contributionRecurUpdate['failure_threshold']);
+      unset($contributionRecurUpdate['current_sched_contribution_date']);
+      try {
+        civicrm_api3('ContributionRecur', 'create', $contributionRecurUpdate);
+      }
+      catch (Exception $e) {
+        // Ignore this, though perhaps I should log it.
+      }
+    }
     if ($use_repeattransaction) {
       // We processed it successflly and I can try to use repeattransaction. 
       // Requires the original contribution id.
@@ -141,13 +169,18 @@ class CRM_Iats_Transaction {
         // If repeattransaction succeded.
         // First restore/add various fields that the repeattransaction api may overwrite or ignore.
         // TODO - fix this in core to allow these to be set above.
-        civicrm_api3('contribution', 'create', array('id' => $contribution['id'], 
-          'invoice_id' => $contribution['invoice_id'],
-          'source' => $contribution['source'],
-          'receive_date' => $contribution['receive_date'],
-          'payment_instrument_id' => $contribution['payment_instrument_id'],
-          // '' => $contribution['receive_date'],
-        ));
+        try {
+          civicrm_api3('contribution', 'create', array('id' => $contribution['id'], 
+            'invoice_id' => $contribution['invoice_id'],
+            'source' => $contribution['source'],
+            'receive_date' => $contribution['receive_date'],
+            'payment_instrument_id' => $contribution['payment_instrument_id'],
+            // '' => $contribution['receive_date'],
+          ));
+        }
+        catch (Exception $e) {
+          // Not sure why this might fail, but let's be careful
+        }
         // Save my status in the contribution array that was passed in.
         $contribution['contribution_status_id'] = $payment_result['payment_status_id'];
         if ($contribution['contribution_status_id'] == 1) {
@@ -390,7 +423,7 @@ class CRM_Iats_Transaction {
    * @param $start_date a timestamp, only return dates after this.
    * @param $allow_days an array of allowable days of the month.
    *
-   *   A low-level utility function for triggering a transaction on iATS.
+   *   A low-level utility function used to get an array of the next allowable start dates
    */
   static function get_future_monthly_start_dates($start_date, $allow_days) {
     // Future date options.
@@ -399,7 +432,7 @@ class CRM_Iats_Transaction {
     $today = date('Ymd').'030000';
     // If not set, only allow for the first 28 days of the month.
     if (max($allow_days) <= 0) {
-      $allow_days = range(1,28);
+      $allow_days = range(1,31);
     }
     for ($j = 0; $j < count($allow_days); $j++) {
       // So I don't get into an infinite loop somehow ..
@@ -422,4 +455,21 @@ class CRM_Iats_Transaction {
     }
     return $start_dates;
   }
+
+
+  /*
+   * Get the ip of the source of the transaction.
+   *
+   * Test to make sure we're not sending an invalid value!
+   * $param $filter = an optional additional FILTER to validate the ip
+   */
+  static function remote_ip_address($filter = 0) {
+    $ipAddress = (function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']);
+    $filter = $filter | FILTER_FLAG_NO_PRIV_RANGE;
+    if (!filter_var($ipAddress, FILTER_VALIDATE_IP, $filter)) {
+        $ipAddress = '';
+    }
+    return $ipAddress;
+  }
+
 }
diff --git a/civicrm/ext/iatspayments/CRM/Iats/iATSServiceRequest.php b/civicrm/ext/iatspayments/CRM/Iats/iATSServiceRequest.php
index 1370012442c280d0dd5ab35b6197470f9605fdd1..7e149e17df4a81cca54591b4ef16e91e3d228312 100644
--- a/civicrm/ext/iatspayments/CRM/Iats/iATSServiceRequest.php
+++ b/civicrm/ext/iatspayments/CRM/Iats/iATSServiceRequest.php
@@ -75,19 +75,19 @@ class CRM_Iats_iATSServiceRequest {
     $this->options = $options;
     $this->options['debug'] = _iats_civicrm_domain_info('debug_enabled');
     // Check for valid currencies with domain/method combinations.
-    if (isset($options['currencyID'])) {
+    if (isset($options['currency'])) {
       $valid = FALSE;
       switch ($iats_domain) {
         case 'www2.iatspayments.com':
         case 'www.iatspayments.com':
-          if (in_array($options['currencyID'], array('USD', 'CAD'))) {
+          if (in_array($options['currency'], array('USD', 'CAD'))) {
             $valid = TRUE;
           }
           break;
 
         case 'www.uk.iatspayments.com':
           if ('cc' == substr($method, 0, 2) || 'create_credit_card_customer' == $method) {
-            if (in_array($options['currencyID'], array('AUD', 'USD', 'EUR', 'GBP', 'IEE', 'CHF', 'HKD', 'JPY', 'SGD', 'MXN'))) {
+            if (in_array($options['currency'], array('AUD', 'USD', 'EUR', 'GBP', 'IEE', 'CHF', 'HKD', 'JPY', 'SGD', 'MXN'))) {
               $valid = TRUE;
             }
           }
@@ -98,7 +98,7 @@ class CRM_Iats_iATSServiceRequest {
           break;
       }
       if (!$valid) {
-        throw new PaymentProcessorException(ts('Invalid currency selection: %1 for domain %2', [1 => $options['currencyID'], 2=> $iats_domain]));
+        throw new PaymentProcessorException(ts('Invalid currency selection: %1 for domain %2', [1 => $options['currency'], 2=> $iats_domain]));
       }
     }
   }
@@ -272,8 +272,8 @@ class CRM_Iats_iATSServiceRequest {
       case 'process':
         if (!empty($response->PROCESSRESULT)) {
           $processresult = $response->PROCESSRESULT;
-          $result['auth_result'] = trim(current($processresult->AUTHORIZATIONRESULT));
-          $result['remote_id'] = current($processresult->TRANSACTIONID);
+          $result['auth_result'] = trim(((array) $processresult->AUTHORIZATIONRESULT)[0] ?? '');
+          $result['remote_id'] = ((array) $processresult->TRANSACTIONID)[0] ?? '';
           // If we didn't get an approval response code...
           // Note: do not use SUCCESS property, which just means iATS said "hello".
           $result['status'] = (substr($result['auth_result'], 0, 2) == self::iATS_TXN_OK) ? 1 : 0;
@@ -621,7 +621,7 @@ class CRM_Iats_iATSServiceRequest {
         return 'Invalid transaction. Verify and re-enter credit card information.';
 
       case 'REJECT: 6':
-        return 'Please have cardholder call the number on the back of the card.';
+        return 'US: Please have cardholder call the number on the back of the card. UK/EU: Transaction not supported by institution.';
 
       case 'REJECT: 7':
         return 'Lost or stolen card.';
@@ -680,10 +680,10 @@ class CRM_Iats_iATSServiceRequest {
         return 'Please have cardholder call the number on the back of the card.';
 
       case 'REJECT: 32':
-        return 'Invalid charge card number.';
+        return 'Invalid Credit Card Number.';
 
       case 'REJECT: 39':
-        return 'Contact iATS at 1-888-955-5455.';
+        return 'US: Contact iATS at 1-888-955-5455. UK/EU: Contact IATS at 0808-234-0466';
 
       case 'REJECT: 40':
         return 'Invalid card number. Card not supported by iATS.';
@@ -695,7 +695,7 @@ class CRM_Iats_iATSServiceRequest {
         return 'CVV2 required.';
 
       case 'REJECT: 43':
-        return 'Incorrect AVS.';
+        return 'US: Contact iATS at 1-888-955-5455. UK/EU: Incorrect AVS.';
 
       case 'REJECT: 45':
         return 'Credit card name blocked. Call iATS at 1-888-955-5455.';
@@ -721,11 +721,65 @@ class CRM_Iats_iATSServiceRequest {
       case 'REJECT: 52':
         return 'Credit card BIN country blocked. Call iATS at 1-888-955-5455.';
 
+      case 'REJECT: 53':
+        return 'Wrong File Total';
+
+      case 'REJECT: 54':
+        return 'Wrong Currency';
+
+      case 'REJECT: 55':
+        return 'System Error';
+
+      case 'REJECT: 56':
+        return 'REJ Other';
+
+      case 'REJECT: 60':
+        return 'Set-up error. Please contact IATS Customer Service at 0808-234-0466.';
+
+      case 'REJECT: 61':
+        return 'Authentication failed.';
+
+      case 'REJECT: 62':
+        return 'Invalid timestamp. Please contact IATS Customer Service at 0808-234-0466.';
+
+      case 'REJECT: 63':
+        return 'Payment voided.';
+
+      case 'REJECT: 64':
+        return 'Pre-authorization expired / CC mismatch / Insufficient amount.';
+
+      case 'REJECT: 65':
+        return 'Unsupported card scheme. Please contact IATS Customer Service at 0808-234-0466.';
+
+      case 'REJECT: 66':
+        return 'Set-up error - Channel issue. Please contact IATS Customer Service at 0808-234-0466.';
+
+      case 'REJECT: 67':
+        return 'Set-up error - No submitter. Please contact IATS Customer Service at 0808-234-0466.';
+
+      case 'REJECT: 68':
+        return 'Void payment failure.';
+
+      case 'REJECT: 69':
+        return 'Unexpected response. Please contact IATS Customer Service at 0808-234-0466.';
+
+      case 'REJECT: 70':
+        return 'Result indeterminate. Please contact IATS Customer Service at 0808-234-0466.';
+
+      case 'REJECT: 71':
+        return 'Mandatory field missing.';
+
+      case 'REJECT: 91':
+        return 'Issuer unavailable.';
+
+      case 'REJECT: 98':
+        return 'Mod Check on RDFI-ID Failed.';
+
       case 'REJECT: 100':
         return 'DO NOT REPROCESS. Call iATS at 1-888-955-5455.';
 
-      case 'Timeout':
-        return 'The system has not responded in the time allotted. Call iATS at 1-888-955-5455.';
+      case 'TIMEOUT':
+        return 'The system has not responded in the time allotted. Call iATS at US: 1-888-955-5455, UK/EU: 0808-234-0466';
     }
 
     return $code;
diff --git a/civicrm/ext/iatspayments/README.md b/civicrm/ext/iatspayments/README.md
index db73818c88a8cb4bc7672aa81e39b7e9899aa5fa..5fb5fc9b538479969e28f9a2503872d0fe2534ce 100644
--- a/civicrm/ext/iatspayments/README.md
+++ b/civicrm/ext/iatspayments/README.md
@@ -109,6 +109,8 @@ Some issues may be related to core CiviCRM issues, and may not have an immediate
 
 Below is a list of some of the most common issues:
 
+Unexpected failures. If you get an unexpectedly large number of failures for your recurring contributions, please review this page to understand how the extension does it's best to handle them and what administrators can do: https://github.com/iATSPayments/com.iatspayments.civicrm/wiki/Recurring-Contribution-Failure-Handling
+
 9002 Error - if you get this when trying to make a contribution, then you're getting that error back from the iATS server due to an account misconfiguration. When this happens and your using a 'legacy' iATS account -> check if you have special characters in your password (and remove them). If you're using '1st Pay' contact iATS Customer Service to ensure your account is configured properly.
 
 CiviCRM core assigns Membership status (=new) and extends Membership End date as well as Event status (=registered) as soon as ACH/EFT is submitted (so while payment is still pending - this could be several days for ACH/EFT). If the contribution receives a Ok:BankAccept -> the extension will mark the contribution in CiviCRM as completed. If the contribution does NOT receive a Ok:BankAccept -> the extension will mark the contribution in CiviCRM as rejected - however - associated existing Membership and Event records may need to be updated manually.
diff --git a/civicrm/ext/iatspayments/api/v3/Job/Iatsrecurringcontributions.php b/civicrm/ext/iatspayments/api/v3/Job/Iatsrecurringcontributions.php
index 632bbe4de9271cc42824861bbc19cf987b2d98fe..aeb3a49ec6874fd29bf38cca99ac009c14839f6c 100644
--- a/civicrm/ext/iatspayments/api/v3/Job/Iatsrecurringcontributions.php
+++ b/civicrm/ext/iatspayments/api/v3/Job/Iatsrecurringcontributions.php
@@ -106,7 +106,7 @@ function civicrm_api3_job_Iatsrecurringcontributions($params) {
   $error_count  = 0;
   $output  = [];
   $settings = Civi::settings()->get('iats_settings');
-  $receipt_recurring = $settings['receipt_recurring'];
+  $receipt_recurring = $settings['receipt_recurring'] ?? null;
   $email_failure_report = empty($settings['email_recurring_failure_report']) ? '' : $settings['email_recurring_failure_report'];
   // By default, after 3 failures move the next scheduled contribution date forward.
   $failure_threshhold = empty($settings['recurring_failure_threshhold']) ? 3 : (int) $settings['recurring_failure_threshhold'];
@@ -124,7 +124,6 @@ function civicrm_api3_job_Iatsrecurringcontributions($params) {
     // CRM_Core_Error::debug_var('Contribution Template', $contribution_template);
     // generate my invoice id like CiviCRM does
     $hash = md5(uniqid(rand(), TRUE));
-    $failure_count    = $recurringContribution['failure_count'];
     $paymentProcessor = $paymentProcessors[$payment_processor_id];
     $paymentClass = substr($paymentProcessor['class_name'],8);
     $source = E::ts('iATS Payments (%1) Recurring Contribution ( id = %2 )', [
@@ -234,31 +233,20 @@ function civicrm_api3_job_Iatsrecurringcontributions($params) {
     // create the pending contribution and try to get the money, and then do one of:
     // update the contribution to failed, leave as pending for server failure, complete the transaction,
     // or update a pending ach/eft with it's transaction id.
-    // But first: advance the next collection date now so that in case of server failure on return from a payment request I don't try to take money again.
-    // Save the current value to restore in case of payment failure (perhaps ...).
-    $saved_next_sched_contribution_date = $recurringContribution['next_sched_contribution_date'];
-    /* calculate the next collection date, based on the recieve date (note effect of catchup mode, above)  */
+    // Assemble an array of recurring information so that process_contribution_payment can update the recurring record.
+    // But first: calculate next collection date now so that in case of server failure on return from a payment request I don't try to take money again.
+    // The next collection date is based on receive_ts, "recieve timestamp" (note effect of catchup mode, above)
     $next_collection_date = date('Y-m-d H:i:s', strtotime('+'.$recurringContribution['frequency_interval'].' '.$recurringContribution['frequency_unit'], $receive_ts));
-    $contribution_recur_set = array('version' => 3, 'id' => $contribution['contribution_recur_id'], 'next_sched_contribution_date' => $next_collection_date);
-    $result = CRM_Iats_Transaction::process_contribution_payment($contribution, $paymentProcessor, $payment_token);
+    // Note: keep track of the currently defined "next_sched_contribution_date" as "current_sched_contribution_date" in case of confirmed transient card failures.
+    $contribution_recur_update = array('id' => $contribution['contribution_recur_id'], 'next_sched_contribution_date' => $next_collection_date, 'failure_count' => $recurringContribution['failure_count'], 'failure_threshold' => $failure_threshhold, 'current_sched_contribution_date' => $recurringContribution['next_sched_contribution_date']);
+    // process the payment and update the contribution and recurring contribution records:
+    $result = CRM_Iats_Transaction::process_contribution_payment($contribution, $paymentProcessor, $payment_token, $contribution_recur_update);
     // append result message to report if I'm going to mail out a failures
     // report
     if ($email_failure_report && !$result['result']['success']) {
       $failure_report_text .= "\n".$result['message'];
     }
     $output[] = $result['message'];
-    /* by default, just set the failure count back to 0 */
-    $contribution_recur_set = array('version' => 3, 'id' => $contribution['contribution_recur_id'], 'failure_count' => '0', 'next_sched_contribution_date' => $next_collection_date);
-    /* special handling for failures: try again at next opportunity if we haven't failed too often */
-    if (4 == $contribution['contribution_status_id']) {
-      $contribution_recur_set['failure_count'] = $failure_count + 1;
-      /* if it has failed and the failure threshold will not be reached with this failure, set the next sched contribution date to what it was */
-      if ($contribution_recur_set['failure_count'] < $failure_threshhold) {
-        // Should the failure count be reset otherwise? It is not.
-        $contribution_recur_set['next_sched_contribution_date'] = $saved_next_sched_contribution_date;
-      }
-    }
-    civicrm_api('ContributionRecur', 'create', $contribution_recur_set);
     $result = civicrm_api('activity', 'create',
       array(
         'version'       => 3,
@@ -317,14 +305,28 @@ function civicrm_api3_job_Iatsrecurringcontributions($params) {
     list($fromName, $fromEmail) = CRM_Core_BAO_Domain::getNameAndEmail();
     $mailparams = array(
       'from' => $fromName . ' <' . $fromEmail . '> ',
-      'to' => 'System Administrator <' . $email_failure_report . '>',
+      'toName' => empty($fromName) ? ts('System Administrator') : $fromName,
+      'toEmail' => $email_failure_report,
+      'bcc' =>  !empty($bcc_email_failure_report) ?  $bcc_email_failure_report : '',
       'subject' => ts('iATS Recurring Payment job failure report: ' . date('c')),
       'text' => $failure_report_text,
-      'returnPath' => $fromEmail,
     );
     // print_r($mailparams);
     CRM_Utils_Mail::send($mailparams);
   }
+
+  // Send receipt with error message if [recurring only?] contribution fails   $email_failure_contribution_receipt
+  // CRM_Core_Error::debug_var('Contribution', $contribution);
+  // CRM_Core_Error::debug_var('iATS response message', $failure_report_text);
+    if ((strlen($failure_report_text) > 0) && $email_failure_contribution_receipt) {
+        return civicrm_api3('Contribution', 'sendconfirmation', [
+        'id' => $contribution['id'],
+        'receipt_from_name' => empty($fromName) ? ts('Admin') : $fromName,
+        'receipt_from_email' => $fromEmail,
+        'receipt_text' => ts('It seems something is not quite right with your recurring contribution payment. Please see details below.') . '<hr><br>' . $failure_report_text,
+        'bcc_receipt' => !empty($email_failure_report)? $email_failure_report: $fromEmail,
+        ]);
+    }
   // If errors ..
   if ($error_count > 0) {
     return civicrm_api3_create_error(
diff --git a/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.php b/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.php
index a86648b1fe335401645f92803aad9efc0e52b422..549d7530ad318050ccc5c4d6329a7d1e422114c7 100644
--- a/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.php
+++ b/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.php
@@ -96,7 +96,6 @@ function civicrm_api3_job_iatsreport($params) {
       foreach ($process_methods_per_type as $method => $payment_status_id) {
         // initialize my counts
         $processed[$user_name][$type][$method] = 0;
-        // watchdog('civicrm_iatspayments_com', 'pp: <pre>!pp</pre>', array('!pp' => print_r($payment_processor,TRUE)), WATCHDOG_NOTICE);
         /* get approvals from yesterday, approvals from previous days, and then rejections for this payment processor */
         /* we're going to assume that all the payment_processors_per_type are using the same server */
         $iats_service_params['method'] = $method;
@@ -187,7 +186,6 @@ function civicrm_api3_job_iatsreport($params) {
     }
   }
   Civi::settings()->set('iats_journal', $iats_journal);
-  // watchdog('civicrm_iatspayments_com', 'found: <pre>!found</pre>', array('!found' => print_r($processed,TRUE)), WATCHDOG_NOTICE);
   $message = '';
   foreach ($processed as $user_name => $p) {
     foreach ($p as $type => $ps) {
diff --git a/civicrm/ext/iatspayments/api/v3/Job/Iatsverify.php b/civicrm/ext/iatspayments/api/v3/Job/Iatsverify.php
index 61346f1f1b64a200e48b813bcc5a465b84d0d585..e648291c705769f2355625890e98ebd7bbb2a720 100644
--- a/civicrm/ext/iatspayments/api/v3/Job/Iatsverify.php
+++ b/civicrm/ext/iatspayments/api/v3/Job/Iatsverify.php
@@ -69,7 +69,7 @@ function _civicrm_api3_job_iatsverify_spec(&$spec) {
 function civicrm_api3_job_iatsverify($params) {
 
   $settings = Civi::settings()->get('iats_settings');
-  $receipt_recurring = $settings['receipt_recurring'];
+  $receipt_recurring = $settings['receipt_recurring'] ?? null;
   define('IATS_VERIFY_DAYS', 30);
   // I've added an extra 2 days when getting candidates from CiviCRM to be sure i've got them all.
   $verify_days = IATS_VERIFY_DAYS + 2;
@@ -183,7 +183,7 @@ function civicrm_api3_job_iatsverify($params) {
             // Restore source field and trxn_id that completetransaction overwrites
             civicrm_api3('contribution', 'create', array(
               'id' => $contribution['id'],
-              'source' => $contribution['source'],
+              'source' => ($contribution['contribution_source'] ?? $contribution['source']),
               'trxn_id' => $trxn_id,
             ));
             break;
diff --git a/civicrm/ext/iatspayments/iats.php b/civicrm/ext/iatspayments/iats.php
index 8f253aa1b32e18e3982ebed393d602ec3aad4101..23b32e0d73ac11f5cb0315770b4c12517a472027 100644
--- a/civicrm/ext/iatspayments/iats.php
+++ b/civicrm/ext/iatspayments/iats.php
@@ -338,10 +338,13 @@ function iats_civicrm_buildForm($formName, &$form) {
 /**
  * Modifications to a (public/frontend) contribution financial forms for iATS
  * procesors.
- * 1. enable public selection of future recurring contribution start date.
+ * 1. enable public selection of future recurring contribution start date, but only if the form allows recurring!
  * 
  * We're only handling financial payment class forms here. Note that we can no
  * longer test for whether the page has/is recurring or not. 
+ * Special note - if a page offers recurring contributions and if
+ * future recurring start dates are enabled with restrictions on which days,
+ * then any one-time contribution will be forced to use those rules as well.
  */
 
 function iats_civicrm_buildForm_CRM_Financial_Form_Payment(&$form) {
@@ -354,8 +357,11 @@ function iats_civicrm_buildForm_CRM_Financial_Form_Payment(&$form) {
   if (empty($type)) {
     return;
   }
-
-  // If enabled provide a way to set future contribution dates. 
+  // Skip if this form doesn't allow recurring
+  if (empty($form->_values['is_recur'])) {
+    return;
+  }
+  // If future public start dates are enabled on a recurring-enabled page ...
   // Uses javascript to hide/reset unless they have recurring contributions checked.
   $settings = Civi::settings()->get('iats_settings');
   if (!empty($settings['enable_public_future_recurring_start'])
@@ -819,3 +825,10 @@ function iats_civicrm_buildForm_CRM_Contribute_Form_UpdateBilling(&$form) {
     $form->addElement('hidden', 'crid', $crid);
   }
 }
+
+function _iats_payment_status_complete() {
+  return [
+    'payment_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'),
+    'payment_status' => 'Completed',
+  ];
+}
diff --git a/civicrm/ext/iatspayments/info.xml b/civicrm/ext/iatspayments/info.xml
index 76edc1453976e353de167ece662a40e5bd3d0163..3b438833530179326c3d6dfd380553aaed1e4319 100644
--- a/civicrm/ext/iatspayments/info.xml
+++ b/civicrm/ext/iatspayments/info.xml
@@ -9,7 +9,7 @@
     <url desc="Support">https://github.com/iATSPayments/com.iatspayments.civicrm/issues?state=open</url>
     <url desc="Installation Instructions">https://github.com/iATSPayments/com.iatspayments.civicrm</url>
     <url desc="Documentation">https://github.com/iATSPayments/com.iatspayments.civicrm</url>
-    <url desc="Release Notes">https://github.com/iATSPayments/com.iatspayments.civicrm/blob/master/release-notes/1.7.4.md</url>
+    <url desc="Release Notes">https://github.com/iATSPayments/com.iatspayments.civicrm/blob/master/release-notes/1.7.5.md</url>
     <url desc="Getting Started">https://www.semper-it.com/civicamp-iats-payments-slides</url>
   </urls>
   <license>AGPL-3.0</license>
@@ -17,11 +17,11 @@
     <author>Alan Dixon</author>
     <email>iats@blackflysolutions.ca</email>
   </maintainer>
-  <releaseDate>2021-03-22</releaseDate>
-  <version>1.7.4</version>
+  <releaseDate>2023-03-08</releaseDate>
+  <version>1.7.5</version>
   <develStage>stable</develStage>
   <compatibility>
-    <ver>5.13</ver>
+    <ver>5.49</ver>
   </compatibility>
   <comments>A recommended 1.7.x maintenance release, see release notes above for details.</comments>
   <civix>
diff --git a/civicrm/ext/iatspayments/js/dd_cad.js b/civicrm/ext/iatspayments/js/dd_cad.js
index f7260c0aa2b2f30d01b847e4d932359a96c3be35..f412abd38bff0b0c19475d65cd112df3a81eb863 100644
--- a/civicrm/ext/iatspayments/js/dd_cad.js
+++ b/civicrm/ext/iatspayments/js/dd_cad.js
@@ -21,7 +21,7 @@ CRM.$(function ($) {
   });
   $('#cad_bank_number').blur(function(eventObj) {
     var myCount = onlyNumbers($(this));
-    if (myCount != 3) {
+    if ((myCount > 0) && (myCount != 3)) {
       $(this).crmError(ts('Your Bank Number requires three digits, use a leading "0" if necessary')); 
     }
     switch($(this).val()) {
@@ -48,7 +48,7 @@ CRM.$(function ($) {
   });
   $('#cad_transit_number').blur(function(eventObj) {
     var myCount = onlyNumbers($(this));
-    if (myCount != 5) {
+    if ((myCount > 0) && (myCount != 5)) {
       $(this).crmError(ts('Your Bank Transit Number requires exactly five digits')); 
     }
     iatsSetBankIdenficationNumber();
diff --git a/civicrm/ext/iatspayments/phpunit.xml.dist b/civicrm/ext/iatspayments/phpunit.xml.dist
index 754565f4043a2c67445623df6e8adbb29c104272..2e4e326f14ea376ea9eebf26f40d77ce9343fa5c 100644
--- a/civicrm/ext/iatspayments/phpunit.xml.dist
+++ b/civicrm/ext/iatspayments/phpunit.xml.dist
@@ -1,5 +1,5 @@
 <?xml version="1.0"?>
-<phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/phpunit/bootstrap.php">
+<phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/phpunit/bootstrap.php" cacheResult="false">
   <testsuites>
     <testsuite name="iATS Test Suite">
       <directory>./tests/phpunit</directory>
diff --git a/civicrm/ext/iatspayments/release-notes/1.7.5.md b/civicrm/ext/iatspayments/release-notes/1.7.5.md
new file mode 100644
index 0000000000000000000000000000000000000000..1394b3ac4d96451685e9624aca17c99040a1b409
--- /dev/null
+++ b/civicrm/ext/iatspayments/release-notes/1.7.5.md
@@ -0,0 +1,27 @@
+# iATS CiviCRM Extension 1.7.5
+
+Mar 8, 2023
+
+This release is a maintenance release for the 1.7 series.
+
+It is recommended for all CiviCRM installs on 5.57+ and above. 
+
+This release covers a collection of accumulated issues and improvements since Mar 2021.
+
+The most important changes are in handling failures and in complex combinations with public selection of future recurring contributions.
+
+Here's a fuller list with thanks and credit to their authors:
+
+1. @mattwire: update use of parameter currencyID to currency
+2. Some unit test updates from @seamuslee001 and demeritcowboy
+3. Handle $0 amount transactions
+4. @yurg: updated iats response code labels
+5. @yurg: create an option to email an contribution invoice on recurring contribution failure
+6. @seamuslee001: filter out ipv6 addresses as client ip for FirstPay
+7. also prevent internal ips from getting sent as client ips (e.g. when using a front-end proxy).
+8. various fixes to how the "public selection of future recurring contribution dates" is implemented.
+9. @seamuslee001: update to recurring contribution flow to avoid multiple charges when other issues cause failures
+10. @shaneonabike: handle invalid doPayment requests that have is_recur but no recurring contribution id
+11. @demeritcowboy and @paulrooney php8 fixes
+12. @shaneonabike: better handling of ACH user input errors
+13. @demeritcowboy fix of contribution source field issue
diff --git a/civicrm/ext/iatspayments/tests/phpunit/CRM/Iats/BaseTestClass.php b/civicrm/ext/iatspayments/tests/phpunit/CRM/Iats/BaseTestClass.php
index 75a51e98898e02e273b1e22b7af97da0a90477ed..40c5523dabc84543b2bbb60662f58372ffba326d 100644
--- a/civicrm/ext/iatspayments/tests/phpunit/CRM/Iats/BaseTestClass.php
+++ b/civicrm/ext/iatspayments/tests/phpunit/CRM/Iats/BaseTestClass.php
@@ -17,7 +17,8 @@ use Civi\Test\TransactionalInterface;
  *       b. Disable TransactionalInterface, and handle all setup/teardown yourself.
  */
 abstract class BaseTestClass extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
-  //class BaseTestClass extends \PHPUnit_Framework_TestCase implements HeadlessInterface, HookInterface {
+
+  use \Civi\Test\Api3TestTrait;
 
   /**
    * Configure the headless environment.
@@ -30,167 +31,58 @@ abstract class BaseTestClass extends \PHPUnit\Framework\TestCase implements Head
       ->apply();
   }
 
-  private $_apiversion = 3;
-
   /**
-   * wrap api functions.
-   * so we can ensure they succeed & throw exceptions without litterering the test with checks
+   * Copied from civicrm core CiviUnitTestCase.
    *
-   * @param string $entity
-   * @param string $action
-   * @param array $params
-   * @param mixed $checkAgainst
-   *   Optional value to check result against, implemented for getvalue,.
-   *   getcount, getsingle. Note that for getvalue the type is checked rather than the value
-   *   for getsingle the array is compared against an array passed in - the id is not compared (for
-   *   better or worse )
+   * Instantiate form object.
    *
-   * @return array|int
-   */
-  public function callAPISuccess($entity, $action, $params, $checkAgainst = NULL) {
-    $params = array_merge(array(
-      'version' => $this->_apiversion,
-      'debug' => 1,
-    ),
-      $params
-    );
-    switch (strtolower($action)) {
-      case 'getvalue':
-        return $this->callAPISuccessGetValue($entity, $params, $checkAgainst);
-
-      case 'getsingle':
-        return $this->callAPISuccessGetSingle($entity, $params, $checkAgainst);
-
-      case 'getcount':
-        return $this->callAPISuccessGetCount($entity, $params, $checkAgainst);
-    }
-    $result = $this->civicrm_api($entity, $action, $params);
-    $this->assertAPISuccess($result, "Failure in api call for $entity $action");
-    return $result;
-  }
-  /**
-   * This function exists to wrap api getValue function & check the result
-   * so we can ensure they succeed & throw exceptions without litterering the test with checks
-   * There is a type check in this
+   * We need to instantiate the form to run preprocess, which means we have to trick it about the request method.
    *
-   * @param string $entity
-   * @param array $params
-   * @param string $type
-   *   Per http://php.net/manual/en/function.gettype.php possible types.
-   *   - boolean
-   *   - integer
-   *   - double
-   *   - string
-   *   - array
-   *   - object
+   * @param string $class
+   *   Name of form class.
    *
-   * @return array|int
-   */
-  public function callAPISuccessGetValue($entity, $params, $type = NULL) {
-    $params += array(
-      'version' => $this->_apiversion,
-      'debug' => 1,
-    );
-    $result = $this->civicrm_api($entity, 'getvalue', $params);
-    if ($type) {
-      if ($type == 'integer') {
-        // api seems to return integers as strings
-        $this->assertTrue(is_numeric($result), "expected a numeric value but got " . print_r($result, 1));
-      }
-      else {
-        $this->assertType($type, $result, "returned result should have been of type $type but was ");
-      }
-    }
-    return $result;
-  }
-  /**
-   * This function exists to wrap api getValue function & check the result
-   * so we can ensure they succeed & throw exceptions without litterering the test with checks
-   * There is a type check in this
-   * @param string $entity
-   * @param array $params
-   * @param null $count
-   * @throws Exception
-   * @return array|int
-   */
-  public function callAPISuccessGetCount($entity, $params, $count = NULL) {
-    $params += array(
-      'version' => $this->_apiversion,
-      'debug' => 1,
-    );
-    $result = $this->civicrm_api($entity, 'getcount', $params);
-    if (!is_int($result) || !empty($result['is_error']) || isset($result['values'])) {
-      throw new Exception('Invalid getcount result : ' . print_r($result, TRUE) . " type :" . gettype($result));
-    }
-    if (is_int($count)) {
-      $this->assertEquals($count, $result, "incorrect count returned from $entity getcount");
-    }
-    return $result;
-  }
-  /**
-   * This function exists to wrap api getsingle function & check the result
-   * so we can ensure they succeed & throw exceptions without litterering the test with checks
+   * @param array $formValues
    *
-   * @param string $entity
-   * @param array $params
-   * @param array $checkAgainst
-   *   Array to compare result against.
-   *   - boolean
-   *   - integer
-   *   - double
-   *   - string
-   *   - array
-   *   - object
+   * @param string $pageName
    *
-   * @throws Exception
-   * @return array|int
-   */
-  public function callAPISuccessGetSingle($entity, $params, $checkAgainst = NULL) {
-    $params += array(
-      'version' => $this->_apiversion,
-      'debug' => 1,
-    );
-    $result = $this->civicrm_api($entity, 'getsingle', $params);
-    if (!is_array($result) || !empty($result['is_error']) || isset($result['values'])) {
-      throw new Exception('Invalid getsingle result' . print_r($result, TRUE));
-    }
-    if ($checkAgainst) {
-      // @todo - have gone with the fn that unsets id? should we check id?
-      $this->checkArrayEquals($result, $checkAgainst);
-    }
-    return $result;
-  }
-  /**
-   * Check that api returned 'is_error' => 0.
+   * @param array $searchFormValues
+   *   Values for the search form if the form is a task eg.
+   *   for selected ids 6 & 8:
+   *   [
+   *      'radio_ts' => 'ts_sel',
+   *      'task' => CRM_Member_Task::PDF_LETTER,
+   *      'mark_x_6' => 1,
+   *      'mark_x_8' => 1,
+   *   ]
    *
-   * @param array $apiResult
-   *   Api result.
-   * @param string $prefix
-   *   Extra test to add to message.
+   * @return \CRM_Core_Form
    */
-  public function assertAPISuccess($apiResult, $prefix = '') {
-    if (!empty($prefix)) {
-      $prefix .= ': ';
+  public function getFormObject($class, $formValues = [], $pageName = '', $searchFormValues = []) {
+    $_POST = $formValues;
+    /* @var CRM_Core_Form $form */
+    $form = new $class();
+    $_SERVER['REQUEST_METHOD'] = 'GET';
+    switch ($class) {
+      case 'CRM_Event_Cart_Form_Checkout_Payment':
+      case 'CRM_Event_Cart_Form_Checkout_ParticipantsAndPrices':
+        $form->controller = new CRM_Event_Cart_Controller_Checkout();
+        break;
+
+      default:
+        $form->controller = new CRM_Core_Controller();
     }
-    $errorMessage = empty($apiResult['error_message']) ? '' : " " . $apiResult['error_message'];
-    if (!empty($apiResult['debug_information'])) {
-      $errorMessage .= "\n " . print_r($apiResult['debug_information'], TRUE);
+    if (!$pageName) {
+      $pageName = $form->getName();
     }
-    if (!empty($apiResult['trace'])) {
-      $errorMessage .= "\n" . print_r($apiResult['trace'], TRUE);
+    $form->controller->setStateMachine(new CRM_Core_StateMachine($form->controller));
+    $_SESSION['_' . $form->controller->_name . '_container']['values'][$pageName] = $formValues;
+    if ($searchFormValues) {
+      $_SESSION['_' . $form->controller->_name . '_container']['values']['Search'] = $searchFormValues;
     }
-    $this->assertEquals(0, $apiResult['is_error'], $prefix . $errorMessage);
-  }
-  /**
-   * A stub for the API interface. This can be overriden by subclasses to change how the API is called.
-   *
-   * @param $entity
-   * @param $action
-   * @param array $params
-   * @return array|int
-   */
-  public function civicrm_api($entity, $action, $params) {
-    return civicrm_api($entity, $action, $params);
+    if (isset($formValues['_qf_button_name'])) {
+      $_SESSION['_' . $form->controller->_name . '_container']['_qf_button_name'] = $formValues['_qf_button_name'];
+    }
+    return $form;
   }
 
 }
diff --git a/civicrm/ext/iatspayments/tests/phpunit/CRM/Iats/ContributionIATSTest.php b/civicrm/ext/iatspayments/tests/phpunit/CRM/Iats/ContributionIATSTest.php
index f81d2e57b2bbe13b9d3778895d24b6270fe71798..3811c329460e962f3bfadc81b4e2f8242969cf30 100644
--- a/civicrm/ext/iatspayments/tests/phpunit/CRM/Iats/ContributionIATSTest.php
+++ b/civicrm/ext/iatspayments/tests/phpunit/CRM/Iats/ContributionIATSTest.php
@@ -35,19 +35,19 @@ class CRM_Iats_ContributioniATSTest extends BaseTestClass {
       ->apply();
   }
 
-  public function setUp() {
+  public function setUp(): void {
     $this->_apiversion = 3;
     parent::setUp();
   }
 
-  public function tearDown() {
+  public function tearDown(): void {
     parent::tearDown();
   }
 
   /**
    * Test a Credit Card Contribution - one time iATS Credit Card - TEST41 - Backend
    */
-  public function testIATSCreditCardBackend() {
+  public function testIATSCreditCardBackend(): void {
 
     $params = array(
       'sequential' => 1,
@@ -64,14 +64,11 @@ class CRM_Iats_ContributioniATSTest extends BaseTestClass {
     $processor = $this->paymentProcessor->getPaymentProcessor();
     $this->paymentProcessorID = $processor['id'];
 
-    $form = new CRM_Contribute_Form_Contribution();
-    $form->_mode = 'Live';
-
     $contribution_params = array(
+      'soft_credit_contact_id' => array(),
       'total_amount' => 1.00,
       'financial_type_id' => 1,
-      'receive_date' => '08/03/2017',
-      'receive_date_time' => '11:59PM',
+      'receive_date' => date('Y-m-d H:i:s'),
       'contact_id' => $individual['id'],
       'payment_instrument_id' => 1,
       'contribution_status_id' => 1,
@@ -96,13 +93,16 @@ class CRM_Iats_ContributioniATSTest extends BaseTestClass {
       'hidden_AdditionalDetail' => 1,
       'hidden_Premium' => 1,
       'receipt_date' => '',
-      'receipt_date_time' => '',
+      'cancel_date' => '',
       'payment_processor_id' => $this->paymentProcessorID,
       'currency' => 'CAD',
       'source' => 'iATS CC TEST88',
     );
 
-    $form->testSubmit($contribution_params, CRM_Core_Action::ADD);
+    $form = $this->getFormObject('CRM_Contribute_Form_Contribution', $contribution_params);
+    $form->buildForm();
+    $form->_mode = 'Live';
+    $form->postProcess();
 
     $contribution = $this->callAPISuccessGetSingle('Contribution', array(
       'contact_id' => $individual['id'],
@@ -120,7 +120,7 @@ class CRM_Iats_ContributioniATSTest extends BaseTestClass {
   /**
    * Test a SWIPE Contribution - one time iATS SWIPE - TEST41 - Backend
    */
-  public function testIATSSWIPEBackend() {
+  public function testIATSSWIPEBackend(): void {
 
     $params = array(
       'sequential' => 1,
@@ -137,14 +137,11 @@ class CRM_Iats_ContributioniATSTest extends BaseTestClass {
     $processor = $this->paymentProcessor->getPaymentProcessor();
     $this->paymentProcessorID = $processor['id'];
 
-    $form = new CRM_Contribute_Form_Contribution();
-    $form->_mode = 'Live';
-
     $contribution_params = array(
+      'soft_credit_contact_id' => array(),
       'total_amount' => 2.00,
       'financial_type_id' => 1,
-      'receive_date' => '08/03/2017',
-      'receive_date_time' => '11:59PM',
+      'receive_date' => date('Y-m-d H:i:s'),
       'contact_id' => $individual['id'],
       'payment_instrument_id' => 1,
       'contribution_status_id' => 1,
@@ -170,13 +167,16 @@ class CRM_Iats_ContributioniATSTest extends BaseTestClass {
       'hidden_AdditionalDetail' => 1,
       'hidden_Premium' => 1,
       'receipt_date' => '',
-      'receipt_date_time' => '',
+      'cancel_date' => '',
       'payment_processor_id' => $this->paymentProcessorID,
       'currency' => 'CAD',
       'source' => 'iATS SWIPE TEST88',
     );
 
-    $form->testSubmit($contribution_params, CRM_Core_Action::ADD);
+    $form = $this->getFormObject('CRM_Contribute_Form_Contribution', $contribution_params);
+    $form->buildForm();
+    $form->_mode = 'Live';
+    $form->postProcess();
 
     $contribution = $this->callAPISuccessGetSingle('Contribution', array(
       'contact_id' => $individual['id'],
diff --git a/civicrm/ext/iatspayments/tests/phpunit/bootstrap.php b/civicrm/ext/iatspayments/tests/phpunit/bootstrap.php
index ddb9b30ac0cd861643864218c97c2c7e01ae8901..df1524daf1c0bca4c49adf8a6087a29ee8efe9e3 100644
--- a/civicrm/ext/iatspayments/tests/phpunit/bootstrap.php
+++ b/civicrm/ext/iatspayments/tests/phpunit/bootstrap.php
@@ -1,7 +1,6 @@
 <?php
 
 ini_set('memory_limit', '2G');
-ini_set('safe_mode', 0);
 eval(cv('php:boot --level=classloader', 'phpcode'));
 
 require_once __DIR__ . '/CRM/Iats/BaseTestClass.php';
diff --git a/civicrm/ext/legacycustomsearches/info.xml b/civicrm/ext/legacycustomsearches/info.xml
index 0b63aa514f4ef27df4df573df2c1058023a13338..fd05619ca0b3257a4dc6c00ef426f4c18838374f 100644
--- a/civicrm/ext/legacycustomsearches/info.xml
+++ b/civicrm/ext/legacycustomsearches/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2021-07-25</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <tags>
     <tag>mgmt:hidden</tag>
diff --git a/civicrm/ext/message_admin/info.xml b/civicrm/ext/message_admin/info.xml
index 6cbd34f5794ae7946dd74275c6c4828f94786d5f..27dd551427715bd944df6b93db4435f35bdc79bb 100644
--- a/civicrm/ext/message_admin/info.xml
+++ b/civicrm/ext/message_admin/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2021-06-12</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/oauth-client/info.xml b/civicrm/ext/oauth-client/info.xml
index 62afa975ce0f9bb4a9d8c71f76574937e8b53149..b5f07c6f097bce8fc81309f33926b73781e6f2d6 100644
--- a/civicrm/ext/oauth-client/info.xml
+++ b/civicrm/ext/oauth-client/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-10-23</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/payflowpro/info.xml b/civicrm/ext/payflowpro/info.xml
index b505fe1948634e995bdfd11e5e24eea2e6f09628..5aa54aaf0a21998f8c50debf2882057f003a4b3a 100644
--- a/civicrm/ext/payflowpro/info.xml
+++ b/civicrm/ext/payflowpro/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2021-04-13</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.59</ver>
diff --git a/civicrm/ext/recaptcha/info.xml b/civicrm/ext/recaptcha/info.xml
index 87b8b910cf9623b5b67f97f461d3e4a063490e92..7eb2eca5e111ffccecc78184e86f7fbbd5b22c60 100644
--- a/civicrm/ext/recaptcha/info.xml
+++ b/civicrm/ext/recaptcha/info.xml
@@ -13,7 +13,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2021-04-03</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/search_kit/info.xml b/civicrm/ext/search_kit/info.xml
index 100768b71ba461edce30b536c0421e39f4fe5b2f..ed8ca3afd2f53a9d2694a2651398d6f79fe50264 100644
--- a/civicrm/ext/search_kit/info.xml
+++ b/civicrm/ext/search_kit/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2021-01-06</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <develStage>stable</develStage>
   <tags>
     <tag>mgmt:required</tag>
diff --git a/civicrm/ext/sequentialcreditnotes/info.xml b/civicrm/ext/sequentialcreditnotes/info.xml
index 87067952f1ea4d3fd6b0ec60be3dedb18a41a18b..1fb3612d038f6e654d1b9d7fe42f94603080f402 100644
--- a/civicrm/ext/sequentialcreditnotes/info.xml
+++ b/civicrm/ext/sequentialcreditnotes/info.xml
@@ -15,7 +15,7 @@
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
   <releaseDate>2020-01-28</releaseDate>
-  <version>5.59.3</version>
+  <version>5.59.4</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/release-notes.md b/civicrm/release-notes.md
index f0703c8e4c531b87f25fdf16cfa936e6a69b37cd..7741436a04422056b77dddd1f31ee84a0276ead0 100644
--- a/civicrm/release-notes.md
+++ b/civicrm/release-notes.md
@@ -15,6 +15,15 @@ Other resources for identifying changes are:
     * https://github.com/civicrm/civicrm-joomla
     * https://github.com/civicrm/civicrm-wordpress
 
+## CiviCRM 5.59.4
+
+Released March 27, 2023
+
+- **[Synopsis](release-notes/5.59.4.md#synopsis)**
+- **[Bugs resolved](release-notes/5.59.4.md#bugs)**
+- **[Credits](release-notes/5.59.4.md#credits)**
+- **[Feedback](release-notes/5.59.4.md#feedback)**
+
 ## CiviCRM 5.59.3
 
 Released March 15, 2023
diff --git a/civicrm/release-notes/5.59.4.md b/civicrm/release-notes/5.59.4.md
new file mode 100644
index 0000000000000000000000000000000000000000..59272f37d8b8c08918f70007307945148dcd8c75
--- /dev/null
+++ b/civicrm/release-notes/5.59.4.md
@@ -0,0 +1,43 @@
+# CiviCRM 5.59.4
+
+Released March 27, 2023
+
+- **[Synopsis](#synopsis)**
+- **[Bugs resolved](#bugs)**
+- **[Credits](#credits)**
+- **[Feedback](#feedback)**
+
+## <a name="synopsis"></a>Synopsis
+
+| *Does this version...?*                                         |          |
+| --------------------------------------------------------------- | -------- |
+| Change the database schema?                                     | no       |
+| Alter the API?                                                  | no       |
+| Require attention to configuration options?                     | no       |
+| Fix problems installing or upgrading to a previous version?     | no       |
+| Introduce features?                                             | no       |
+| **Fix bugs?**                                                   | **yes**  |
+| Fix security vulnerabilities?                                   | no       |
+
+## <a name="bugs"></a>Bugs resolved
+
+* **_CiviCase_: "Delete Case" leads to "Network Error" ([dev/core#4190](https://lab.civicrm.org/dev/core/-/issues/4190): [#25849](https://github.com/civicrm/civicrm-core/pull/25849))**
+* **_CiviContribute_: Sorting by soft-credit name/date leads to AJAX error ([dev/core#4187](https://lab.civicrm.org/dev/core/-/issues/4187): [#25846](https://github.com/civicrm/civicrm-core/pull/25846))**
+* **_CiviContribute_: Confirmation screen displays extraneous debit fields ([dev/core#4189](https://lab.civicrm.org/dev/core/-/issues/4189): [#25910](https://github.com/civicrm/civicrm-core/pull/25910))**
+* **_CiviEvent_: Event created via API incorrectly marked as template ([dev/core#4205](https://lab.civicrm.org/dev/core/-/issues/4205): [#25932](https://github.com/civicrm/civicrm-core/pull/25932))**
+* **_Dedupe/Merge_: Harden against possibility of inconsistent value for `is_view` ([dev/core#4197](https://lab.civicrm.org/dev/core/-/issues/4197): [#25912](https://github.com/civicrm/civicrm-core/pull/25912))**
+
+## <a name="credits"></a>Credits
+
+This release was developed by the following authors and reviewers:
+
+Wikimedia Foundation - Eileen McNaughton; Romy Ebert; MJW Consulting - Matthew Wire;
+Lemniscus - Noah Miller; Korlon - Stuart Gaston; JMA Consulting - Seamus Lee; Coop
+SymbioTIC - Mathieu Lutfy; composerjk; CiviCRM - Tim Otten; Black Brick Software - David
+Hayes; Benjamin W; Andreas Howiller
+
+## <a name="feedback"></a>Feedback
+
+These release notes are edited by Tim Otten and Andie Hunt.  If you'd like to
+provide feedback on them, please login to https://chat.civicrm.org/civicrm and
+contact `@agh1`.
diff --git a/civicrm/sql/civicrm_data.mysql b/civicrm/sql/civicrm_data.mysql
index 88867fe8178247e2cb23a34aa81424a280b7d512..ee8acfbe9fda97794348fe32759d9cac56e6be5d 100644
--- a/civicrm/sql/civicrm_data.mysql
+++ b/civicrm/sql/civicrm_data.mysql
@@ -23678,4 +23678,4 @@ INSERT INTO `civicrm_report_instance`
     ( `domain_id`, `title`, `report_id`, `description`, `permission`, `form_values`)
 VALUES
     (  @domainID, 'Survey Details', 'survey/detail', 'Detailed report for canvassing, phone-banking, walk lists or other surveys.', 'access CiviReport', 'a:39:{s:6:"fields";a:2:{s:9:"sort_name";s:1:"1";s:6:"result";s:1:"1";}s:22:"assignee_contact_id_op";s:2:"eq";s:25:"assignee_contact_id_value";s:0:"";s:12:"sort_name_op";s:3:"has";s:15:"sort_name_value";s:0:"";s:17:"street_number_min";s:0:"";s:17:"street_number_max";s:0:"";s:16:"street_number_op";s:3:"lte";s:19:"street_number_value";s:0:"";s:14:"street_name_op";s:3:"has";s:17:"street_name_value";s:0:"";s:15:"postal_code_min";s:0:"";s:15:"postal_code_max";s:0:"";s:14:"postal_code_op";s:3:"lte";s:17:"postal_code_value";s:0:"";s:7:"city_op";s:3:"has";s:10:"city_value";s:0:"";s:20:"state_province_id_op";s:2:"in";s:23:"state_province_id_value";a:0:{}s:13:"country_id_op";s:2:"in";s:16:"country_id_value";a:0:{}s:12:"survey_id_op";s:2:"in";s:15:"survey_id_value";a:0:{}s:12:"status_id_op";s:2:"eq";s:15:"status_id_value";s:1:"1";s:11:"custom_1_op";s:2:"in";s:14:"custom_1_value";a:0:{}s:11:"custom_2_op";s:2:"in";s:14:"custom_2_value";a:0:{}s:17:"custom_3_relative";s:1:"0";s:13:"custom_3_from";s:0:"";s:11:"custom_3_to";s:0:"";s:11:"description";s:75:"Detailed report for canvassing, phone-banking, walk lists or other surveys.";s:13:"email_subject";s:0:"";s:8:"email_to";s:0:"";s:8:"email_cc";s:0:"";s:10:"permission";s:17:"access CiviReport";s:6:"groups";s:0:"";s:9:"domain_id";i:1;}');
-UPDATE civicrm_domain SET version = '5.59.3';
+UPDATE civicrm_domain SET version = '5.59.4';
diff --git a/civicrm/sql/civicrm_generated.mysql b/civicrm/sql/civicrm_generated.mysql
index 0858513092d129e31e0f36b0e82c57d4a159a02c..d747b24a9b5977266410e4e7c33584d364b9d60b 100644
--- a/civicrm/sql/civicrm_generated.mysql
+++ b/civicrm/sql/civicrm_generated.mysql
@@ -3055,7 +3055,7 @@ UNLOCK TABLES;
 LOCK TABLES `civicrm_domain` WRITE;
 /*!40000 ALTER TABLE `civicrm_domain` DISABLE KEYS */;
 INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `version`, `contact_id`, `locales`, `locale_custom_strings`) VALUES
- (1,'Default Domain Name',NULL,'5.59.3',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}');
+ (1,'Default Domain Name',NULL,'5.59.4',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}');
 /*!40000 ALTER TABLE `civicrm_domain` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/civicrm/templates/CRM/Contribute/Form/Contribution/Confirm.tpl b/civicrm/templates/CRM/Contribute/Form/Contribution/Confirm.tpl
index 9ef4ad93d078e29edf9e708da1eaa0f804693aff..bff0c22270dc092165bb4b692ff9551486f55168 100644
--- a/civicrm/templates/CRM/Contribute/Form/Contribution/Confirm.tpl
+++ b/civicrm/templates/CRM/Contribute/Form/Contribution/Confirm.tpl
@@ -243,25 +243,25 @@
             {$paymentFieldsetLabel}
           </div>
         {/if}
-        {if in_array('bank_account_number', $form)}
+        {if in_array('bank_account_number', $form) && $bank_account_number}
           <div class="display-block">
             {ts}Account Holder{/ts}: {$account_holder}<br/>
             {ts}Bank Account Number{/ts}: {$bank_account_number}<br/>
             {ts}Bank Identification Number{/ts}: {$bank_identification_number}<br/>
             {ts}Bank Name{/ts}: {$bank_name}<br/>
           </div>
-          {if $contributeMode eq 'direct'}
+          {if $paymentAgreementText}
             <div class="crm-group debit_agreement-group">
               <div class="header-dark">
-                {ts}Agreement{/ts}
+                {$paymentAgreementTitle}
               </div>
               <div class="display-block">
-                {ts}Your account data will be used to charge your bank account via direct debit. While submitting this form you agree to the charging of your bank account via direct debit.{/ts}
+                {$paymentAgreementText}
               </div>
             </div>
           {/if}
         {/if}
-        {if in_array('credit_card_number', $form)}
+        {if in_array('credit_card_number', $form) && $credit_card_number}
           <div class="crm-section no-label credit_card_details-section">
             <div class="content">{$credit_card_type}</div>
             <div class="content">{$credit_card_number}</div>
diff --git a/civicrm/templates/CRM/Contribute/Form/Contribution/ThankYou.tpl b/civicrm/templates/CRM/Contribute/Form/Contribution/ThankYou.tpl
index f119e135ca3c04793cb5f02ae8800a806ddfd330..697baf22d8b672e79bed51dd9c8116bdd8e2ba36 100644
--- a/civicrm/templates/CRM/Contribute/Form/Contribution/ThankYou.tpl
+++ b/civicrm/templates/CRM/Contribute/Form/Contribution/ThankYou.tpl
@@ -281,7 +281,7 @@
     {/if}
   {/if}
 
-  {if $contributeMode eq 'direct' and ! $is_pay_later and $is_monetary and ( $amount GT 0 OR $minimum_fee GT 0 )}
+  {if in_array('credit_card_number', $form) || in_array('bank_account_number', $form) && ($amount GT 0 OR $minimum_fee GT 0)}
     {crmRegion name="contribution-thankyou-billing-block"}
       <div class="crm-group credit_card-group">
         {if $paymentFieldsetLabel}
@@ -289,7 +289,7 @@
             {$paymentFieldsetLabel}
           </div>
         {/if}
-        {if $paymentProcessor.payment_type == 2}
+          {if in_array('bank_account_number', $form) && $bank_account_number}
           <div class="display-block">
             {ts}Account Holder{/ts}: {$account_holder}<br />
             {ts}Bank Identification Number{/ts}: {$bank_identification_number}<br />
diff --git a/civicrm/vendor/autoload.php b/civicrm/vendor/autoload.php
index c5905e4e4bef2c25f3fa9e15253a50ab59732337..e35fae40b4fc0375a873d5446ae556aee9ee7c29 100644
--- a/civicrm/vendor/autoload.php
+++ b/civicrm/vendor/autoload.php
@@ -4,4 +4,4 @@
 
 require_once __DIR__ . '/composer/autoload_real.php';
 
-return ComposerAutoloaderInit5b9fa8178f0547c9453906e5e95835fb::getLoader();
+return ComposerAutoloaderInit85641ecafad0fd91a9bd9d9f47db5e32::getLoader();
diff --git a/civicrm/vendor/composer/autoload_real.php b/civicrm/vendor/composer/autoload_real.php
index 5fa4f9c48083a7ac027e0923b91a58b75322dc15..1bd06617e6a5ad25e360d0d50af19bbd7e098155 100644
--- a/civicrm/vendor/composer/autoload_real.php
+++ b/civicrm/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@
 
 // autoload_real.php @generated by Composer
 
-class ComposerAutoloaderInit5b9fa8178f0547c9453906e5e95835fb
+class ComposerAutoloaderInit85641ecafad0fd91a9bd9d9f47db5e32
 {
     private static $loader;
 
@@ -24,9 +24,9 @@ class ComposerAutoloaderInit5b9fa8178f0547c9453906e5e95835fb
 
         require __DIR__ . '/platform_check.php';
 
-        spl_autoload_register(array('ComposerAutoloaderInit5b9fa8178f0547c9453906e5e95835fb', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInit85641ecafad0fd91a9bd9d9f47db5e32', 'loadClassLoader'), true, true);
         self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
-        spl_autoload_unregister(array('ComposerAutoloaderInit5b9fa8178f0547c9453906e5e95835fb', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInit85641ecafad0fd91a9bd9d9f47db5e32', 'loadClassLoader'));
 
         $includePaths = require __DIR__ . '/include_paths.php';
         $includePaths[] = get_include_path();
@@ -36,7 +36,7 @@ class ComposerAutoloaderInit5b9fa8178f0547c9453906e5e95835fb
         if ($useStaticLoader) {
             require __DIR__ . '/autoload_static.php';
 
-            call_user_func(\Composer\Autoload\ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb::getInitializer($loader));
+            call_user_func(\Composer\Autoload\ComposerStaticInit85641ecafad0fd91a9bd9d9f47db5e32::getInitializer($loader));
         } else {
             $map = require __DIR__ . '/autoload_namespaces.php';
             foreach ($map as $namespace => $path) {
@@ -57,12 +57,12 @@ class ComposerAutoloaderInit5b9fa8178f0547c9453906e5e95835fb
         $loader->register(true);
 
         if ($useStaticLoader) {
-            $includeFiles = Composer\Autoload\ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb::$files;
+            $includeFiles = Composer\Autoload\ComposerStaticInit85641ecafad0fd91a9bd9d9f47db5e32::$files;
         } else {
             $includeFiles = require __DIR__ . '/autoload_files.php';
         }
         foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequire5b9fa8178f0547c9453906e5e95835fb($fileIdentifier, $file);
+            composerRequire85641ecafad0fd91a9bd9d9f47db5e32($fileIdentifier, $file);
         }
 
         return $loader;
@@ -74,7 +74,7 @@ class ComposerAutoloaderInit5b9fa8178f0547c9453906e5e95835fb
  * @param string $file
  * @return void
  */
-function composerRequire5b9fa8178f0547c9453906e5e95835fb($fileIdentifier, $file)
+function composerRequire85641ecafad0fd91a9bd9d9f47db5e32($fileIdentifier, $file)
 {
     if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
         $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
diff --git a/civicrm/vendor/composer/autoload_static.php b/civicrm/vendor/composer/autoload_static.php
index 1663548a41e0b3c0339fcac7dd847dbc87546903..a58958a7322f3696c91fd4fec1c905d006fe7b90 100644
--- a/civicrm/vendor/composer/autoload_static.php
+++ b/civicrm/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@
 
 namespace Composer\Autoload;
 
-class ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb
+class ComposerStaticInit85641ecafad0fd91a9bd9d9f47db5e32
 {
     public static $files = array (
         'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
@@ -744,11 +744,11 @@ class ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb::$prefixDirsPsr4;
-            $loader->prefixesPsr0 = ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb::$prefixesPsr0;
-            $loader->fallbackDirsPsr0 = ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb::$fallbackDirsPsr0;
-            $loader->classMap = ComposerStaticInit5b9fa8178f0547c9453906e5e95835fb::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInit85641ecafad0fd91a9bd9d9f47db5e32::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit85641ecafad0fd91a9bd9d9f47db5e32::$prefixDirsPsr4;
+            $loader->prefixesPsr0 = ComposerStaticInit85641ecafad0fd91a9bd9d9f47db5e32::$prefixesPsr0;
+            $loader->fallbackDirsPsr0 = ComposerStaticInit85641ecafad0fd91a9bd9d9f47db5e32::$fallbackDirsPsr0;
+            $loader->classMap = ComposerStaticInit85641ecafad0fd91a9bd9d9f47db5e32::$classMap;
 
         }, null, ClassLoader::class);
     }
diff --git a/civicrm/vendor/composer/installed.php b/civicrm/vendor/composer/installed.php
index c981e51d1c538c8041ec9eec5b4e545f9687a790..550f889dcf0c5b42e45a2f4d09471860505a76c5 100644
--- a/civicrm/vendor/composer/installed.php
+++ b/civicrm/vendor/composer/installed.php
@@ -5,7 +5,7 @@
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
-        'reference' => 'a470f36404babb775566c3ace40a84f8b05be44e',
+        'reference' => '5a40b8aac7a5b0cd7b8f432c31deae79c454b9be',
         'name' => 'civicrm/civicrm-core',
         'dev' => true,
     ),
@@ -61,7 +61,7 @@
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
-            'reference' => 'a470f36404babb775566c3ace40a84f8b05be44e',
+            'reference' => '5a40b8aac7a5b0cd7b8f432c31deae79c454b9be',
             'dev_requirement' => false,
         ),
         'civicrm/civicrm-cxn-rpc' => array(
diff --git a/civicrm/xml/version.xml b/civicrm/xml/version.xml
index 353b88311691883b9c7e60a1a5d2bf3260e5fe7d..22e99f76801b9e685ec9391fcacec34d8e8c53cf 100644
--- a/civicrm/xml/version.xml
+++ b/civicrm/xml/version.xml
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="iso-8859-1" ?>
 <version>
-  <version_no>5.59.3</version_no>
+  <version_no>5.59.4</version_no>
 </version>