diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php
index fa6a812d17c000209ae877c8e10406190a6794f3..f386a57ef278bc492145b4802b1f988273bfe0ab 100644
--- a/tests/phpunit/CRM/Stripe/IpnTest.php
+++ b/tests/phpunit/CRM/Stripe/IpnTest.php
@@ -54,191 +54,6 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
       ->apply($force);
   }
 
-  /**
-   * DRY code. Sets up the database as it would be after a recurring contrib
-   * has been set up with Stripe.
-   *
-   * Results in a ContributionRecur plus a pending Contribution record.
-   *
-   * The following mock Stripe IDs strings are used:
-   *
-   * - pm_mock   PaymentMethod
-   * - pi_mock   PaymentIntent
-   * - cus_mock  Customer
-   * - ch_mock   Charge
-   * - txn_mock  Balance transaction
-   * - sub_mock  Subscription
-   */
-  protected function mockRecurringPaymentSetup() {
-    PropertySpy::$buffer = 'none';
-    // Set this to 'print' or 'log' maybe more helpful in debugging but for
-    // generally running tests 'exception' suits as we don't expect any output.
-    PropertySpy::$outputMode = 'exception';
-
-    $this->assertInstanceOf('CRM_Core_Payment_Stripe', $this->paymentObject);
-
-    // Create a mock stripe client.
-    $stripeClient = $this->createMock('Stripe\\StripeClient');
-    // Update our CRM_Core_Payment_Stripe object and ensure any others
-    // instantiated separately will also use it.
-    $this->paymentObject->setMockStripeClient($stripeClient);
-
-    // Mock the payment methods service.
-    $mockPaymentMethod = $this->createMock('Stripe\\PaymentMethod');
-    $mockPaymentMethod->method('__get')
-                      ->will($this->returnValueMap([
-                        [ 'id', 'pm_mock']
-                      ]));
-    $stripeClient->paymentMethods = $this->createMock('Stripe\\Service\\PaymentMethodService');
-    $stripeClient->paymentMethods
-                 ->method('create')
-                 ->willReturn($mockPaymentMethod);
-    $stripeClient->paymentMethods
-                 ->expects($this->atLeastOnce())
-                 ->method('retrieve')
-                 ->with($this->equalTo('pm_mock'))
-                 ->willReturn($mockPaymentMethod);
-
-    // Mock the Customers service
-    $stripeClient->customers = $this->createMock('Stripe\\Service\\CustomerService');
-    $stripeClient->customers
-                 ->method('create')
-                 ->willReturn(
-                     new PropertySpy('customers.create', ['id' => 'cus_mock'])
-                 );
-    $stripeClient->customers
-                 ->method('retrieve')
-                 ->with($this->equalTo('cus_mock'))
-                 ->willReturn(
-                     new PropertySpy('customers.retrieve', ['id' => 'cus_mock'])
-                 );
-
-    $mockPlan = $this->createMock('Stripe\\Plan');
-    $mockPlan
-      ->method('__get')
-      ->will($this->returnValueMap([
-        ['id', 'every-1-month-' . ($this->total * 100) . '-usd-test']
-      ]));
-    $stripeClient->plans = $this->createMock('Stripe\\Service\\PlanService');
-    $stripeClient->plans
-      ->method('retrieve')
-      ->willReturn($mockPlan);
-
-
-    // Need a mock intent with id and status
-    $mockCharge = $this->createMock('Stripe\\Charge');
-    $mockCharge
-      ->method('__get')
-      ->will($this->returnValueMap([
-        ['id', 'ch_mock'],
-        ['captured', TRUE],
-        ['status', 'succeeded'],
-        ['balance_transaction', 'txn_mock'],
-      ]));
-    $mockPaymentIntent = $this->createMock('Stripe\\PaymentIntent');
-    $mockPaymentIntent
-      ->method('__get')
-      ->will($this->returnValueMap([
-        ['id', 'pi_mock'],
-        ['status', 'succeeded'],
-        ['charges', (object) ['data' => [ $mockCharge ]]]
-      ]));
-
-    $mockSubscription = new PropertySpy('subscription.create', [
-          'id' => 'sub_mock',
-          'current_period_end' => time()+60*60*24,
-          'latest_invoice' => [
-            'id' => 'in_mock',
-            'payment_intent' => $mockPaymentIntent,
-          ],
-          'pending_setup_intent' => '',
-        ]);
-    $stripeClient->subscriptions = $this->createMock('Stripe\\Service\\SubscriptionService');
-    $stripeClient->subscriptions
-        ->method('create')
-        ->willReturn($mockSubscription);
-    $stripeClient->subscriptions
-        ->method('retrieve')
-        ->with($this->equalTo('sub_mock'))
-        ->willReturn($mockSubscription);
-
-    $stripeClient->balanceTransactions = $this->createMock('Stripe\\Service\\BalanceTransactionService');
-    $stripeClient->balanceTransactions
-    ->method('retrieve')
-    ->with($this->equalTo('txn_mock'))
-    ->willReturn(new PropertySpy('balanceTransaction', [
-      'id' => 'txn_mock',
-      'fee' => 1190, /* means $11.90 */
-      'currency' => 'usd',
-      'exchange_rate' => NULL,
-      'object' => 'balance_transaction',
-    ]));
-
-    // $stripeClient->paymentIntents = $this->createMock('Stripe\\Service\\PaymentIntentService');
-    // todo change the status from requires_capture to ?
-    //$stripeClient->paymentIntents ->method('update') ->willReturn();
-
-    $mockInvoice = new PropertySpy('Invoice', [
-        'amount_due' => $this->total*100,
-        'charge_id' => 'ch_mock', //xxx
-        'created' => time(),
-        'currency' => 'usd',
-        'customer' => 'cus_mock',
-        'id' => 'in_mock',
-        'object' => 'invoice',
-        'subscription' => 'sub_mock',
-      ]);
-    $stripeClient->invoices = $this->createMock('Stripe\\Service\\InvoiceService');
-    $stripeClient->invoices
-                 ->expects($this->never())
-                 ->method($this->anything())
-               ;
-    /*
-      ->method('all')
-      ->willReturn(['data' => $mockInvoice]);
-     */
-
-    $stripeClient->charges = $this->createMock('Stripe\\Service\\ChargeService');
-    $stripeClient->charges
-                 ->method('retrieve')
-                 ->with($this->equalTo('ch_mock'))
-                 ->willReturn($mockCharge);
-
-    // Setup a recurring contribution for $this->total per month.
-    $this->setupRecurringTransaction();
-
-    // Submit the payment.
-    $payment_extra_params = [
-      'is_recur'            => 1,
-      'contributionRecurID' => $this->contributionRecurID,
-      'contributionID'      => $this->contributionID,
-      'frequency_unit'      => $this->frequency_unit,
-      'frequency_interval'  => $this->frequency_interval,
-      'installments'        => $this->installments,
-    ];
-    $this->doPayment($payment_extra_params);
-
-    //
-    // Check the Contribution
-    // ...should be pending
-    // ...its transaction ID should be our Invoice ID.
-    //
-    $this->checkContrib([
-      'contribution_status_id' => 'Pending',
-      'trxn_id'                => 'in_mock',
-    ]);
-
-    //
-    // Check the CotnributionRecur
-    //
-    // The subscription ID should be in both processor_id and trxn_id fields
-    // We expect it to be pending
-    $this->checkContribRecur([
-      'contribution_status_id' => 'Pending',
-      'trxn_id'                => 'sub_mock',
-      'processor_id'           => 'sub_mock',
-    ]);
-  }
   /**
    * Test creating a recurring contribution and
    * update it after creation. @todo The membership should also be updated.
@@ -262,6 +77,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
           'charge'       => 'ch_mock',
           'created'      => time(),
           'amount_due'   => $this->total*100,
+          'status'      => 'paid',
         ]
       ],
     ]);
@@ -427,14 +243,8 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
     $this->checkContribRecur([ 'contribution_status_id' => 'In Progress' ]);
 
     // We should have a new contribution.
-    $contributions = civicrm_api3('Contribution', 'get', [
-      'contribution_recur_id' => $this->contributionRecurID,
-      'is_test'               => 1,
-      'options'               => ['sort' => 'id'],
-      'sequential'            => 1,
-    ]);
-    $this->assertEquals(2, $contributions['count']);
-    $contrib2 = $contributions['values'][1];
+    $contributions = $this->getContributionsAndAssertCount(2);
+    $contrib2 = $contributions[1];
     $this->checkContrib([
       'contribution_status_id' => 'Pending',
       'trxn_id'                => 'in_mock_2',
@@ -457,6 +267,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
           'charge'       => 'ch_mock_2',
           'created'      => time(),
           'amount_due'   => $this->total*100,
+          'status'      => 'paid',
         ]
       ],
     ], TRUE);
@@ -475,8 +286,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
    * - Creates invoice.payment_succeeded: which should complete the Contribution
    *   and should update its trxn_id, appending the new charge ID
    *
-   * - Creates invoice.finalized event: this should create a Pending
-   *   Contribution with its trxn_id set to the new invoice ID.
+   * - Creates invoice.finalized event: this should basically not do anything.
    *
    *
    */
@@ -503,19 +313,14 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
           'charge'       => 'ch_mock_2',
           'created'      => time(),
           'amount_due'   => $this->total*100,
+          'status'      => 'paid',
         ]
       ],
     ], TRUE);
 
     // We should have a new, Completed contribution.
-    $contributions = civicrm_api3('Contribution', 'get', [
-      'contribution_recur_id' => $this->contributionRecurID,
-      'is_test'               => 1,
-      'options'               => ['sort' => 'id'],
-      'sequential'            => 1,
-    ]);
-    $this->assertEquals(2, $contributions['count']);
-    $contrib2 = $contributions['values'][1];
+    $contributions = $this->getContributionsAndAssertCount(2);
+    $contrib2 = $contributions[1];
     // Check the new contribution
     $this->checkContrib([
       'contribution_status_id' => 'Completed',
@@ -539,51 +344,55 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
     $this->checkContribRecur([ 'contribution_status_id' => 'In Progress' ]);
 
     // We should still have just 2 contribs.
-    $contributions = civicrm_api3('Contribution', 'get', [
-      'contribution_recur_id' => $this->contributionRecurID,
-      'is_test'               => 1,
-      'options'               => ['sort' => 'id'],
-      'sequential'            => 1,
-    ]);
-    $this->assertEquals(2, $contributions['count']);
+    $contributions = $this->getContributionsAndAssertCount(2);
 
     // Our 2nd contribution should still be Completed and have the same trxn_id
-    $contrib2 = $contributions['values'][1];
+    $contrib2 = $contributions[1];
     $this->checkContrib([
       'contribution_status_id' => 'Completed',
       'trxn_id'                => 'in_mock_2,ch_mock_2',
-    ], (int) $contrib2['id']);
+    ], $contrib2);
 
   }
   /**
-   * DRY code
+   * Tests situation when the initial recurring payment came in OK,
+   * but the next one fails.
+   *
+   * 1. Repeats basic test on first successful payment to get the database set up.
+   *
+   * 2. Simulates invoice.finalized. This is the normal case and creates a Pending Contribution.
+   *
+   * 3. Simulates invoice.payment_failed: this time the Contribution created in
+   *    (2) should be updated to Failed.
+   *
    */
-  protected function getMocksForRecurringInvoiceFinalized() :array {
+  public function testRecurringInvoicePaymentFailedThenSucceeds() {
 
-    // Now a new one comes in...
-    // invoice.finalized
-    $common = [
+    // Initial payment comes in...
+    $this->testNewRecurringInvoicePaymentSucceeded();
+
+    //
+    // Now test if we get invoice.finalized first.
+    //
+    // To do this we'll need a new invoice and a new charge.
+    // and pending balance transaction
+    $mockInvoice2 = new PropertySpy('invoice2', [
+          'id'           => 'in_mock_2',
+          'object'       => 'invoice',
+          'amount_due'   => $this->total*100,
+          'charge'       => 'ch_mock_2',
           'subscription' => 'sub_mock',
           'customer'     => 'cus_mock',
           'created'      => time(),
-    ];
-    $mockCharge1 = new PropertySpy('charge1', $common + [
-          'id'                  => 'ch_mock',
-          'object'              => 'charge',
-          'balance_transaction' => 'txn_mock',
-          'amount'              => $this->total*100,
         ]);
-    $mockCharge2 = new PropertySpy('charge2', $common + [
+    $mockCharge2 = new PropertySpy('charge2', [
           'id'                  => 'ch_mock_2',
           'object'              => 'charge',
           'balance_transaction' => 'txn_mock_2',
           'amount'              => $this->total*100,
-        ]);
-    $mockInvoice2 = new PropertySpy('invoice2', $common + [
-          'id'           => 'in_mock_2',
-          'object'       => 'invoice',
-          'amount_due'   => $this->total*100,
-          'charge'       => 'ch_mock_2',
+          'subscription' => 'sub_mock',
+          'customer'     => 'cus_mock',
+          'created'      => time(),
         ]);
     $balanceTransaction2 = new PropertySpy('balance_transaction2', [
       'id'            => 'txn_mock_2',
@@ -593,10 +402,9 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
       'currency'      => 'usd',
       'exchange_rate' => NULL,
       'fee'           => 1190, /* means $11.90 */
-      'status'        => 'available',
+      'status'        => 'pending',
       'type'          => 'charge',
     ]);
-
     $this->paymentObject->stripeClient->balanceTransactions = $this->createMock('Stripe\\Service\\BalanceTransactionService');
     $this->paymentObject->stripeClient->balanceTransactions
       ->method('retrieve')
@@ -607,44 +415,190 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
     $this->paymentObject->stripeClient->charges
       ->method('retrieve')
       ->will($this->returnValueMapOrDie([
-        ['ch_mock', NULL, NULL, $mockCharge1],
         ['ch_mock_2', NULL, NULL, $mockCharge2],
       ]));
 
-    return [$mockCharge1, $mockCharge2, $mockInvoice2, $balanceTransaction2];
-  }
-  /**
-   * Simulate an event being sent from Stripe and processed by our IPN code.
-   *
-   * @var array|Stripe\Event|PropertySpy|mock $eventData
-   *
-   * @return bool result from ipn()
-   */
-  protected function simulateEvent($eventData, $exceptionOnFailure=FALSE) {
-
-    // Mock Event service.
-    $stripeClient = $this->paymentObject->stripeClient;
-    $stripeClient->events = $this->createMock('Stripe\\Service\\EventService');
 
-    $mockEvent = PropertySpy::fromMixed('simulate ' . $eventData['type'], $eventData);
-    $stripeClient->events
-                 ->method('all')
-                 ->willReturn(new PropertySpy('events.all', [ 'data' => [ $mockEvent ] ]));
-    $stripeClient->events
-                 ->expects($this->atLeastOnce())
-                 ->method('retrieve')
-                 ->with($this->equalTo($eventData['id']))
-                 ->willReturn(new PropertySpy('events.retrieve', $mockEvent));
+    //
+    // Simulate invoice.finalized
+    //
+    $this->simulateEvent([
+      'type'             => 'invoice.finalized',
+      'id'               => 'evt_mock_3',
+      'object'           => 'event',
+      'livemode'         => false,
+      'pending_webhooks' => 0,
+      'request'          => [ 'id' => NULL ],
+      'data'             => [
+        'object' => $mockInvoice2
+      ],
+    ], TRUE);
+
+    // Recur should still be In Progress.
+    $this->checkContribRecur([ 'contribution_status_id' => 'In Progress' ]);
+
+    // We should have 2 contribs.
+    // The 2nd one should be Pending.
+    $contributions = $this->getContributionsAndAssertCount(2);
+    $this->checkContrib([
+      'contribution_status_id' => 'Pending',
+      'trxn_id' => 'in_mock_2',
+    ], $contributions[1]);
+
+    //
+    // Now simulate a failed invoice again. (normal flow for a failed invoice)
+    //
+    $this->simulateEvent([
+      'id'               => 'evt_mock_4',
+      'object'           => 'event',
+      'type'             => 'invoice.payment_failed',
+      'livemode'         => false,
+      'pending_webhooks' => 0,
+      'request'          => [ 'id' => NULL ],
+      'data'             => [
+        'object' => [
+          'id'                  => 'in_mock_2',
+          'object'              => 'invoice',
+          'charge'              => 'ch_mock_2',
+          'amount_due'          => $this->total*100,
+          'amount_paid'         => 0,
+          'customer'            => 'cus_mock',
+          'created'             => time(),
+          'status'              => 'uncollectible'
+        ]
+      ],
+    ]);
+    // The 2nd contribution should be Failed
+    $contributions = $this->getContributionsAndAssertCount(2);
+    $this->checkContrib([
+      'contribution_status_id' => 'Failed',
+      'trxn_id' => 'in_mock_2',
+    ], $contributions[1]);
+
+    //
+    // Now simulate an invoice.payment_succeeded - e.g. a 2nd charge attempt worked.
+    //
+    $mockCharge3 = new PropertySpy('charge3', [
+          'id'                  => 'ch_mock_3',
+          'object'              => 'charge',
+          'balance_transaction' => 'txn_mock_3',
+          'amount'              => $this->total*100,
+          'subscription'        => 'sub_mock',
+          'customer'            => 'cus_mock',
+          'created'             => time(),
+        ]);
+    $balanceTransaction2 = new PropertySpy('balance_transaction3', [
+      'id'            => 'txn_mock_3',
+      'object'        => 'balance_transaction',
+      'amount'        => $this->total * 100,
+      'created'       => time(),
+      'currency'      => 'usd',
+      'exchange_rate' => NULL,
+      'fee'           => 1190, /* means $11.90 */
+      'status'        => 'available',
+      'type'          => 'charge',
+    ]);
+    $this->paymentObject->stripeClient->balanceTransactions = $this->createMock('Stripe\\Service\\BalanceTransactionService');
+    $this->paymentObject->stripeClient->balanceTransactions
+      ->method('retrieve')
+      ->with($this->equalTo('txn_mock_3'))
+      ->willReturn($balanceTransaction2);
+
+    $this->paymentObject->stripeClient->charges = $this->createMock('Stripe\\Service\\ChargeService');
+    $this->paymentObject->stripeClient->charges
+      ->method('retrieve')
+      ->will($this->returnValueMapOrDie([
+        ['ch_mock_3', NULL, NULL, $mockCharge3],
+      ]));
+
+    $this->simulateEvent([
+      'id'               => 'evt_mock_5',
+      'object'           => 'event',
+      'type'             => 'invoice.payment_succeeded',
+      'livemode'         => false,
+      'pending_webhooks' => 0,
+      'request'          => [ 'id' => NULL ],
+      'data'             => [
+        'object' => [
+          'id'          => 'in_mock_2', // still same invoice
+          'object'      => 'invoice',
+          'charge'      => 'ch_mock_3', // different charge
+          'amount_due'  => $this->total*100,
+          'amount_paid' => 0,
+          'customer'    => 'cus_mock',
+          'created'     => time(),
+          'status'      => 'paid',
+        ]
+      ],
+    ]);
+    // The 2nd contribution should now be Completed and have the invoice and the successful charge as its trxn_id
+    $contributions = $this->getContributionsAndAssertCount(2);
+    $this->checkContrib([
+      'contribution_status_id' => 'Completed',
+      'trxn_id' => 'in_mock_2,ch_mock_3',
+    ], $contributions[1]);
+
+
 
-    // Fetch the event
-    // Previously used the following - but see docblock of getEvent()
-    // $event = $this->getEvent($eventData['type']);
-    // $this->assertNotEmpty($event, "Failed to fetch event type $eventData[type]");
 
-    // Process it with the IPN/webhook
-    return $this->ipn($mockEvent, TRUE, $exceptionOnFailure);
   }
+  /**
+   * Tests situation when the initial recurring payment came in OK,
+   * but the subscription is then deleted.
+   */
+  public function testRecurringDeletedAfterInitialSuccess() {
+
+    // Initial payment comes in...
+    $this->testNewRecurringInvoicePaymentSucceeded();
 
+    $cancelTimestamp = time();
+    $mockSubscription = new PropertySpy('subscription', [
+      'id'                 => 'sub_mock',
+      'object'             => 'subscription',
+      'current_period_end' => time()+60*60*24,
+      'status'             => 'cancelled',
+      // Note US? spelling of cancelled
+      'canceled_at'        => $cancelTimestamp,
+      'plan' => [
+        'amount' => $this->total,
+        'currency' => 'usd',
+      ],
+    ]);
+    $stripeClient = $this->paymentObject->stripeClient;
+    $stripeClient->subscriptions = $this->createMock('Stripe\\Service\\SubscriptionService');
+    $stripeClient->subscriptions
+        ->method('retrieve')
+        ->will($this->returnValueMapOrDie([
+          ['sub_mock', NULL, NULL, $mockSubscription],
+        ]));
+    //
+    // Now test if we get customer.subscription.deleted .
+    //
+    $this->simulateEvent([
+      'type'             => 'customer.subscription.deleted',
+      'id'               => 'evt_mock_2',
+      'object'           => 'event',
+      'livemode'         => false,
+      'pending_webhooks' => 0,
+      'request'          => [ 'id' => NULL ],
+      'data'             => [
+        'object' => $mockSubscription
+      ],
+    ], TRUE);
+
+    // Recur should be Cancelled.
+    $this->checkContribRecur( [
+        'contribution_status_id' => 'Cancelled',
+        'cancel_date'            => date('Y-m-d H:i:s', $cancelTimestamp),
+      ]);
+
+    // We should still have 1 contrib which should be unaffected.
+    $this->getContributionsAndAssertCount(1);
+    $this->checkContrib([
+      'contribution_status_id' => 'Completed',
+      'trxn_id'                => 'in_mock,ch_mock',
+    ]);
+  }
   /**
    * Retrieve the event with a matching subscription id
    *
@@ -688,7 +642,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
     $ipnClass = new CRM_Core_Payment_StripeIPN();
 
     if ($exceptionOnFailure) {
-      // We dont' expect failure, so ensure exceptions are not caught.
+      // We don’t' expect failure, so ensure exceptions are not caught.
       $ipnClass->exceptionOnFailure = $exceptionOnFailure;
     }
     $ipnClass->setEventID($event->id);
@@ -782,12 +736,300 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
       $this->assertEquals($expect, $contribution[$field]);
     }
   }
+  /**
+   * Returns an array of arrays of contributions.
+   */
+  protected function getContributionsAndAssertCount(int $expectedCount):array {
+    $contributions = civicrm_api3('Contribution', 'get', [
+      'contribution_recur_id' => $this->contributionRecurID,
+      'is_test'               => 1,
+      'options'               => ['sort' => 'id'],
+      'sequential'            => 1,
+    ]);
+    $this->assertEquals($expectedCount, $contributions['count']);
+    return $contributions['values'];
+  }
+  /**
+   * DRY code
+   */
+  protected function getMocksForRecurringInvoiceFinalized() :array {
+
+    $common = [
+          'subscription' => 'sub_mock',
+          'customer'     => 'cus_mock',
+          'created'      => time(),
+    ];
+    $mockCharge1 = new PropertySpy('charge1', $common + [
+          'id'                  => 'ch_mock',
+          'object'              => 'charge',
+          'balance_transaction' => 'txn_mock',
+          'amount'              => $this->total*100,
+        ]);
+    $mockCharge2 = new PropertySpy('charge2', $common + [
+          'id'                  => 'ch_mock_2',
+          'object'              => 'charge',
+          'balance_transaction' => 'txn_mock_2',
+          'amount'              => $this->total*100,
+        ]);
+    $mockInvoice2 = new PropertySpy('invoice2', $common + [
+          'id'           => 'in_mock_2',
+          'object'       => 'invoice',
+          'amount_due'   => $this->total*100,
+          'charge'       => 'ch_mock_2',
+        ]);
+    $balanceTransaction2 = new PropertySpy('balance_transaction2', [
+      'id'            => 'txn_mock_2',
+      'object'        => 'balance_transaction',
+      'amount'        => $this->total * 100,
+      'created'       => time(),
+      'currency'      => 'usd',
+      'exchange_rate' => NULL,
+      'fee'           => 1190, /* means $11.90 */
+      'status'        => 'available',
+      'type'          => 'charge',
+    ]);
+
+    $this->paymentObject->stripeClient->balanceTransactions = $this->createMock('Stripe\\Service\\BalanceTransactionService');
+    $this->paymentObject->stripeClient->balanceTransactions
+      ->method('retrieve')
+      ->with($this->equalTo('txn_mock_2'))
+      ->willReturn($balanceTransaction2);
+
+    $this->paymentObject->stripeClient->charges = $this->createMock('Stripe\\Service\\ChargeService');
+    $this->paymentObject->stripeClient->charges
+      ->method('retrieve')
+      ->will($this->returnValueMapOrDie([
+        ['ch_mock', NULL, NULL, $mockCharge1],
+        ['ch_mock_2', NULL, NULL, $mockCharge2],
+      ]));
+
+    return [$mockCharge1, $mockCharge2, $mockInvoice2, $balanceTransaction2];
+  }
+  /**
+   * DRY code. Sets up the database as it would be after a recurring contrib
+   * has been set up with Stripe.
+   *
+   * Results in a pending ContributionRecur and a pending Contribution record.
+   *
+   * The following mock Stripe IDs strings are used:
+   *
+   * - pm_mock   PaymentMethod
+   * - pi_mock   PaymentIntent
+   * - cus_mock  Customer
+   * - ch_mock   Charge
+   * - txn_mock  Balance transaction
+   * - sub_mock  Subscription
+   */
+  protected function mockRecurringPaymentSetup() {
+    PropertySpy::$buffer = 'none';
+    // Set this to 'print' or 'log' maybe more helpful in debugging but for
+    // generally running tests 'exception' suits as we don't expect any output.
+    PropertySpy::$outputMode = 'exception';
+
+    $this->assertInstanceOf('CRM_Core_Payment_Stripe', $this->paymentObject);
+
+    // Create a mock stripe client.
+    $stripeClient = $this->createMock('Stripe\\StripeClient');
+    // Update our CRM_Core_Payment_Stripe object and ensure any others
+    // instantiated separately will also use it.
+    $this->paymentObject->setMockStripeClient($stripeClient);
+
+    // Mock the payment methods service.
+    $mockPaymentMethod = $this->createMock('Stripe\\PaymentMethod');
+    $mockPaymentMethod->method('__get')
+                      ->will($this->returnValueMap([
+                        [ 'id', 'pm_mock']
+                      ]));
+    $stripeClient->paymentMethods = $this->createMock('Stripe\\Service\\PaymentMethodService');
+    $stripeClient->paymentMethods
+                 ->method('create')
+                 ->willReturn($mockPaymentMethod);
+    $stripeClient->paymentMethods
+                 ->expects($this->atLeastOnce())
+                 ->method('retrieve')
+                 ->with($this->equalTo('pm_mock'))
+                 ->willReturn($mockPaymentMethod);
+
+    // Mock the Customers service
+    $stripeClient->customers = $this->createMock('Stripe\\Service\\CustomerService');
+    $stripeClient->customers
+                 ->method('create')
+                 ->willReturn(
+                     new PropertySpy('customers.create', ['id' => 'cus_mock'])
+                 );
+    $stripeClient->customers
+                 ->method('retrieve')
+                 ->with($this->equalTo('cus_mock'))
+                 ->willReturn(
+                     new PropertySpy('customers.retrieve', ['id' => 'cus_mock'])
+                 );
+
+    $mockPlan = $this->createMock('Stripe\\Plan');
+    $mockPlan
+      ->method('__get')
+      ->will($this->returnValueMap([
+        ['id', 'every-1-month-' . ($this->total * 100) . '-usd-test']
+      ]));
+    $stripeClient->plans = $this->createMock('Stripe\\Service\\PlanService');
+    $stripeClient->plans
+      ->method('retrieve')
+      ->willReturn($mockPlan);
+
+
+    // Need a mock intent with id and status
+    $mockCharge = $this->createMock('Stripe\\Charge');
+    $mockCharge
+      ->method('__get')
+      ->will($this->returnValueMap([
+        ['id', 'ch_mock'],
+        ['captured', TRUE],
+        ['status', 'succeeded'],
+        ['balance_transaction', 'txn_mock'],
+      ]));
+    $mockPaymentIntent = $this->createMock('Stripe\\PaymentIntent');
+    $mockPaymentIntent
+      ->method('__get')
+      ->will($this->returnValueMap([
+        ['id', 'pi_mock'],
+        ['status', 'succeeded'],
+        ['charges', (object) ['data' => [ $mockCharge ]]]
+      ]));
+
+    $mockSubscription = new PropertySpy('subscription.create', [
+          'id' => 'sub_mock',
+          'current_period_end' => time()+60*60*24,
+          'latest_invoice' => [
+            'id' => 'in_mock',
+            'payment_intent' => $mockPaymentIntent,
+          ],
+          'pending_setup_intent' => '',
+        ]);
+    $stripeClient->subscriptions = $this->createMock('Stripe\\Service\\SubscriptionService');
+    $stripeClient->subscriptions
+        ->method('create')
+        ->willReturn($mockSubscription);
+    $stripeClient->subscriptions
+        ->method('retrieve')
+        ->with($this->equalTo('sub_mock'))
+        ->willReturn($mockSubscription);
+
+    $stripeClient->balanceTransactions = $this->createMock('Stripe\\Service\\BalanceTransactionService');
+    $stripeClient->balanceTransactions
+    ->method('retrieve')
+    ->with($this->equalTo('txn_mock'))
+    ->willReturn(new PropertySpy('balanceTransaction', [
+      'id' => 'txn_mock',
+      'fee' => 1190, /* means $11.90 */
+      'currency' => 'usd',
+      'exchange_rate' => NULL,
+      'object' => 'balance_transaction',
+    ]));
+
+    // $stripeClient->paymentIntents = $this->createMock('Stripe\\Service\\PaymentIntentService');
+    // todo change the status from requires_capture to ?
+    //$stripeClient->paymentIntents ->method('update') ->willReturn();
+
+    $mockInvoice = new PropertySpy('Invoice', [
+        'amount_due' => $this->total*100,
+        'charge_id' => 'ch_mock', //xxx
+        'created' => time(),
+        'currency' => 'usd',
+        'customer' => 'cus_mock',
+        'id' => 'in_mock',
+        'object' => 'invoice',
+        'subscription' => 'sub_mock',
+      ]);
+    $stripeClient->invoices = $this->createMock('Stripe\\Service\\InvoiceService');
+    $stripeClient->invoices
+                 ->expects($this->never())
+                 ->method($this->anything())
+               ;
+    /*
+      ->method('all')
+      ->willReturn(['data' => $mockInvoice]);
+     */
+
+    $stripeClient->charges = $this->createMock('Stripe\\Service\\ChargeService');
+    $stripeClient->charges
+                 ->method('retrieve')
+                 ->with($this->equalTo('ch_mock'))
+                 ->willReturn($mockCharge);
+
+    // Setup a recurring contribution for $this->total per month.
+    $this->setupRecurringTransaction();
+
+    // Submit the payment.
+    $payment_extra_params = [
+      'is_recur'            => 1,
+      'contributionRecurID' => $this->contributionRecurID,
+      'contributionID'      => $this->contributionID,
+      'frequency_unit'      => $this->frequency_unit,
+      'frequency_interval'  => $this->frequency_interval,
+      'installments'        => $this->installments,
+    ];
+
+    $this->doPayment($payment_extra_params);
+
+    //
+    // Check the Contribution
+    // ...should be pending
+    // ...its transaction ID should be our Invoice ID.
+    //
+    $this->checkContrib([
+      'contribution_status_id' => 'Pending',
+      'trxn_id'                => 'in_mock',
+    ]);
+
+    //
+    // Check the CotnributionRecur
+    //
+    // The subscription ID should be in both processor_id and trxn_id fields
+    // We expect it to be pending
+    $this->checkContribRecur([
+      'contribution_status_id' => 'Pending',
+      'trxn_id'                => 'sub_mock',
+      'processor_id'           => 'sub_mock',
+    ]);
+  }
   /**
    *
    */
   protected function returnValueMapOrDie($map) :ValueMapOrDie {
     return new ValueMapOrDie($map);
   }
+  /**
+   * Simulate an event being sent from Stripe and processed by our IPN code.
+   *
+   * @var array|Stripe\Event|PropertySpy|mock $eventData
+   * @var bool $exceptionOnFailure
+   *
+   * @return bool result from ipn()
+   */
+  protected function simulateEvent($eventData, $exceptionOnFailure=TRUE) {
+
+    // Mock Event service.
+    $stripeClient = $this->paymentObject->stripeClient;
+    $stripeClient->events = $this->createMock('Stripe\\Service\\EventService');
+
+    $mockEvent = PropertySpy::fromMixed('simulate ' . $eventData['type'], $eventData);
+    $stripeClient->events
+                 ->method('all')
+                 ->willReturn(new PropertySpy('events.all', [ 'data' => [ $mockEvent ] ]));
+    $stripeClient->events
+                 ->expects($this->atLeastOnce())
+                 ->method('retrieve')
+                 ->with($this->equalTo($eventData['id']))
+                 ->willReturn(new PropertySpy('events.retrieve', $mockEvent));
+
+    // Fetch the event
+    // Previously used the following - but see docblock of getEvent()
+    // $event = $this->getEvent($eventData['type']);
+    // $this->assertNotEmpty($event, "Failed to fetch event type $eventData[type]");
+
+    // Process it with the IPN/webhook
+    return $this->ipn($mockEvent, TRUE, $exceptionOnFailure);
+  }
+
 }
 
 /**