Commit 20ef4d93 authored by mattwire's avatar mattwire
Browse files

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

parent a6536825
This diff is collapsed.
......@@ -6,7 +6,7 @@
class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
use CRM_Core_Payment_StripeIPNTrait;
use CRM_Core_Payment_MJWIPNTrait;
/**
* Transaction ID is the contribution in the redirect flow and a random number in the on-site->POST flow
......@@ -48,7 +48,7 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
protected $amount = NULL;
protected $fee = NULL;
protected $net_amount = NULL;
protected $previous_contribution = [];
protected $contribution = [];
/**
* CRM_Core_Payment_StripeIPN constructor.
......@@ -110,7 +110,7 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
* We override base because our input parameter is an object
*
* @param array $parameters
*/
*/
public function setInputParameters($parameters) {
if (!is_object($parameters)) {
$this->exception('Invalid input parameters');
......@@ -168,7 +168,6 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
public function main() {
// Collect and determine all data about this event.
$this->event_type = CRM_Stripe_Api::getParam('event_type', $this->_inputParameters);
$pendingStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
// NOTE: If you add an event here make sure you add it to the webhook or it will never be received!
......@@ -176,14 +175,14 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
// Successful recurring payment.
case 'invoice.payment_succeeded':
$this->setInfo();
if ($this->previous_contribution['contribution_status_id'] == $pendingStatusId) {
if ($this->contribution['contribution_status_id'] == $pendingStatusId) {
$this->completeContribution();
}
elseif ($this->previous_contribution['trxn_id'] != $this->charge_id) {
elseif ($this->contribution['trxn_id'] != $this->charge_id) {
// The first contribution was completed, so create a new one.
// api contribution repeattransaction repeats the appropriate contribution if it is given
// simply the recurring contribution id. It also updates the membership for us.
civicrm_api3('Contribution', 'repeattransaction', array(
civicrm_api3('Contribution', 'repeattransaction', [
'contribution_recur_id' => $this->contribution_recur_id,
'contribution_status_id' => 'Completed',
'receive_date' => $this->receive_date,
......@@ -191,15 +190,15 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
'total_amount' => $this->amount,
'fee_amount' => $this->fee,
'is_email_receipt' => $this->getSendEmailReceipt(),
));
]);
}
// Successful charge & more to come.
civicrm_api3('ContributionRecur', 'create', array(
civicrm_api3('ContributionRecur', 'create', [
'id' => $this->contribution_recur_id,
'failure_count' => 0,
'contribution_status_id' => 'In Progress'
));
]);
return TRUE;
// Failed recurring payment.
......@@ -207,14 +206,14 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
$this->setInfo();
$failDate = date('YmdHis');
if ($this->previous_contribution['contribution_status_id'] == $pendingStatusId) {
if ($this->contribution['contribution_status_id'] == $pendingStatusId) {
// If this contribution is Pending, set it to Failed.
civicrm_api3('Contribution', 'create', array(
'id' => $this->previous_contribution['id'],
civicrm_api3('Contribution', 'create', [
'id' => $this->contribution['id'],
'contribution_status_id' => "Failed",
'receive_date' => $failDate,
'is_email_receipt' => 0,
));
]);
}
else {
$contributionParams = [
......@@ -227,96 +226,80 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
civicrm_api3('Contribution', 'repeattransaction', $contributionParams);
}
$failureCount = civicrm_api3('ContributionRecur', 'getvalue', array(
'id' => $this->contribution_recur_id,
'return' => 'failure_count',
));
$failureCount = civicrm_api3('ContributionRecur', 'getvalue', [
'id' => $this->contribution_recur_id,
'return' => 'failure_count',
]);
$failureCount++;
// Change the status of the Recurring and update failed attempts.
civicrm_api3('ContributionRecur', 'create', array(
civicrm_api3('ContributionRecur', 'create', [
'id' => $this->contribution_recur_id,
'contribution_status_id' => "Failed",
'failure_count' => $failureCount,
'modified_date' => $failDate,
));
]);
return TRUE;
// Subscription is cancelled
case 'customer.subscription.deleted':
$this->setInfo();
// Cancel the recurring contribution
civicrm_api3('ContributionRecur', 'cancel', array(
civicrm_api3('ContributionRecur', 'cancel', [
'id' => $this->contribution_recur_id,
));
]);
return TRUE;
// One-time donation and per invoice payment.
case 'charge.failed':
$chargeId = $this->retrieve('charge_id', 'String');
$failureCode = $this->retrieve('failure_code', 'String');
$failureMessage = $this->retrieve('failure_message', 'String');
$contribution = civicrm_api3('Contribution', 'getsingle', ['trxn_id' => $chargeId]);
$failedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Failed');
if ($contribution['contribution_status_id'] != $failedStatusId) {
$note = $failureCode . ' : ' . $failureMessage;
civicrm_api3('Contribution', 'create', ['id' => $contribution['id'], 'contribution_status_id' => $failedStatusId, 'note' => $note]);
}
$chargeId = $this->retrieve('charge_id', 'String');
// @fixme: Check if "note" param actually does anything!
$params = [
'note' => "{$failureCode} : {$failureMessage}",
'contribution_id' => civicrm_api3('Contribution', 'getvalue', ['trxn_id' => $chargeId, 'return' => 'id']),
];
$this->recordFailed($params);
return TRUE;
case 'charge.refunded':
$chargeId = $this->retrieve('charge_id', 'String');
$refunded = $this->retrieve('refunded', 'Boolean');
$refundAmount = $this->retrieve('amount_refunded', 'Integer');
$contribution = civicrm_api3('Contribution', 'getsingle', ['trxn_id' => $chargeId]);
if ($refunded) {
$refundedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Refunded');
if ($contribution['contribution_status_id'] != $refundedStatusId) {
civicrm_api3('Contribution', 'create', [
'id' => $contribution['id'],
'contribution_status_id' => $refundedStatusId
]);
}
elseif ($refundAmount > 0) {
$partiallyRefundedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Partially Refunded');
if ($contribution['contribution_status_id'] != $partiallyRefundedStatusId) {
civicrm_api3('Contribution', 'create', [
'id' => $contribution['id'],
'contribution_status_id' => $refundedStatusId
]);
}
}
}
$params = [
'contribution_id' => civicrm_api3('Contribution', 'getvalue', ['trxn_id' => $chargeId, 'return' => 'id']),
'total_amount' => $this->retrieve('amount_refunded', 'Integer'),
];
$this->recordRefund($params);
return TRUE;
case 'charge.succeeded':
$this->setInfo();
if ($this->previous_contribution['contribution_status_id'] == $pendingStatusId) {
$this->completeContribution();
if ($this->contribution['contribution_status_id'] == $pendingStatusId) {
$this->recordCompleted(['contribution_id' => $this->contribution['id']]);
}
return TRUE;
case 'customer.subscription.updated':
$this->setInfo();
if (empty($this->previous_plan_id)) {
// Not a plan change...don't care.
return TRUE;
}
civicrm_api3('ContributionRecur', 'create', [
'id' => $this->contribution_recur_id,
$this->setInfo();
if (empty($this->previous_plan_id)) {
// Not a plan change...don't care.
return TRUE;
}
civicrm_api3('ContributionRecur', 'create', [
'id' => $this->contribution_recur_id,
'amount' => $this->plan_amount,
'auto_renew' => 1,
'created_date' => $this->plan_start,
'frequency_unit' => $this->frequency_unit,
'frequency_interval' => $this->frequency_interval,
]);
]);
civicrm_api3('Contribution', 'create', [
'id' => $this->previous_contribution['id'],
civicrm_api3('Contribution', 'create', [
'id' => $this->contribution['id'],
'total_amount' => $this->plan_amount,
'contribution_recur_id' => $this->contribution_recur_id,
]);
]);
return TRUE;
}
// Unhandled event type.
......@@ -330,15 +313,15 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
*/
public function completeContribution() {
// Update the contribution to include the fee.
civicrm_api3('Contribution', 'create', array(
'id' => $this->previous_contribution['id'],
civicrm_api3('Contribution', 'create', [
'id' => $this->contribution['id'],
'total_amount' => $this->amount,
'fee_amount' => $this->fee,
'net_amount' => $this->net_amount,
));
]);
// The last one was not completed, so complete it.
civicrm_api3('Contribution', 'completetransaction', array(
'id' => $this->previous_contribution['id'],
civicrm_api3('Contribution', 'completetransaction', [
'id' => $this->contribution['id'],
'trxn_date' => $this->receive_date,
'trxn_id' => $this->charge_id,
'total_amount' => $this->amount,
......@@ -346,10 +329,10 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
'fee_amount' => $this->fee,
'payment_processor_id' => $this->_paymentProcessor['id'],
'is_email_receipt' => $this->getSendEmailReceipt(),
));
]);
}
/**
/**
* Gather and set info as class properties.
*
* Given the data passed to us via the Stripe Event, try to determine
......@@ -420,13 +403,13 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
if ($this->contribution_recur_id) {
try {
// Same approach as api repeattransaction.
$contribution = civicrm_api3('contribution', 'getsingle', array(
'return' => array('id', 'contribution_status_id', 'total_amount', 'trxn_id'),
$contribution = civicrm_api3('contribution', 'getsingle', [
'return' => ['id', 'contribution_status_id', 'total_amount', 'trxn_id'],
'contribution_recur_id' => $this->contribution_recur_id,
'contribution_test' => isset($this->_paymentProcessor['is_test']) && $this->_paymentProcessor['is_test'] ? 1 : 0,
'options' => array('limit' => 1, 'sort' => 'id DESC'),
));
$this->previous_contribution = $contribution;
'options' => ['limit' => 1, 'sort' => 'id DESC'],
]);
$this->contribution = $contribution;
}
catch (Exception $e) {
$this->exception('Cannot find any contributions with recurring contribution ID: ' . $this->contribution_recur_id . '. ' . $e->getMessage());
......
<?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);