Commit 20ef4d93 authored by mattwire's avatar mattwire

Refactoring and resolve various issues so we can now submit a single payment via PaymentIntents

parent a6536825
This diff is collapsed.
This diff is collapsed.
<?php
/**
* Shared payment IPN functions that should one day be migrated to CiviCRM core
* Version: 20190304
*/
trait CRM_Core_Payment_StripeIPNTrait {
/**
* @var array Payment processor
*/
private $_paymentProcessor;
/**
* Get the payment processor
* The $_GET['processor_id'] value is set by CRM_Core_Payment::handlePaymentMethod.
*/
protected function getPaymentProcessor() {
$paymentProcessorId = (int) CRM_Utils_Array::value('processor_id', $_GET);
if (empty($paymentProcessorId)) {
$this->exception('Failed to get payment processor id');
}
try {
$this->_paymentProcessor = \Civi\Payment\System::singleton()->getById($paymentProcessorId)->getPaymentProcessor();
}
catch(Exception $e) {
$this->exception('Failed to get payment processor');
}
}
/**
* Mark a contribution as cancelled and update related entities
*
* @param array $params [ 'id' -> contribution_id, 'payment_processor_id' -> payment_processor_id]
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function canceltransaction($params) {
return $this->incompletetransaction($params, 'cancel');
}
/**
* Mark a contribution as failed and update related entities
*
* @param array $params [ 'id' -> contribution_id, 'payment_processor_id' -> payment_processor_id]
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function failtransaction($params) {
return $this->incompletetransaction($params, 'fail');
}
/**
* Handler for failtransaction and canceltransaction - do not call directly
*
* @param array $params
* @param string $mode
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function incompletetransaction($params, $mode) {
$requiredParams = ['id', 'payment_processor_id'];
foreach ($requiredParams as $required) {
if (!isset($params[$required])) {
$this->exception('canceltransaction: Missing mandatory parameter: ' . $required);
}
}
if (isset($params['payment_processor_id'])) {
$input['payment_processor_id'] = $params['payment_processor_id'];
}
$contribution = new CRM_Contribute_BAO_Contribution();
$contribution->id = $params['id'];
if (!$contribution->find(TRUE)) {
throw new CiviCRM_API3_Exception('A valid contribution ID is required', 'invalid_data');
}
if (!$contribution->loadRelatedObjects($input, $ids, TRUE)) {
throw new CiviCRM_API3_Exception('failed to load related objects');
}
$input['trxn_id'] = !empty($params['trxn_id']) ? $params['trxn_id'] : $contribution->trxn_id;
if (!empty($params['fee_amount'])) {
$input['fee_amount'] = $params['fee_amount'];
}
$objects['contribution'] = &$contribution;
$objects = array_merge($objects, $contribution->_relatedObjects);
$transaction = new CRM_Core_Transaction();
switch ($mode) {
case 'cancel':
return $this->cancelled($objects, $transaction);
case 'fail':
return $this->failed($objects, $transaction);
default:
throw new CiviCRM_API3_Exception('Unknown incomplete transaction type: ' . $mode);
}
}
}
<?php
/**
* Shared payment functions that should one day be migrated to CiviCRM core
*/
trait CRM_Core_Payment_StripeTrait {
/**********************
* Version 20190313
*********************/
/**
* Get the billing email address
*
* @param array $params
* @param int $contactId
*
* @return string|NULL
*/
protected function getBillingEmail($params, $contactId) {
$billingLocationId = CRM_Core_BAO_LocationType::getBilling();
$emailAddress = CRM_Utils_Array::value("email-{$billingLocationId}", $params,
CRM_Utils_Array::value('email-Primary', $params,
CRM_Utils_Array::value('email', $params, NULL)));
if (empty($emailAddress) && !empty($contactId)) {
// Try and retrieve an email address from Contact ID
try {
$emailAddress = civicrm_api3('Email', 'getvalue', array(
'contact_id' => $contactId,
'return' => ['email'],
));
}
catch (CiviCRM_API3_Exception $e) {
return NULL;
}
}
return $emailAddress;
}
/**
* Get the contact id
*
* @param array $params
*
* @return int ContactID
*/
protected function getContactId($params) {
// contactID is set by: membership payment workflow
$contactId = CRM_Utils_Array::value('contactID', $params,
CRM_Utils_Array::value('contact_id', $params,
CRM_Utils_Array::value('cms_contactID', $params,
CRM_Utils_Array::value('cid', $params, NULL
))));
if (!empty($contactId)) {
return $contactId;
}
// FIXME: Ref: https://lab.civicrm.org/extensions/stripe/issues/16
// The problem is that when registering for a paid event, civicrm does not pass in the
// contact id to the payment processor (civicrm version 5.3). So, I had to patch your
// getContactId to check the session for a contact id. It's a hack and probably should be fixed in core.
// The code below is exactly what CiviEvent does, but does not pass it through to the next function.
$session = CRM_Core_Session::singleton();
return $session->get('transaction.userID', NULL);
}
/**
* Get the contribution ID
*
* @param $params
*
* @return mixed
*/
protected function getContributionId($params) {
/*
* contributionID is set in the contribution workflow
* We do NOT have a contribution ID for event and membership payments as they are created after payment!
* See: https://github.com/civicrm/civicrm-core/pull/13763 (for events)
*/
return CRM_Utils_Array::value('contributionID', $params);
}
/**
* Get the recurring contribution ID from parameters passed in to cancelSubscription
* Historical the data passed to cancelSubscription is pretty poor and doesn't include much!
*
* @param array $params
*
* @return int|null
*/
protected function getRecurringContributionId($params) {
// Not yet passed, but could be added via core PR
$contributionRecurId = CRM_Utils_Array::value('contribution_recur_id', $params);
if (!empty($contributionRecurId)) {
return $contributionRecurId;
}
// Not yet passed, but could be added via core PR
$contributionId = CRM_Utils_Array::value('contribution_id', $params);
try {
return civicrm_api3('Contribution', 'getvalue', ['id' => $contributionId, 'return' => 'contribution_recur_id']);
}
catch (Exception $e) {
$subscriptionId = CRM_Utils_Array::value('subscriptionId', $params);
if (!empty($subscriptionId)) {
try {
return civicrm_api3('ContributionRecur', 'getvalue', ['processor_id' => $subscriptionId, 'return' => 'id']);
}
catch (Exception $e) {
return NULL;
}
}
return NULL;
}
}
/**
*
* @param array $params ['name' => payment instrument name]
*
* @return int|null
* @throws \CiviCRM_API3_Exception
*/
public static function createPaymentInstrument($params) {
$mandatoryParams = ['name'];
foreach ($mandatoryParams as $value) {
if (empty($params[$value])) {
Civi::log()->error('createPaymentInstrument: Missing mandatory parameter: ' . $value);
return NULL;
}
}
// Create a Payment Instrument
// See if we already have this type
$paymentInstrument = civicrm_api3('OptionValue', 'get', array(
'option_group_id' => "payment_instrument",
'name' => $params['name'],
));
if (empty($paymentInstrument['count'])) {
// Otherwise create it
try {
$financialAccount = civicrm_api3('FinancialAccount', 'getsingle', [
'financial_account_type_id' => "Asset",
'name' => "Payment Processor Account",
]);
}
catch (Exception $e) {
$financialAccount = civicrm_api3('FinancialAccount', 'getsingle', [
'financial_account_type_id' => "Asset",
'name' => "Payment Processor Account",
'options' => ['limit' => 1, 'sort' => "id ASC"],
]);
}
$paymentParams = [
'option_group_id' => "payment_instrument",
'name' => $params['name'],
'description' => $params['name'],
'financial_account_id' => $financialAccount['id'],
];
$paymentInstrument = civicrm_api3('OptionValue', 'create', $paymentParams);
$paymentInstrumentId = $paymentInstrument['values'][$paymentInstrument['id']]['value'];
}
else {
$paymentInstrumentId = $paymentInstrument['id'];
}
return $paymentInstrumentId;
}
}
<?php
/**
* https://civicrm.org/licensing
*/
/**
* Class CRM_Stripe_AJAX
*/
class CRM_Stripe_AJAX {
public static function getClientSecret() {
$amount = CRM_Utils_Request::retrieveValue('amount', 'Money', NULL, TRUE);
$currency = CRM_Utils_Request::retrieveValue('currency', 'String', CRM_Core_Config::singleton()->defaultCurrency);
$processorID = CRM_Utils_Request::retrieveValue('id', 'Integer', NULL, TRUE);
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $processorID]));
$processor->setAPIParams();
$intent = \Stripe\PaymentIntent::create([
'amount' => $processor->getAmount(['amount' => $amount]),
'currency' => $currency,
]);
CRM_Utils_JSON::output(['client_secret' => $intent->client_secret]);
}
public static function confirmPayment() {
$paymentMethodID = CRM_Utils_Request::retrieveValue('payment_method_id', 'String', NULL, TRUE);
$paymentIntentID = CRM_Utils_Request::retrieveValue('payment_intent_id', 'String');
$amount = CRM_Utils_Request::retrieveValue('amount', 'Money', NULL, TRUE);
$currency = CRM_Utils_Request::retrieveValue('currency', 'String', CRM_Core_Config::singleton()->defaultCurrency);
$processorID = CRM_Utils_Request::retrieveValue('id', 'Integer', NULL, TRUE);
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $processorID]));
$processor->setAPIParams();
if ($paymentIntentID) {
$intent = \Stripe\PaymentIntent::retrieve($paymentIntentID);
$intent->confirm([
'payment_method' => $paymentMethodID,
]);
}
else {
$intent = \Stripe\PaymentIntent::create([
'payment_method' => $paymentMethodID,
'amount' => $processor->getAmount(['amount' => $amount]),
'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' => TRUE,
]);
}
self::generatePaymentResponse($intent);
}
private static function generatePaymentResponse($intent) {
if ($intent->status == 'requires_action' &&
$intent->next_action->type == 'use_stripe_sdk') {
// Tell the client to handle the action
CRM_Utils_JSON::output([
'requires_action' => true,
'payment_intent_client_secret' => $intent->client_secret,
//'payment_method_id' => $intent->payment_method,
]);
} else if ($intent->status == 'requires_capture') {
// The payment intent has been confirmed, we just need to capture the payment
// Handle post-payment fulfillment
CRM_Utils_JSON::output([
'success' => true,
'paymentIntent' => ['id' => $intent->id],
]);
} else {
// Invalid status
CRM_Utils_JSON::output(['error' => ['message' => 'Invalid PaymentIntent status']]);
}
}
}
......@@ -24,7 +24,7 @@ class CRM_Stripe_Customer {
1 => [$params['contact_id'], 'String'],
2 => [$params['processor_id'], 'Positive'],
];
return CRM_Core_DAO::singleValueQuery("SELECT id
FROM civicrm_stripe_customers
......@@ -108,13 +108,14 @@ class CRM_Stripe_Customer {
}
/**
* @param $params
* @param array $params
* @param \CRM_Core_Payment_Stripe $stripe
*
* @return \Stripe\ApiResource
* @throws \CiviCRM_API3_Exception
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public static function create($params) {
public static function create($params, $stripe) {
$requiredParams = ['contact_id', 'card_token', 'processor_id'];
// $optionalParams = ['email'];
foreach ($requiredParams as $required) {
......@@ -140,7 +141,7 @@ class CRM_Stripe_Customer {
}
catch (Exception $e) {
$err = CRM_Core_Payment_Stripe::parseStripeException('create_customer', $e, FALSE);
$errorMessage = CRM_Core_Payment_Stripe::handleErrorNotification($err, $params['stripe_error_url']);
$errorMessage = $stripe->handleErrorNotification($err, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to create Stripe Customer: ' . $errorMessage);
}
......
......@@ -20,6 +20,7 @@ class CRM_Stripe_Webhook {
$result = civicrm_api3('PaymentProcessor', 'get', [
'class_name' => 'Payment_Stripe',
'is_active' => 1,
'domain_id' => CRM_Core_Config::domainID(),
]);
foreach ($result['values'] as $paymentProcessor) {
......@@ -37,10 +38,7 @@ class CRM_Stripe_Webhook {
$messages[] = new CRM_Utils_Check_Message(
'stripe_webhook',
$error,
E::ts('Stripe Payment Processor: %1 (%2)', [
1 => $paymentProcessor['name'],
2 => $paymentProcessor['id'],
]),
self::getTitle($paymentProcessor),
\Psr\Log\LogLevel::ERROR,
'fa-money'
);
......@@ -93,9 +91,12 @@ class CRM_Stripe_Webhook {
}
}
else {
$messageTexts[] = E::ts('Stripe Webhook missing! Please visit <a href="%1">Fix Stripe Webhook</a> to fix.', [
1 => CRM_Utils_System::url('civicrm/stripe/fix-webhook'),
]);
$messageTexts[] = E::ts('Stripe Webhook missing! Please visit <a href="%1">Fix Stripe Webhook</a> to fix.<br />Expected webhook path is: <a href="%2" target="_blank">%2</a>',
[
1 => CRM_Utils_System::url('civicrm/stripe/fix-webhook'),
2 => $webhook_path,
]
);
}
}
......@@ -103,10 +104,7 @@ class CRM_Stripe_Webhook {
$messages[] = new CRM_Utils_Check_Message(
'stripe_webhook',
$messageText,
E::ts('Stripe Payment Processor Webhook: %1 (%2)', [
1 => $paymentProcessor['name'],
2 => $paymentProcessor['id'],
]),
self::getTitle($paymentProcessor),
\Psr\Log\LogLevel::WARNING,
'fa-money'
);
......@@ -114,6 +112,22 @@ class CRM_Stripe_Webhook {
}
}
/**
* Get the error message title for the system check
* @param array $paymentProcessor
*
* @return string
*/
private static function getTitle($paymentProcessor) {
if (!empty($paymentProcessor['is_test'])) {
$paymentProcessor['name'] .= ' (test)';
}
return E::ts('Stripe Payment Processor: %1 (%2)', [
1 => $paymentProcessor['name'],
2 => $paymentProcessor['id'],
]);
}
/**
* Create a new webhook for payment processor
*
......
......@@ -258,8 +258,7 @@ function civicrm_api3_stripe_customer_updatestripemetadata($params) {
throw new CiviCRM_API3_Exception('Could not find contact ID for stripe customer: ' . $customerId);
}
$paymentProcessor = \Civi\Payment\System::singleton()
->getById($customerParams['processor_id']);
$paymentProcessor = \Civi\Payment\System::singleton()->getById($customerParams['processor_id']);
$paymentProcessor->setAPIParams();
// Get the stripe customer from stripe
......@@ -267,7 +266,7 @@ function civicrm_api3_stripe_customer_updatestripemetadata($params) {
$stripeCustomer = \Stripe\Customer::retrieve($customerId);
} catch (Exception $e) {
$err = CRM_Core_Payment_Stripe::parseStripeException('retrieve_customer', $e, FALSE);
$errorMessage = CRM_Core_Payment_Stripe::handleErrorNotification($err, NULL);
$errorMessage = $paymentProcessor->handleErrorNotification($err, NULL);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to retrieve Stripe Customer: ' . $errorMessage);
}
......
<?xml version="1.0"?>
<extension key="com.drastikbydesign.stripe" type="module">
<file>stripe</file>
<name>Stripe</name>
<name>Stripe (SCA payments development version)</name>
<description>Stripe Payment Processor</description>
<urls>
<url desc="Main Extension Page">https://lab.civicrm.org/extensions/stripe</url>
......@@ -12,15 +12,18 @@
<author>Matthew Wire (MJW Consulting)</author>
<email>mjw@mjwconsult.co.uk</email>
</maintainer>
<releaseDate>2019-08-31</releaseDate>
<version>6.0.dev</version>
<develStage>beta</develStage>
<releaseDate>2019-09-03</releaseDate>
<version>6.0.alpha1</version>
<develStage>alpha</develStage>
<compatibility>
<ver>5.13</ver>
</compatibility>
<comments>Original Author: Joshua Walker (drastik) - Drastik by Design.
Jamie Mcclelland (ProgressiveTech) did a lot of the 5.x compatibility work.
</comments>
<requires>
<ext>mjwshared</ext>
</requires>
<civix>
<namespace>CRM/Stripe</namespace>
</civix>
......
This diff is collapsed.
<?php
use CRM_Stripe_ExtensionUtil as E;
return [
'stripe_jsdebug' => [
'name' => 'stripe_jsdebug',
'type' => 'Boolean',
'html_type' => 'checkbox',
'default' => 0,
'add' => '5.13',
'is_domain' => 1,
'is_contact' => 0,
'title' => E::ts('Enable Stripe Javascript debugging?'),
'description' => E::ts('Enables debug logging to browser console for stripe payment processors.'),
'html_attributes' => [],
'settings_pages' => [
'stripe' => [
'weight' => 10,
]
],
]
];
......@@ -73,6 +73,13 @@ function stripe_civicrm_managed(&$entities) {
_stripe_civix_civicrm_managed($entities);
}
/**
* Implements hook_civicrm_alterSettingsFolders().
*/
function stripe_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
_stripe_civix_civicrm_alterSettingsFolders($metaDataFolders);
}
/**
* Implementation of hook_civicrm_validateForm().
*
......@@ -170,3 +177,18 @@ function stripe_civicrm_buildForm($formName, &$form) {
function stripe_civicrm_check(&$messages) {
CRM_Stripe_Webhook::check($messages);
}
/**
* Implements hook_civicrm_navigationMenu().
*/
function stripe_civicrm_navigationMenu(&$menu) {
_stripe_civix_insert_navigation_menu($menu, 'Administer/CiviContribute', array(
'label' => E::ts('Stripe Settings'),
'name' => 'stripe_settings',
'url' => 'civicrm/admin/setting/stripe',
'permission' => 'administer CiviCRM',
'operator' => 'OR',
'separator' => 0,
));
_stripe_civix_navigationMenu($menu);
}
{* https://civicrm.org/licensing *}
<script src="https://js.stripe.com/v3/"></script>
<label for="card-element">
<legend>Credit or debit card</legend>
......
......@@ -6,4 +6,22 @@
<title>Check and Fix Stripe Webhooks</title>
<access_arguments>access CiviCRM</access_arguments>
</item>
<item>
<path>civicrm/stripe/client-secret</path>
<page_callback>CRM_Stripe_AJAX::getClientSecret</page_callback>
<title>Client Secret</title>
<access_callback>1</access_callback>
</item>
<item>
<path>civicrm/stripe/confirm-payment</path>
<page_callback>CRM_Stripe_AJAX::confirmPayment</page_callback>
<title>Client Secret</title>
<access_callback>1</access_callback>
</item>
<item>
<path>civicrm/admin/setting/stripe</path>
<title>Stripe Settings</title>
<page_callback>CRM_Admin_Form_Generic</page_callback>
<access_arguments>administer CiviCRM</access_arguments>
</item>
</menu>
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