Skip to content
Snippets Groups Projects
Commit 51b5670b authored by mattwire's avatar mattwire
Browse files

Add unit tests for Stripe.Importsubscription/ImportCharge API. Support...

Add unit tests for Stripe.Importsubscription/ImportCharge API. Support importing subscriptions with future startdate/trial period (no invoice so we create a template contribution)
parent 84ae2b38
Branches
Tags
1 merge request!176Release 6.7
......@@ -62,10 +62,10 @@ function civicrm_api3_stripe_importcharge($params) {
$paymentProcessor->setAPIParams();
// Retrieve the Stripe charge.
$charge = $paymentProcessor->stripeClient->charges->retrieve($params['charge']);
$stripeCharge = $paymentProcessor->stripeClient->charges->retrieve($params['charge']);
// Get the related invoice.
$stripeInvoice = $paymentProcessor->stripeClient->invoices->retrieve($charge->invoice);
$stripeInvoice = $paymentProcessor->stripeClient->invoices->retrieve($stripeCharge->invoice);
if (!$stripeInvoice) {
throw new \CiviCRM_API3_Exception(E::ts('The charge does not have an invoice, it cannot be imported.'));
}
......@@ -81,17 +81,14 @@ function civicrm_api3_stripe_importcharge($params) {
$sourceText = 'Stripe: Manual import via API';
}
$is_test = $paymentProcessor->getIsTestMode();
// Check for a subscription.
$subscription = CRM_Stripe_Api::getObjectParam('subscription_id', $stripeInvoice);
$contribution_recur_id = NULL;
if ($subscription) {
// Lookup the contribution_recur_id.
$cr_results = \Civi\Api4\ContributionRecur::get()
$cr_results = \Civi\Api4\ContributionRecur::get(FALSE)
->addWhere('trxn_id', '=', $subscription)
->addWhere('is_test', '=', $is_test)
->setCheckPermissions(FALSE)
->addWhere('is_test', '=', $paymentProcessor->getIsTestMode())
->execute();
$contribution_recur = $cr_results->first();
if (!$contribution_recur) {
......@@ -107,7 +104,6 @@ function civicrm_api3_stripe_importcharge($params) {
// We update these parameters regardless if it's a new contribution
// or an existing contributions.
$contributionParams['receive_date'] = CRM_Stripe_Api::getObjectParam('receive_date', $stripeInvoice);
$contributionParams['contribution_status_id'] = CRM_Stripe_Api::getObjectParam('status_id', $stripeInvoice);
$contributionParams['total_amount'] = CRM_Stripe_Api::getObjectParam('total_amount', $stripeInvoice);
// Check if a contribution already exists.
......@@ -118,10 +114,9 @@ function civicrm_api3_stripe_importcharge($params) {
}
else {
// Check database.
$c_results = \Civi\Api4\Contribution::get()
$c_results = \Civi\Api4\Contribution::get(FALSE)
->addWhere('trxn_id', 'LIKE', '%'. $params['charge'].'%')
->addWhere('is_test', '=', $is_test)
->setCheckPermissions(FALSE)
->addWhere('is_test', '=', $paymentProcessor->getIsTestMode())
->execute();
$contribution = $c_results->first();
if ($contribution) {
......@@ -140,17 +135,43 @@ function civicrm_api3_stripe_importcharge($params) {
$contributionParams['currency'] = CRM_Stripe_Api::getObjectParam('currency', $stripeInvoice);
$contributionParams['receive_date'] = CRM_Stripe_Api::getObjectParam('receive_date', $stripeInvoice);
$contributionParams['trxn_id'] = CRM_Stripe_Api::getObjectParam('charge_id', $stripeInvoice);
$contributionParams['contribution_status_id'] = CRM_Stripe_Api::getObjectParam('status_id', $stripeInvoice);
$contributionParams['payment_instrument_id'] = !empty($params['payment_instrument_id']) ? $params['payment_instrument_id'] : 'Credit Card';
$contributionParams['financial_type_id'] = !empty($params['financial_type_id']) ? $params['financial_type_id'] : 'Donation';
$contributionParams['is_test'] = isset($paymentProcessor['is_test']) && $paymentProcessor['is_test'] ? 1 : 0;
$contributionParams['is_test'] = $paymentProcessor->getIsTestMode();
$contributionParams['contribution_source'] = $sourceText;
$contributionParams['is_test'] = $is_test;
$contributionParams['contribution_status_id:name'] = 'Pending';
if ($contribution_recur_id) {
$contributionParams['contribution_recur_id'] = $contribution_recur_id;
}
}
$contribution = civicrm_api3('Contribution', 'create', $contributionParams);
return civicrm_api3_create_success($contribution['values']);
$contribution = \Civi\Api4\Contribution::create(FALSE)
->setValues($contributionParams)
->execute()
->first();
if (CRM_Stripe_Api::getObjectParam('status_id', $stripeInvoice) === 'Completed') {
$paymentParams = [
'contribution_id' => $contribution['id'],
'total_amount' => $contributionParams['total_amount'],
'trxn_date' => $contributionParams['receive_date'],
'payment_processor_id' => $params['ppid'],
'is_send_contribution_notification' => FALSE,
'trxn_id' => CRM_Stripe_Api::getObjectParam('charge_id', $stripeInvoice),
'order_reference' => CRM_Stripe_Api::getObjectParam('invoice_id', $stripeInvoice),
'payment_instrument_id' => $contributionParams['payment_instrument_id'],
];
if (!empty(CRM_Stripe_Api::getObjectParam('balance_transaction', $stripeCharge))) {
$stripeBalanceTransaction = $paymentProcessor->stripeClient->balanceTransactions->retrieve(
CRM_Stripe_Api::getObjectParam('balance_transaction', $stripeCharge)
);
$paymentParams['fee_amount'] = $stripeBalanceTransaction->fee / 100;
}
civicrm_api3('Payment', 'create', $paymentParams);
}
$contribution = \Civi\Api4\Contribution::get(FALSE)
->addWhere('id', '=', $contribution['id'])
->execute()
->first();
return civicrm_api3_create_success($contribution);
}
......@@ -26,6 +26,9 @@ use CRM_Stripe_ExtensionUtil as E;
function civicrm_api3_stripe_importsubscription($params) {
civicrm_api3_verify_mandatory($params, NULL, ['subscription', 'contact_id', 'ppid']);
$params['payment_instrument_id'] = $params['payment_instrument_id'] ?? 1; // Credit Card
$params['financial_type_id'] = $params['financial_type_id'] ?? 1; // Donation
/** @var \CRM_Core_Payment_Stripe $paymentProcessor */
$paymentProcessor = \Civi\Payment\System::singleton()->getById($params['ppid']);
$paymentProcessor->setAPIParams();
......@@ -37,13 +40,14 @@ function civicrm_api3_stripe_importsubscription($params) {
$customerParams = [
'customer_id' => CRM_Stripe_Api::getObjectParam('customer_id', $stripeSubscription),
'processor_id' => (int) $params['ppid'],
'contact_id' => $params['contact_id']
];
$custresult = civicrm_api3('StripeCustomer', 'get', $customerParams);
if ($custresult['count'] == 0) {
// Create the customer.
$customerParams['contact_id'] = $params['contact_id'];
$custresult = civicrm_api3('StripeCustomer', 'create', $customerParams);
civicrm_api3('StripeCustomer', 'create', $customerParams);
$custresult = civicrm_api3('StripeCustomer', 'get', $customerParams);
}
$customer = array_pop($custresult['values']);
......@@ -52,9 +56,12 @@ function civicrm_api3_stripe_importsubscription($params) {
}
// Create the recur record in CiviCRM if it doesn't exist.
$contributionRecur = civicrm_api3('ContributionRecur', 'get', ['trxn_id' => $params['subscription'] ]);
$contributionRecur = \Civi\Api4\ContributionRecur::get(FALSE)
->addWhere('processor_id', '=', $params['subscription'])
->execute()
->first();
if ($contributionRecur['count'] == 0) {
if (empty($contributionRecur)) {
$contributionRecurParams = [
'contact_id' => $params['contact_id'],
'amount' => CRM_Stripe_Api::getObjectParam('plan_amount', $stripeSubscription),
......@@ -63,13 +70,11 @@ function civicrm_api3_stripe_importsubscription($params) {
'frequency_interval' => CRM_Stripe_Api::getObjectParam('frequency_interval', $stripeSubscription),
'start_date' => CRM_Stripe_Api::getObjectParam('plan_start', $stripeSubscription),
'processor_id' => $params['subscription'],
'trxn_id' => $params['subscription'],
'contribution_status_id' => CRM_Stripe_Api::getObjectParam('status_id', $stripeSubscription),
'cycle_day' => CRM_Stripe_Api::getObjectParam('cycle_day', $stripeSubscription),
'auto_renew' => 1,
'payment_processor_id' => $params['ppid'],
'payment_instrument_id' => !empty($params['payment_instrument_id']) ? $params['payment_instrument_id'] : 'Credit Card',
'financial_type_id' => !empty($params['financial_type_id']) ? $params['financial_type_id'] : 'Donation',
'payment_instrument_id' => $params['payment_instrument_id'],
'financial_type_id' => $params['financial_type_id'],
'is_email_receipt' => !empty($params['is_email_receipt']) ? 1 : 0,
'is_test' => $paymentProcessor->getIsTestMode(),
'contribution_source' => !empty($params['contribution_source']) ? $params['contribution_source'] : '',
......@@ -77,7 +82,10 @@ function civicrm_api3_stripe_importsubscription($params) {
if (isset($params['recur_id']) && $params['recur_id']) {
$contributionRecurParams['id'] = $params['recur_id'];
}
$contributionRecur = civicrm_api3('ContributionRecur', 'create', $contributionRecurParams);
$contributionRecur = \Civi\Api4\ContributionRecur::create(FALSE)
->setValues($contributionRecurParams)
->execute()
->first();
}
// Get the invoices for the subscription
$invoiceParams = [
......@@ -85,58 +93,81 @@ function civicrm_api3_stripe_importsubscription($params) {
'limit' => 100,
];
$stripeInvoices = $paymentProcessor->stripeClient->invoices->all($invoiceParams);
foreach ($stripeInvoices->data as $stripeInvoice) {
if (CRM_Stripe_Api::getObjectParam('subscription_id', $stripeInvoice) === $params['subscription']) {
$charge = CRM_Stripe_Api::getObjectParam('charge_id', $stripeInvoice);
if (empty($charge)) {
continue;
}
$exists_params = [
'contribution_test' => $paymentProcessor->getIsTestMode(),
'trxn_id' => $charge
];
$contribution = civicrm_api3('Mjwpayment', 'get_contribution', $exists_params);
if ($contribution['count'] == 0) {
// It has not been imported, so import it now.
$charge_params = [
'charge' => $charge,
'financial_type_id' => $params['financial_type_id'],
'payment_instrument_id' => $params['payment_instrument_id'],
'ppid' => $params['ppid'],
'contact_id' => $params['contact_id'],
'contribution_source' => ($params['contribution_source'] ?? ''),
];
$contribution = civicrm_api3('Stripe', 'Importcharge', $charge_params);
// Link to membership record
// By default we'll match the latest active membership, unless membership_id is passed in.
if (!empty($params['membership_id'])) {
$membershipParams = [
'id' => $params['membership_id'],
'contribution_recur_id' => $contributionRecur['id'],
];
$membership = civicrm_api3('Membership', 'create', $membershipParams);
if ($stripeInvoices->count()) {
// We have one or more invoices to import (as contributions)
foreach ($stripeInvoices->data as $stripeInvoice) {
if (CRM_Stripe_Api::getObjectParam('subscription_id', $stripeInvoice) === $params['subscription']) {
$charge = CRM_Stripe_Api::getObjectParam('charge_id', $stripeInvoice);
if (empty($charge)) {
continue;
}
elseif (!empty($params['membership_auto'])) {
$membershipParams = [
$exists_params = [
'contribution_test' => $paymentProcessor->getIsTestMode(),
'trxn_id' => $charge
];
$contribution = civicrm_api3('Mjwpayment', 'get_contribution', $exists_params);
if ($contribution['count'] == 0) {
// It has not been imported, so import it now.
$charge_params = [
'charge' => $charge,
'financial_type_id' => $params['financial_type_id'],
'payment_instrument_id' => $params['payment_instrument_id'],
'ppid' => $params['ppid'],
'contact_id' => $params['contact_id'],
'options' => ['limit' => 1, 'sort' => "id DESC"],
'contribution_recur_id' => ['IS NULL' => 1],
'is_test' => $paymentProcessor->getIsTestMode(),
'active_only' => 1,
'contribution_source' => ($params['contribution_source'] ?? ''),
];
$membership = civicrm_api3('Membership', 'get', $membershipParams);
if (!empty($membership['id'])) {
$contribution = civicrm_api3('Stripe', 'Importcharge', $charge_params);
// Link to membership record
// By default we'll match the latest active membership, unless membership_id is passed in.
if (!empty($params['membership_id'])) {
$membershipParams = [
'id' => $membership['id'],
'id' => $params['membership_id'],
'contribution_recur_id' => $contributionRecur['id'],
];
$membership = civicrm_api3('Membership', 'create', $membershipParams);
}
elseif (!empty($params['membership_auto'])) {
$membershipParams = [
'contact_id' => $params['contact_id'],
'options' => ['limit' => 1, 'sort' => "id DESC"],
'contribution_recur_id' => ['IS NULL' => 1],
'is_test' => $paymentProcessor->getIsTestMode(),
'active_only' => 1,
];
$membership = civicrm_api3('Membership', 'get', $membershipParams);
if (!empty($membership['id'])) {
$membershipParams = [
'id' => $membership['id'],
'contribution_recur_id' => $contributionRecur['id'],
];
civicrm_api3('Membership', 'create', $membershipParams);
}
}
}
}
}
}
else {
// We have no invoices to import. This will be for one of the following reasons:
// - Stripe subscription is in a free "trial" period.
// - Stripe subscription has not yet reached the start date.
// In this case we have to create a template contribution (see https://lab.civicrm.org/dev/financial/-/issues/6)
// because CiviCRM currently expects at least 1 contribution per ContributionRecur.
$contribution = \Civi\Api4\Contribution::create(FALSE)
->addValue('contribution_recur_id', $contributionRecur['id'])
->addValue('contact_id', $contributionRecur['contact_id'])
->addValue('financial_type_id', $contributionRecur['financial_type_id'])
->addValue('payment_instrument_id', $contributionRecur['payment_instrument_id'])
->addValue('source', $params['contribution_source'] ?? '')
->addValue('total_amount', $contributionRecur['amount'])
->addValue('currency', $contributionRecur['currency'])
->addValue('is_test', $paymentProcessor->getIsTestMode())
->addValue('is_template', TRUE)
->addValue('contribution_status_id:name', 'Template')
->execute()
->first();
}
$results = [
'subscription' => $params['subscription'],
......
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
* @file
*
* The purpose of these tests is to test this extension's code. We are not
* focussed on testing that the StripeAPI behaves as it should, and therefore
* we mock the Stripe API. This approach enables us to focus on our code,
* removes external factors like network connectivity, and enables tests to
* run quickly.
*
* Gotchas for developers new to phpunit's mock objects
*
* - once you have created a mock and called method('x') you cannot call
* method('x') again; you'll need to make a new mock.
* - $this->any() refers to an argument for a with() matcher.
* - $this->anything() refers to a method for a method() matcher.
*
*/
/**
* Stripe (CiviCRM) API3 tests
*
* @group headless
*/
require ('BaseTest.php');
class CRM_Stripe_ApiTest extends CRM_Stripe_BaseTest {
protected $contributionRecurID;
protected $created_ts;
protected $contributionRecur = [
'frequency_unit' => 'month',
'frequency_interval' => 1,
'installments' => 5,
];
// This test is particularly dirty for some reason so we have to
// force a reset.
public function setUpHeadless() {
$force = FALSE;
return \Civi\Test::headless()
->install('mjwshared')
->installMe(__DIR__)
->apply($force);
}
/**
* Test importing a subscription which has one paid invoice
* This also tests the Stripe.Importcharge API
*/
public function testImportSubscription() {
$this->mockStripeSubscription();
/** @var \CRM_Core_Payment_Stripe $paymentProcessor */
$paymentProcessor = \Civi\Payment\System::singleton()->getById($this->paymentProcessorID);
$paymentProcessor->setAPIParams();
$result = civicrm_api3('Stripe', 'importsubscription', [
'subscription' => 'sub_mock',
'contact_id' => $this->contactID,
'ppid' => $this->paymentProcessorID
]);
$this->contributionID = $result['values']['contribution_id'];
$this->contributionRecurID = $result['values']['recur_id'];
//
// Check the Contribution
// ...should be Completed
// ...its transaction ID should be our Charge ID.
//
$this->checkContrib([
'contribution_status_id' => 'Completed',
'trxn_id' => 'ch_mock',
]);
//
// Check the ContributionRecur
//
// The subscription ID should be in both processor_id and trxn_id fields
// We expect it to be 'In Progress' (because we have a Completed contribution).
$this->checkContribRecur([
'contribution_status_id' => 'In Progress',
'trxn_id' => 'sub_mock',
'processor_id' => 'sub_mock',
]);
// Check the payment. It should have trxn_id=Stripe charge ID and order_reference=Stripe Invoice ID
$this->checkPayment([
// Completed
'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'),
'trxn_id' => 'ch_mock',
'order_reference' => 'in_mock',
]);
}
/**
* Test importing a subscription which has no paid invoices
* This can happen if start_date is in the future or subscription has a free trial period
* In this case we create a template contribution
* This also tests the Stripe.Importcharge API
*/
public function testImportSubscriptionWithNoInvoice() {
$this->mockStripeSubscription(['hasPaidInvoice' => FALSE]);
/** @var \CRM_Core_Payment_Stripe $paymentProcessor */
$paymentProcessor = \Civi\Payment\System::singleton()->getById($this->paymentProcessorID);
$paymentProcessor->setAPIParams();
$result = civicrm_api3('Stripe', 'importsubscription', [
'subscription' => 'sub_mock',
'contact_id' => $this->contactID,
'ppid' => $this->paymentProcessorID
]);
$this->contributionID = $result['values']['contribution_id'];
$this->contributionRecurID = $result['values']['recur_id'];
//
// Check the Contribution
// ...should be Completed
// ...its transaction ID should be our Charge ID.
//
$this->checkContrib([
'contribution_status_id' => 'Template',
'trxn_id' => '',
'is_template' => TRUE
]);
//
// Check the ContributionRecur
//
// The subscription ID should be in both processor_id and trxn_id fields
// We expect it to be 'In Progress' (because we have a Completed contribution).
$this->checkContribRecur([
'contribution_status_id' => 'Pending',
'trxn_id' => 'sub_mock',
'processor_id' => 'sub_mock',
]);
}
/**
* DRY code. Sets up the Stripe objects needed to import a subscription
*
* 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
*
* @return \PHPUnit\Framework\MockObject\MockObject
*/
protected function mockStripeSubscription($subscriptionParams = []) {
$subscriptionParams['hasPaidInvoice'] = $subscriptionParams['hasPaidInvoice'] ?? TRUE;
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 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'],
['amount', $this->total*100],
['currency', 'usd'],
['interval_count', $this->contributionRecur['frequency_interval']],
['interval', $this->contributionRecur['frequency_unit']],
]));
$stripeClient->plans = $this->createMock('Stripe\\Service\\PlanService');
$stripeClient->plans
->method('retrieve')
->willReturn($mockPlan);
$mockSubscriptionParams = [
'id' => 'sub_mock',
'object' => 'subscription',
'customer' => 'cus_mock',
'current_period_end' => time()+60*60*24,
'pending_setup_intent' => '',
'plan' => $mockPlan,
'start_date' => time(),
];
if ($subscriptionParams['hasPaidInvoice']) {
// Need a mock intent with id and status
$mockCharge = $this->createMock('Stripe\\Charge');
$mockCharge
->method('__get')
->will($this->returnValueMap([
['id', 'ch_mock'],
['object', 'charge'],
['captured', TRUE],
['status', 'succeeded'],
['balance_transaction', 'txn_mock'],
['invoice', 'in_mock']
]));
$mockChargesCollection = new \Stripe\Collection();
$mockChargesCollection->data = [$mockCharge];
$mockPaymentIntent = $this->createMock('Stripe\\PaymentIntent');
$mockPaymentIntent
->method('__get')
->will($this->returnValueMap([
['id', 'pi_mock'],
['status', 'succeeded'],
['charges', $mockChargesCollection]
]));
$mockSubscriptionParams['latest_invoice'] = [
'id' => 'in_mock',
'payment_intent' => $mockPaymentIntent,
];
}
$mockSubscription = new PropertySpy('subscription.create', $mockSubscriptionParams);
$stripeClient->subscriptions = $this->createMock('Stripe\\Service\\SubscriptionService');
$stripeClient->subscriptions
->method('create')
->willReturn($mockSubscription);
$stripeClient->subscriptions
->method('retrieve')
->with($this->equalTo('sub_mock'))
->willReturn($mockSubscription);
if ($subscriptionParams['hasPaidInvoice']) {
$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',
]));
$mockCharge = new PropertySpy('Charge', [
'id' => 'ch_mock',
'object' => 'charge',
'captured' => TRUE,
'status' => 'succeeded',
'balance_transaction' => 'txn_mock',
'invoice' => 'in_mock'
]);
$stripeClient->charges = $this->createMock('Stripe\\Service\\ChargeService');
$stripeClient->charges
->method('retrieve')
->with($this->equalTo('ch_mock'))
->willReturn($mockCharge);
$mockInvoice = new PropertySpy('Invoice', [
'amount_due' => $this->total * 100,
'charge' => 'ch_mock', //xxx
'created' => time(),
'currency' => 'usd',
'customer' => 'cus_mock',
'id' => 'in_mock',
'object' => 'invoice',
'subscription' => 'sub_mock',
'paid' => TRUE
]);
$mockInvoicesCollection = new \Stripe\Collection();
$mockInvoicesCollection->data = [$mockInvoice];
$stripeClient->invoices = $this->createMock('Stripe\\Service\\InvoiceService');
$stripeClient->invoices
->method('all')
->willReturn($mockInvoicesCollection);
$stripeClient->invoices
->method('retrieve')
->with($this->equalTo('in_mock'))
->willReturn($mockInvoice);
}
else {
// No invoices
$mockInvoicesCollection = new \Stripe\Collection();
$mockInvoicesCollection->data = [];
$stripeClient->invoices = $this->createMock('Stripe\\Service\\InvoiceService');
$stripeClient->invoices
->method('all')
->willReturn($mockInvoicesCollection);
}
}
/**
*
*/
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);
}
}
/**
* This class provides a data structure for mocked stripe responses, and will detect
* if a property is requested that is not already mocked.
*
* This enables us to only need to mock the things we actually use, which
* hopefully makes the code more readable/maintainable.
*
* It implements the same interfaces as StripeObject does.
*
*
*/
class PropertySpy implements ArrayAccess, Iterator, Countable, JsonSerializable {
/**
* @var string $outputMode print|log|exception
*
* log means Civi::log()->debug()
* exception means throw a RuntimeException. Use this once your tests are passing,
* so that in future if the code starts relying on something we have not
* mocked we can figure it out quickly.
*/
public static $outputMode = 'print';
/**
* @var string $buffer
*
* - 'none' output immediately.
* - 'global' tries to output things chronologically at end when all objects have been killed.
* - 'local' outputs everything that happened to this object on destruction
*/
public static $buffer = 'none'; /* none|global|local */
protected $_name;
protected $_props;
protected $localLog = [];
public static $globalLog = [];
public static $globalObjects = 0;
protected $iteratorIdx=0;
// Iterator
public function current() {
// $this->warning("Iterating " . array_keys($this->_props)[$this->key()]);
return current($this->_props);
}
/**
* Implemetns Countable
*/
public function count() {
return \count($this->_props);
}
public function key() {
return key($this->_props);
}
public function next() {
return next($this->_props);
}
public function rewind() {
return reset($this->_props);
}
public function valid() {
return array_key_exists(key($this->_props), $this->_props);
}
public function __construct($name, $props) {
$this->_name = $name;
foreach ($props as $k => $v) {
$this->$k = $v;
}
static::$globalObjects++;
}
/**
* Factory method
*
* @param array|PropertySpy
*/
public static function fromMixed($name, $data) {
if ($data instanceof PropertySpy) {
return $data;
}
if (is_array($data)) {
return new static($name, $data);
}
throw new \Exception("PropertySpy::fromMixed requires array|PropertySpy, got "
. is_object($data) ? get_class($data) : gettype($data)
);
}
public function __destruct() {
static::$globalObjects--;
if (static::$buffer === 'local') {
$msg = "PropertySpy: $this->_name\n"
. json_encode($this->localLog, JSON_PRETTY_PRINT) . "\n";
if (static::$outputMode === 'print') {
print $msg;
}
elseif (static::$outputMode === 'log') {
\Civi::log()->debug($msg);
}
elseif (static::$outputMode === 'exception') {
throw new \RuntimeException($msg);
}
}
elseif (static::$buffer === 'global' && static::$globalObjects === 0) {
// End of run.
$msg = "PropertySpy:\n" . json_encode(static::$globalLog, JSON_PRETTY_PRINT) . "\n";
if (static::$outputMode === 'print') {
print $msg;
}
elseif (static::$outputMode === 'log') {
\Civi::log()->debug($msg);
}
elseif (static::$outputMode === 'exception') {
throw new \RuntimeException($msg);
}
}
}
protected function warning($msg) {
if (static::$buffer === 'none') {
// Immediate output
if (static::$outputMode === 'print') {
print "$this->_name $msg\n";
}
elseif (static::$outputMode === 'log') {
Civi::log()->debug("$this->_name $msg\n");
}
}
elseif (static::$buffer === 'global') {
static::$globalLog[] = "$this->_name $msg";
}
elseif (static::$buffer === 'local') {
$this->localLog[] = $msg;
}
}
public function __get($prop) {
if ($prop === 'log') {
throw new \Exception("stop");
}
if (array_key_exists($prop, $this->_props)) {
return $this->_props[$prop];
}
$this->warning("->$prop requested but not defined");
return NULL;
}
public function __set($prop, $value) {
$this->_props[$prop] = $value;
if (is_array($value)) {
// Iterative spies.
$value = new static($this->_name . "{" . "$prop}", $value);
}
$this->_props[$prop] = $value;
}
public function offsetGet($prop) {
if (array_key_exists($prop, $this->_props)) {
return $this->_props[$prop];
}
$this->warning("['$prop'] requested but not defined");
}
public function offsetExists($prop) {
if (!array_key_exists($prop, $this->_props)) {
$this->warning("['$prop'] offsetExists requested but not defined");
return FALSE;
}
return TRUE;
}
public function __isset($prop) {
if (!array_key_exists($prop, $this->_props)) {
$this->warning("isset(->$prop) but not defined");
}
return isset($this->_props[$prop]);
}
public function offsetSet($prop, $value) {
$this->warning("['$prop'] offsetSet");
$this->_props[$prop] = $value;
}
public function offsetUnset($prop) {
$this->warning("['$prop'] offsetUnset");
unset($this->_props[$prop]);
}
/**
* Implement JsonSerializable
*/
public function jsonSerialize() {
return $this->_props;
}
}
/**
* Stubs a method by returning a value from a map.
*/
class ValueMapOrDie implements \PHPUnit\Framework\MockObject\Stub {
protected $valueMap;
public function __construct(array $valueMap) {
$this->valueMap = $valueMap;
}
public function invoke(PHPUnit\Framework\MockObject\Invocation $invocation) {
// This is functionally identical to phpunit 6's ReturnValueMap
$params = $invocation->getParameters();
$parameterCount = \count($params);
foreach ($this->valueMap as $map) {
if (!\is_array($map) || $parameterCount !== (\count($map) - 1)) {
continue;
}
$return = \array_pop($map);
if ($params === $map) {
return $return;
}
}
// ...until here, where we throw an exception if not found.
throw new \InvalidArgumentException("Mock called with unexpected arguments: "
. $invocation->toString());
}
public function toString(): string {
return 'return value from a map or throw InvalidArgumentException';
}
}
......@@ -173,6 +173,10 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles
if (array_key_exists('trxn_id', $ret)) {
$this->trxn_id = $ret['trxn_id'];
$contribution = new CRM_Contribute_BAO_Contribution();
$contribution->id = $params['contribution_id'];
$contribution->trxn_id = $ret['trxn_id'];
$contribution->save();
}
if (array_key_exists('contributionRecurID', $ret)) {
// Get processor id.
......@@ -206,6 +210,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles
$this->assertTrue($found, 'Assigned trxn_id is valid.');
}
/**
* Create contribition
*/
......@@ -224,4 +229,79 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles
$this->contributionID = $contribution['id'];
}
/**
* Sugar for checking things on the contribution.
*
* @param array $expectations key => value pairs.
* @param mixed $contribution
* - if null, use this->contributionID
* - if array, assume it's the result of a contribution.getsingle
* - if int, load that contrib.
*/
protected function checkContrib(array $expectations, $contribution = NULL) {
if (!empty($expectations['contribution_status_id'])) {
$expectations['contribution_status_id'] = CRM_Core_PseudoConstant::getKey(
'CRM_Contribute_BAO_Contribution', 'contribution_status_id', $expectations['contribution_status_id']);
}
if (!is_array($contribution)) {
$contributionID = $contribution ?? $this->contributionID;
$this->assertGreaterThan(0, $contributionID);
$contribution = \Civi\Api4\Contribution::get(FALSE)
->addWhere('id', '=', $contributionID)
->execute()
->first();
}
foreach ($expectations as $field => $expect) {
$this->assertArrayHasKey($field, $contribution);
$this->assertEquals($expect, $contribution[$field], "Expected Contribution.$field = " . json_encode($expect));
}
}
/**
* Sugar for checking things on the contribution recur.
*/
protected function checkContribRecur(array $expectations) {
if (!empty($expectations['contribution_status_id'])) {
$expectations['contribution_status_id'] = CRM_Core_PseudoConstant::getKey(
'CRM_Contribute_BAO_ContributionRecur', 'contribution_status_id', $expectations['contribution_status_id']);
}
$this->assertGreaterThan(0, $this->contributionRecurID);
$contributionRecur = \Civi\Api4\ContributionRecur::get(FALSE)
->addWhere('id', '=', $this->contributionRecurID)
->execute()
->first();
foreach ($expectations as $field => $expect) {
$this->assertArrayHasKey($field, $contributionRecur);
$this->assertEquals($expect, $contributionRecur[$field]);
}
}
/**
* Sugar for checking things on the payment (financial_trxn).
*
* @param array $expectations key => value pairs.
* @param int $contributionID
* - if null, use this->contributionID
* - Retrieve the payment(s) linked to the contributionID (currently expects one payment only)
*/
protected function checkPayment(array $expectations, $contributionID = NULL) {
if (!empty($expectations['contribution_status_id'])) {
$expectations['contribution_status_id'] = CRM_Core_PseudoConstant::getKey(
'CRM_Contribute_BAO_Contribution', 'contribution_status_id', $expectations['contribution_status_id']);
}
$contributionID = $contributionID ?? $this->contributionID;
$this->assertGreaterThan(0, $contributionID);
// We (currently) only support the first payment if there are multiple
$payment = civicrm_api3('Payment', 'get', ['contribution_id' => $contributionID])['values'];
$payment = reset($payment);
foreach ($expectations as $field => $expect) {
$this->assertArrayHasKey($field, $payment);
$this->assertEquals($expect, $payment[$field], "Expected Payment.$field = " . json_encode($expect));
}
}
}
......@@ -76,7 +76,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
'contribution_status_id' => 'Completed',
'trxn_id' => 'in_mock,ch_mock',
]);
$this->checkContribRecur([ 'contribution_status_id' => 'In Progress' ]);
$this->checkContribRecur(['contribution_status_id' => 'In Progress']);
}
/**
* Test creating a recurring contribution and
......@@ -681,49 +681,6 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
$this->contributionID = $contributionRecur['values']['0']['api.contribution.create']['id'];
}
/**
* Sugar for checking things on the contribution.
*
* @param array $expectations key => value pairs.
* @param mixed $contribution
* - if null, use this->contributionID
* - if array, assume it's the result of a contribution.getsingle
* - if int, load that contrib.
*/
protected function checkContrib(array $expectations, $contribution = NULL) {
if (!empty($expectations['contribution_status_id'])) {
$expectations['contribution_status_id'] = CRM_Core_PseudoConstant::getKey(
'CRM_Contribute_BAO_Contribution', 'contribution_status_id', $expectations['contribution_status_id']);
}
if (!is_array($contribution)) {
$contributionID = $contribution ?? $this->contributionID;
$this->assertGreaterThan(0, $contributionID);
$contribution = civicrm_api3('Contribution', 'getsingle', ['id' => $contributionID]);
}
foreach ($expectations as $field => $expect) {
$this->assertArrayHasKey($field, $contribution);
$this->assertEquals($expect, $contribution[$field], "Expected Contribution.$field = " . json_encode($expect));
}
}
/**
* Sugar for checking things on the contribution recur.
*/
protected function checkContribRecur(array $expectations) {
if (!empty($expectations['contribution_status_id'])) {
$expectations['contribution_status_id'] = CRM_Core_PseudoConstant::getKey(
'CRM_Contribute_BAO_ContributionRecur', 'contribution_status_id', $expectations['contribution_status_id']);
}
$this->assertGreaterThan(0, $this->contributionRecurID);
$contribution = civicrm_api3('ContributionRecur', 'getsingle', ['id' => $this->contributionRecurID]);
foreach ($expectations as $field => $expect) {
$this->assertArrayHasKey($field, $contribution);
$this->assertEquals($expect, $contribution[$field]);
}
}
/**
* Returns an array of arrays of contributions.
*/
......@@ -864,7 +821,6 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
->method('retrieve')
->willReturn($mockPlan);
// Need a mock intent with id and status
$mockCharge = $this->createMock('Stripe\\Charge');
$mockCharge
......@@ -875,23 +831,27 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
['status', 'succeeded'],
['balance_transaction', 'txn_mock'],
]));
$mockChargesCollection = new \Stripe\Collection();
$mockChargesCollection->data = [$mockCharge];
$mockPaymentIntent = $this->createMock('Stripe\\PaymentIntent');
$mockPaymentIntent
->method('__get')
->will($this->returnValueMap([
['id', 'pi_mock'],
['status', 'succeeded'],
['charges', (object) ['data' => [ $mockCharge ]]]
['charges', $mockChargesCollection]
]));
$mockSubscription = new PropertySpy('subscription.create', [
'id' => 'sub_mock',
'object' => 'subscription',
'current_period_end' => time()+60*60*24,
'pending_setup_intent' => '',
'latest_invoice' => [
'id' => 'in_mock',
'payment_intent' => $mockPaymentIntent,
],
'pending_setup_intent' => '',
]);
$stripeClient->subscriptions = $this->createMock('Stripe\\Service\\SubscriptionService');
$stripeClient->subscriptions
......@@ -902,6 +862,15 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest {
->with($this->equalTo('sub_mock'))
->willReturn($mockSubscription);
$stripeClient->paymentIntents = $this->createMock('Stripe\\Service\\PaymentIntentService');
$stripeClient->paymentIntents
->method('retrieve')
->with($this->equalTo('pi_mock'))
->willReturn($mockPaymentIntent);
$stripeClient->paymentIntents
->method('update')
->willReturn($mockPaymentIntent);
$stripeClient->balanceTransactions = $this->createMock('Stripe\\Service\\BalanceTransactionService');
$stripeClient->balanceTransactions
->method('retrieve')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment