Commit ea6870c5 authored by Jamie McClelland's avatar Jamie McClelland Committed by mattwire

make payment intent process an api call, include tests

This merge requests also fixes the previously broken tests.
parent 542a0f05
......@@ -817,9 +817,9 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
'paymentintent_id' => $intent->id,
'payment_processor_id' => $this->_paymentProcessor['id'],
'status' => $intent->status,
'contribution_id' => $params['contributionID'],
'contribution_id' => $params['contributionID'] ?? NULL,
'description' => $this->getDescription($params, 'description'),
'identifier' => $params['qfKey'],
'identifier' => $params['qfKey'] ?? NULL,
'contact_id' => $params['contactID'],
];
if (empty($intentParams['contribution_id'])) {
......
......@@ -9,6 +9,8 @@
+--------------------------------------------------------------------+
*/
use CRM_Stripe_ExtensionUtil as E;
/**
* StripePaymentintent.create API
*
......@@ -44,3 +46,144 @@ function civicrm_api3_stripe_paymentintent_delete($params) {
function civicrm_api3_stripe_paymentintent_get($params) {
return _civicrm_api3_basic_get('CRM_Stripe_BAO_StripePaymentintent', $params, TRUE, 'StripePaymentintent');
}
/**
* StripePaymentintent.process API specification
*
* @param array $spec description of fields supported by this API call
*
* @return void
*/
function _civicrm_api3_stripe_paymentintent_process_spec(&$spec) {
$spec['payment_method_id']['title'] = E::ts("Stripe generated code used to create a payment intent.");
$spec['payment_method_id']['type'] = CRM_Utils_Type::T_STRING;
$spec['payment_method_id']['api.default'] = NULL;
$spec['payment_intent_id']['title'] = ts("The payment intent id itself, if available.");
$spec['payment_intent_id']['type'] = CRM_Utils_Type::T_STRING;
$spec['payment_intent_id']['api.default'] = NULL;
$spec['amount']['title'] = ts("The payment amount.");
$spec['amount']['type'] = CRM_Utils_Type::T_STRING;
$spec['amount']['api.default'] = NULL;
$spec['capture']['title'] = ts("Whether we should try to capture the amount, not just confirm it.");
$spec['capture']['type'] = CRM_Utils_Type::T_BOOLEAN;
$spec['capture']['api.default'] = FALSE;
$spec['description']['title'] = ts("Describe the payment.");
$spec['description']['type'] = CRM_Utils_Type::T_STRING;
$spec['description']['api.default'] = NULL;
$spec['currency']['title'] = ts("Whether we should try to capture the amount, not just confirm it.");
$spec['currency']['type'] = CRM_Utils_Type::T_STRING;
$spec['currency']['api.default'] = CRM_Core_Config::singleton()->defaultCurrency;
$spec['payment_processor_id']['title'] = ts("The stripe payment processor id.");
$spec['payment_processor_id']['type'] = CRM_Utils_Type::T_INT;
$spec['payment_processor_id']['api.required'] = TRUE;
}
/**
* StripePaymentintent.process API
*
* In the normal flow of a CiviContribute form, this will be called with a
* payment_method_id (which is generated by Stripe via its javascript code),
* in which case it will create a PaymentIntent using that and *attempt* to
* 'confirm' it.
*
* This can also be called with a payment_intent_id instead, in which case it
* will retrieve the PaymentIntent and attempt (again) to 'confirm' it. This
* is useful to confirm funds after a user has completed SCA in their
* browser.
*
* 'confirming' a PaymentIntent refers to the process by which the funds are
* reserved in the cardholder's account, but not actually taken yet.
*
* Taking the funds ('capturing') should go through without problems once the
* transaction has been confirmed - this is done later on in the process.
*
* Nb. confirmed funds are released and will become available to the
* cardholder again if the PaymentIntent is cancelled or is not captured
* within 1 week.
*
* @param array $params
*
* @return array API result descriptor
* @throws \API_Exception
* @throws \CiviCRM_API3_Exception
* @throws \Stripe\Error\Api
*/
function civicrm_api3_stripe_paymentintent_process($params) {
$paymentMethodID = $params['payment_method_id'];
$paymentIntentID = $params['payment_intent_id'];
$amount = $params['amount'];
$capture = $params['capture'];
$title = $params['description'];
$confirm = TRUE;
if (empty($amount)) {
$amount = 1;
$confirm = FALSE;
}
$currency = $params['currency'];
$processorID = $params['payment_processor_id'];
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $processorID]));
$processor->setAPIParams();
if ($paymentIntentID) {
// We already have a PaymentIntent, retrieve and attempt confirm.
$intent = \Stripe\PaymentIntent::retrieve($paymentIntentID);
if ($intent->status === 'requires_confirmation') {
$intent->confirm();
}
if ($capture && $intent->status === 'requires_capture') {
$intent->capture();
}
}
else {
// We don't yet have a PaymentIntent, create one using the
// Payment Method ID and attempt to confirm it too.
$intent = \Stripe\PaymentIntent::create([
'payment_method' => $paymentMethodID,
'amount' => $processor->getAmount(['amount' => $amount, 'currency' => $currency]),
'currency' => $currency,
'confirmation_method' => 'manual',
'capture_method' => 'manual',
// authorize the amount but don't take from card yet
'setup_future_usage' => 'off_session',
// Setup the card to be saved and used later
'confirm' => $confirm,
]);
}
// Save the generated paymentIntent in the CiviCRM database for later tracking
$intentParams = [
'paymentintent_id' => $intent->id,
'payment_processor_id' => $processorID,
'status' => $intent->status,
'description' => $title,
];
CRM_Stripe_BAO_StripePaymentintent::create($intentParams);
if ($intent->status === 'requires_action' &&
$intent->next_action->type === 'use_stripe_sdk') {
// Tell the client to handle the action
return civicrm_api3_create_success([
'requires_action' => true,
'payment_intent_client_secret' => $intent->client_secret,
]);
}
elseif (($intent->status === 'requires_capture') || ($intent->status === 'requires_confirmation')) {
// paymentIntent = requires_capture / requires_confirmation
// The payment intent has been confirmed, we just need to capture the payment
// Handle post-payment fulfillment
return civicrm_api3_create_success([
'success' => true,
'paymentIntent' => ['id' => $intent->id],
]);
}
elseif ($intent->status === 'succeeded') {
return civicrm_api3_create_success([
'success' => true,
'paymentIntent' => ['id' => $intent->id],
]);
}
else {
// Invalid status
throw new API_Exception('Invalid PaymentIntent status');
}
}
This diff is collapsed.
......@@ -14,25 +14,13 @@ use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* FIXME - Add test description.
*
* Tips:
* - With HookInterface, you may implement CiviCRM hooks directly in the test class.
* Simply create corresponding functions (e.g. "hook_civicrm_post(...)" or similar).
* - With TransactionalInterface, any data changes made by setUp() or test****() functions will
* rollback automatically -- as long as you don't manipulate schema or truncate tables.
* If this test needs to manipulate schema or truncate tables, then either:
* a. Do all that using setupHeadless() and Civi\Test.
* b. Disable TransactionalInterface, and handle all setup/teardown yourself.
* Test a simple, direct payment via Stripe.
*
* @group headless
*/
require ('BaseTest.php');
class CRM_Stripe_DirectTest extends CRM_Stripe_BaseTest {
protected $_contributionRecurID;
protected $_total = '200';
public function setUpHeadless() {
// Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
// See: https://github.com/civicrm/org.civicrm.testapalooza/blob/master/civi-test.md
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment