Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • extensions/stripe
  • madhavi/stripe
  • wmortada/stripe
  • jamie/stripe
  • michaelmcandrew/stripe
  • bgm/stripe
  • noah/stripe
  • laryn/stripe
  • phani/stripe
  • JonGold/stripe
  • scardinius/stripe
  • varshith/stripe
  • naomi/stripe
  • jhoskins98/stripe
  • artfulrobot/stripe
  • jitendra/stripe
  • justinfreeman/stripe
  • revati_gawas/stripe
  • capo/stripe
  • pradeep/stripe
  • partners/ixiam/stripe
  • homotechsual/stripe
  • Edselopez/stripe
  • goron/stripe
  • tapash/stripe
  • petednz/stripe
  • kartik1000/stripe
  • ananelson/stripe
  • Samuele.Masetto/stripe
  • sluc23/stripe
  • aaron/stripe
  • DaveD/stripe
  • konadave/stripe
  • partners/coopsymbiotic/stripe
  • kurund/stripe
  • AllenShaw/stripe
  • awestbha/stripe
  • mathavan/stripe
  • BjoernE/stripe
  • alietz/stripe
  • seamuslee/stripe
  • damo-civi/stripe
  • dmunio/stripe
  • ufundo/stripe
44 results
Show changes
Commits on Source (27)
Showing
with 1109 additions and 1211 deletions
......@@ -3,18 +3,20 @@
* https://civicrm.org/licensing
*/
use CRM_Stripe_ExtensionUtil as E;
/**
* Class CRM_Core_Payment_Stripe
*/
class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
use CRM_Core_Payment_StripeTrait;
use CRM_Core_Payment_MJWTrait;
/**
*
* @var string
*/
const API_VERSION = '2019-05-16';
const API_VERSION = '2019-09-09';
/**
* Mode of operation: live or test.
......@@ -31,13 +33,14 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
*
* @param string $mode
* The mode of operation: live or test.
* @param array $paymentProcessor
*
* @return void
*/
public function __construct($mode, &$paymentProcessor) {
public function __construct($mode, $paymentProcessor) {
$this->_mode = $mode;
$this->_paymentProcessor = $paymentProcessor;
$this->_processorName = ts('Stripe');
$this->_processorName = E::SHORT_NAME;
}
/**
......@@ -46,7 +49,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* @return string
*/
public static function getSecretKey($paymentProcessor) {
return trim(CRM_Utils_Array::value('user_name', $paymentProcessor));
return trim(CRM_Utils_Array::value('password', $paymentProcessor));
}
/**
......@@ -55,7 +58,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* @return string
*/
public static function getPublicKey($paymentProcessor) {
return trim(CRM_Utils_Array::value('password', $paymentProcessor));
return trim(CRM_Utils_Array::value('user_name', $paymentProcessor));
}
/**
......@@ -105,7 +108,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* The error message if any.
*/
public function checkConfig() {
$error = array();
$error = [];
if (!empty($error)) {
return implode('<p>', $error);
......@@ -120,7 +123,9 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* @return bool
*/
public function supportsBackOffice() {
return TRUE;
// @fixme Make this work again with stripe elements / 6.0
return FALSE;
// return TRUE;
}
/**
......@@ -131,8 +136,21 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
return FALSE;
}
public function supportsRecurring() {
return TRUE;
}
/**
* Does this payment processor support refund?
*
* @return bool
*/
public function supportsRefund() {
return TRUE;
}
/**
* We can configure a start date for a smartdebit mandate
* Can we set a future recur start date? Stripe allows this but we don't (yet) support it.
* @return bool
*/
public function supportsFutureRecurStartDate() {
......@@ -173,14 +191,8 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
*
* @return string errorMessage (or statusbounce if URL is specified)
*/
public static function handleErrorNotification($err, $bounceURL = NULL) {
$debugMsg = $err['type'] . ' ' . $err['code'] . ' ' . $err['message'];
Civi::log()->debug('Stripe Payment Error: ' . $debugMsg);
if ($bounceURL) {
CRM_Core_Error::statusBounce($err['message'], $bounceURL, 'Payment Error');
}
return $debugMsg;
public function handleErrorNotification($err, $bounceURL = NULL) {
return self::handleError("{$err['type']} {$err['code']}", $err['message'], $bounceURL);
}
/**
......@@ -215,9 +227,6 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
public function createPlan($params, $amount) {
$currency = strtolower($params['currencyID']);
$planId = "every-{$params['frequency_interval']}-{$params['frequency_unit']}-{$amount}-" . $currency;
if (isset($params['membership_type_tag'])) {
$planId = $params['membership_type_tag'] . $planId;
}
if ($this->_paymentProcessor['is_test']) {
$planId .= '-test';
......@@ -236,19 +245,19 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
if ($this->_paymentProcessor['is_test']) {
$productName .= '-test';
}
$product = \Stripe\Product::create(array(
$product = \Stripe\Product::create([
"name" => $productName,
"type" => "service"
));
]);
// Create a new Plan.
$stripePlan = array(
$stripePlan = [
'amount' => $amount,
'interval' => $params['frequency_unit'],
'product' => $product->id,
'currency' => $currency,
'id' => $planId,
'interval_count' => $params['frequency_interval'],
);
];
$plan = \Stripe\Plan::create($stripePlan);
}
}
......@@ -261,15 +270,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* @return array
*/
public function getPaymentFormFields() {
return array(
'credit_card_type',
'credit_card_number',
'cvv2',
'credit_card_exp_date',
'stripe_token',
'stripe_pub_key',
'stripe_id',
);
return [];
}
/**
......@@ -281,86 +282,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* field metadata
*/
public function getPaymentFormFieldsMetadata() {
$creditCardType = array('' => ts('- select -')) + CRM_Contribute_PseudoConstant::creditCard();
return array(
'credit_card_number' => array(
'htmlType' => 'text',
'name' => 'credit_card_number',
'title' => ts('Card Number'),
'cc_field' => TRUE,
'attributes' => array(
'size' => 20,
'maxlength' => 20,
'autocomplete' => 'off',
),
'is_required' => TRUE,
),
'cvv2' => array(
'htmlType' => 'text',
'name' => 'cvv2',
'title' => ts('Security Code'),
'cc_field' => TRUE,
'attributes' => array(
'size' => 5,
'maxlength' => 10,
'autocomplete' => 'off',
),
'is_required' => TRUE,
),
'credit_card_exp_date' => array(
'htmlType' => 'date',
'name' => 'credit_card_exp_date',
'title' => ts('Expiration Date'),
'cc_field' => TRUE,
'attributes' => CRM_Core_SelectValues::date('creditCard'),
'is_required' => TRUE,
'month_field' => 'credit_card_exp_date_M',
'year_field' => 'credit_card_exp_date_Y',
'extra' => ['class' => 'crm-form-select'],
),
'credit_card_type' => array(
'htmlType' => 'select',
'name' => 'credit_card_type',
'title' => ts('Card Type'),
'cc_field' => TRUE,
'attributes' => $creditCardType,
'is_required' => FALSE,
),
'stripe_token' => array(
'htmlType' => 'hidden',
'name' => 'stripe_token',
'title' => 'Stripe Token',
'attributes' => array(
'id' => 'stripe-token',
'class' => 'payproc-metadata',
),
'cc_field' => TRUE,
'is_required' => TRUE,
),
'stripe_id' => array(
'htmlType' => 'hidden',
'name' => 'stripe_id',
'title' => 'Stripe ID',
'attributes' => array(
'id' => 'stripe-id',
'class' => 'payproc-metadata',
),
'cc_field' => TRUE,
'is_required' => TRUE,
),
'stripe_pub_key' => array(
'htmlType' => 'hidden',
'name' => 'stripe_pub_key',
'title' => 'Stripe Public Key',
'attributes' => array(
'id' => 'stripe-pub-key',
'class' => 'payproc-metadata',
),
'cc_field' => TRUE,
'is_required' => TRUE,
),
);
return [];
}
/**
......@@ -394,14 +316,26 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* @param \CRM_Core_Form $form
*/
public function buildForm(&$form) {
// Set default values
$paymentProcessorId = CRM_Utils_Array::value('id', $form->_paymentProcessor);
$publishableKey = CRM_Core_Payment_Stripe::getPublicKeyById($paymentProcessorId);
$defaults = [
'stripe_id' => $paymentProcessorId,
'stripe_pub_key' => $publishableKey,
$jsVars = [
'id' => $form->_paymentProcessor['id'],
'currency' => $this->getDefaultCurrencyForForm($form),
'billingAddressID' => CRM_Core_BAO_LocationType::getBilling(),
'publishableKey' => CRM_Core_Payment_Stripe::getPublicKeyById($form->_paymentProcessor['id']),
'jsDebug' => TRUE,
];
$form->setDefaults($defaults);
\Civi::resources()->addVars(E::SHORT_NAME, $jsVars);
// Assign to smarty so we can add via Card.tpl for drupal webform because addVars doesn't work in that context
$form->assign('stripeJSVars', $jsVars);
// Add help and javascript
CRM_Core_Region::instance('billing-block')->add(
['template' => 'CRM/Core/Payment/Stripe/Card.tpl', 'weight' => -1]);
// Add CSS via region (it won't load on drupal webform if added via \Civi::resources()->addStyleFile)
CRM_Core_Region::instance('billing-block')->add([
'styleUrl' => \Civi::resources()->getUrl(E::LONG_NAME, 'css/elements.css'),
'weight' => -1,
]);
\Civi::resources()->addScriptFile(E::LONG_NAME, 'js/civicrm_stripe.js');
}
/**
......@@ -422,22 +356,24 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public function doPayment(&$params, $component = 'contribute') {
if (array_key_exists('credit_card_number', $params)) {
$cc = $params['credit_card_number'];
if (!empty($cc) && substr($cc, 0, 8) != '00000000') {
Civi::log()->debug(ts('ALERT! Unmasked credit card received in back end. Please report this error to the site administrator.'));
}
$params = $this->beginDoPayment($params);
// Get the passed in paymentIntent
if(!empty(CRM_Utils_Array::value('paymentIntentID', $params))) {
$paymentIntentID = CRM_Utils_Array::value('paymentIntentID', $params);
}
elseif (CRM_Core_Session::singleton()->get('stripePaymentIntent')) {
// @fixme Hack for contributionpages - see https://github.com/civicrm/civicrm-core/pull/15252
$paymentIntentID = CRM_Core_Session::singleton()->get('stripePaymentIntent');
$params['paymentIntentID'] = $paymentIntentID;
}
else {
CRM_Core_Error::statusBounce(E::ts('Unable to complete payment! Missing paymentIntent ID.'));
Civi::log()->debug('paymentIntentID not found. $params: ' . print_r($params, TRUE));
}
$completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
$pendingStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
// If we have a $0 amount, skip call to processor and set payment_status to Completed.
if (empty($params['amount'])) {
$params['payment_status_id'] = $completedStatusId;
return $params;
}
$this->setAPIParams();
// Get proper entry URL for returning on error.
......@@ -448,162 +384,135 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
}
else {
$qfKey = $params['qfKey'];
$parsed_url = parse_url($params['entryURL']);
$url_path = substr($parsed_url['path'], 1);
$params['stripe_error_url'] = CRM_Utils_System::url($url_path,
$parsed_url['query'] . "&_qf_Main_display=1&qfKey={$qfKey}", FALSE, NULL, FALSE);
$parsedUrl = parse_url($params['entryURL']);
$urlPath = substr($parsedUrl['path'], 1);
$query = $parsedUrl['query'];
if (strpos($query, '_qf_Main_display=1') === FALSE) {
$query .= '&_qf_Main_display=1';
}
if (strpos($query, 'qfKey=') === FALSE) {
$query .= "&qfKey={$qfKey}";
}
$params['stripe_error_url'] = CRM_Utils_System::url($urlPath, $query, FALSE, NULL, FALSE);
}
$amount = self::getAmount($params);
// Use Stripe.js instead of raw card details.
if (!empty($params['stripe_token'])) {
$card_token = $params['stripe_token'];
}
else if(!empty(CRM_Utils_Array::value('stripe_token', $_POST, NULL))) {
$card_token = CRM_Utils_Array::value('stripe_token', $_POST, NULL);
}
else {
CRM_Core_Error::statusBounce(ts('Unable to complete payment! Please this to the site administrator with a description of what you were trying to do.'));
Civi::log()->debug('Stripe.js token was not passed! Report this message to the site administrator. $params: ' . print_r($params, TRUE));
}
$contactId = $this->getContactId($params);
$email = $this->getBillingEmail($params, $contactId);
// See if we already have a stripe customer
$customerParams = [
'contact_id' => $contactId,
'card_token' => $card_token,
'processor_id' => $this->_paymentProcessor['id'],
'email' => $email,
// Include this to allow redirect within session on payment failure
'stripe_error_url' => $params['stripe_error_url'],
];
// Get the Stripe Customer:
// 1. Look for an existing customer.
// 2. If no customer (or a deleted customer found), create a new one.
// 3. If existing customer found, update the metadata that Stripe holds for this customer.
$stripeCustomerId = CRM_Stripe_Customer::find($customerParams);
// Customer not in civicrm database. Create a new Customer in Stripe.
if (!isset($stripeCustomerId)) {
$stripeCustomer = CRM_Stripe_Customer::create($customerParams);
$stripeCustomer = CRM_Stripe_Customer::create($customerParams, $this);
}
else {
// Customer was found in civicrm database, fetch from Stripe.
$deleteCustomer = FALSE;
try {
$stripeCustomer = \Stripe\Customer::retrieve($stripeCustomerId);
}
catch (Exception $e) {
} catch (Exception $e) {
$err = self::parseStripeException('retrieve_customer', $e, FALSE);
if (($err['type'] == 'invalid_request_error') && ($err['code'] == 'resource_missing')) {
$deleteCustomer = TRUE;
}
$errorMessage = self::handleErrorNotification($err, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to create Stripe Charge: ' . $errorMessage);
$errorMessage = $this->handleErrorNotification($err, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to retrieve Stripe Customer: ' . $errorMessage);
}
if ($deleteCustomer || $stripeCustomer->isDeleted()) {
if ($stripeCustomer->isDeleted()) {
// Customer doesn't exist, create a new one
CRM_Stripe_Customer::delete($customerParams);
try {
$stripeCustomer = CRM_Stripe_Customer::create($customerParams);
}
catch (Exception $e) {
$stripeCustomer = CRM_Stripe_Customer::create($customerParams, $this);
} catch (Exception $e) {
// We still failed to create a customer
$errorMessage = self::handleErrorNotification($stripeCustomer, $params['stripe_error_url']);
$errorMessage = $this->handleErrorNotification($stripeCustomer, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to create Stripe Customer: ' . $errorMessage);
}
}
$stripeCustomer->card = $card_token;
try {
$stripeCustomer->save();
}
catch (Exception $e) {
$err = self::parseStripeException('update_customer', $e, TRUE);
if (($err['type'] == 'invalid_request_error') && ($err['code'] == 'token_already_used')) {
// This error is ok, we've already used the token during create_customer
}
else {
$errorMessage = self::handleErrorNotification($err, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to update Stripe Customer: ' . $errorMessage);
}
else {
CRM_Stripe_Customer::updateMetadata($customerParams, $this, $stripeCustomer->id);
}
}
// Prepare the charge array, minus Customer/Card details.
if (empty($params['description'])) {
$params['description'] = ts('Backend Stripe contribution');
}
// Handle recurring payments in doRecurPayment().
if (CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID']) {
// We set payment status as pending because the IPN will set it as completed / failed
$params['payment_status_id'] = $pendingStatusId;
return $this->doRecurPayment($params, $amount, $stripeCustomer);
$params['description'] = E::ts('Contribution: %1', [1 => $this->getPaymentProcessorLabel()]);
}
// Stripe charge.
$stripeChargeParams = [
'amount' => $amount,
'currency' => strtolower($params['currencyID']),
'description' => $params['description'] . ' # Invoice ID: ' . CRM_Utils_Array::value('invoiceID', $params),
$contactContribution = $this->getContactId($params) . '-' . ($this->getContributionId($params) ?: 'XX');
$intentParams = [
'customer' => $stripeCustomer->id,
'description' => "{$params['description']} {$contactContribution} #" . CRM_Utils_Array::value('invoiceID', $params),
'statement_descriptor_suffix' => "{$contactContribution} " . substr($params['description'],0,7),
];
$intentParams['statement_descriptor'] = substr("{$contactContribution} " . $params['description'], 0, 22);
$intentMetadata = [
'amount_to_capture' => $this->getAmount($params),
];
// Use Stripe Customer if we have a valid one. Otherwise just use the card.
if (!empty($stripeCustomer->id)) {
$stripeChargeParams['customer'] = $stripeCustomer->id;
}
else {
$stripeChargeParams['card'] = $card_token;
}
// This is where we actually charge the customer
try {
$stripeCharge = \Stripe\Charge::create($stripeChargeParams);
\Stripe\PaymentIntent::update($paymentIntentID, $intentParams);
$intent = \Stripe\PaymentIntent::retrieve($paymentIntentID);
$intent->customer = $stripeCustomer->id;
switch ($intent->status) {
case 'requires_confirmation':
$intent->confirm();
case 'requires_capture':
$intent->capture($intentMetadata);
break;
}
}
catch (Exception $e) {
$err = self::parseStripeException('charge_create', $e, FALSE);
if ($e instanceof \Stripe\Error\Card) {
if ($this->getContributionId($params)) {
civicrm_api3('Note', 'create', [
'entity_id' => $this->getContributionId($params),
'contact_id' => $this->getContactId($params),
'subject' => $err['type'],
'note' => $err['code'],
'entity_table' => 'civicrm_contribution',
]);
}
}
$errorMessage = self::handleErrorNotification($err, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to create Stripe Charge: ' . $errorMessage);
$this->handleError($e->getCode(), $e->getMessage(), $params['stripe_error_url']);
}
// Handle recurring payments in doRecurPayment().
if (CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID']) {
// This is where we save the customer card
// @todo For a recurring payment we have to save the card. For a single payment we'd like to develop the
// save card functionality but should not save by default as the customer has not agreed.
$paymentMethod = \Stripe\PaymentMethod::retrieve($intent->payment_method);
$paymentMethod->attach(['customer' => $stripeCustomer->id]);
// We set payment status as pending because the IPN will set it as completed / failed
$params['payment_status_id'] = $pendingStatusId;
return $this->doRecurPayment($params, $amount, $stripeCustomer, $paymentMethod);
}
// Return fees & net amount for Civi reporting.
$stripeCharge = $intent->charges->data[0];
try {
$stripeBalanceTransaction = \Stripe\BalanceTransaction::retrieve($stripeCharge->balance_transaction);
}
catch (Exception $e) {
$err = self::parseStripeException('retrieve_balance_transaction', $e, FALSE);
$errorMessage = self::handleErrorNotification($err, $params['stripe_error_url']);
$errorMessage = $this->handleErrorNotification($err, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to retrieve Stripe Balance Transaction: ' . $errorMessage);
}
// Success!
// Set the desired contribution status which will be set later (do not set on the contribution here!)
$params['contribution_status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
// For contribution workflow we have a contributionId so we can set parameters directly.
// For events/membership workflow we have to return the parameters and they might get set...
$newParams['trxn_id'] = $stripeCharge->id;
$newParams['payment_status_id'] = $completedStatusId;
// For a single charge there is no stripe invoice.
$this->setPaymentProcessorOrderID($stripeCharge->id);
$this->setPaymentProcessorTrxnID($stripeCharge->id);
$newParams['fee_amount'] = $stripeBalanceTransaction->fee / 100;
$newParams['net_amount'] = $stripeBalanceTransaction->net / 100;
if ($this->getContributionId($params)) {
$newParams['id'] = $this->getContributionId($params);
civicrm_api3('Contribution', 'create', $newParams);
unset($newParams['id']);
}
$params = array_merge($params, $newParams);
return $params;
return $this->endDoPayment($params, $newParams);
}
/**
......@@ -614,8 +523,9 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* Assoc array of input parameters for this transaction.
* @param int $amount
* Transaction amount in USD cents.
* @param object $stripeCustomer
* @param \Stripe\Customer $stripeCustomer
* Stripe customer object generated by Stripe API.
* @param \Stripe\PaymentMethod $stripePaymentMethod
*
* @return array
* The result in a nice formatted array (or an error object).
......@@ -623,7 +533,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* @throws \CiviCRM_API3_Exception
* @throws \CRM_Core_Exception
*/
public function doRecurPayment(&$params, $amount, $stripeCustomer) {
public function doRecurPayment($params, $amount, $stripeCustomer, $stripePaymentMethod) {
$requiredParams = ['contributionRecurID', 'frequency_unit'];
foreach ($requiredParams as $required) {
if (!isset($params[$required])) {
......@@ -635,8 +545,6 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
// Make sure frequency_interval is set (default to 1 if not)
empty($params['frequency_interval']) ? $params['frequency_interval'] = 1 : NULL;
$amount = $this->deprecatedHandleCiviDiscount($params, $amount, $stripeCustomer);
// Create the stripe plan
$planId = self::createPlan($params, $amount);
......@@ -644,16 +552,18 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
$subscriptionParams = [
'prorate' => FALSE,
'plan' => $planId,
'default_payment_method' => $stripePaymentMethod,
];
// Create the stripe subscription for the customer
$stripeSubscription = $stripeCustomer->subscriptions->create($subscriptionParams);
$this->setPaymentProcessorSubscriptionID($stripeSubscription->id);
$recurParams = [
'id' => $params['contributionRecurID'],
'trxn_id' => $stripeSubscription->id,
'trxn_id' => $this->getPaymentProcessorSubscriptionID(),
// FIXME processor_id is deprecated as it is not guaranteed to be unique, but currently (CiviCRM 5.9)
// it is required by cancelSubscription (where it is called subscription_id)
'processor_id' => $stripeSubscription->id,
'processor_id' => $this->getPaymentProcessorSubscriptionID(),
'auto_renew' => 1,
'cycle_day' => date('d'),
'next_sched_contribution_date' => $this->calculateNextScheduledDate($params),
......@@ -665,6 +575,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
}
if ($params['installments']) {
$recurParams['end_date'] = $this->calculateEndDate($params);
$recurParams['installments'] = $params['installments'];
}
}
......@@ -672,9 +583,43 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
CRM_Stripe_Hook::updateRecurringContribution($recurParams);
// Update the recurring payment
civicrm_api3('ContributionRecur', 'create', $recurParams);
// Update the contribution status
return $params;
// Set the orderID (trxn_id) to the invoice ID
// The IPN will change it to the charge_id
$this->setPaymentProcessorOrderID($stripeSubscription->latest_invoice);
return $this->endDoPayment($params);
}
/**
* Submit a refund payment
*
* @param array $params
* Assoc array of input parameters for this transaction.
*
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public function doRefund(&$params) {
$requiredParams = ['charge_id', 'payment_processor_id'];
foreach ($requiredParams as $required) {
if (!isset($params[$required])) {
$message = 'Stripe doRefund: Missing mandatory parameter: ' . $required;
Civi::log()->error($message);
Throw new \Civi\Payment\Exception\PaymentProcessorException($message);
}
}
$refundParams = [
'charge' => $params['charge_id'],
];
if (!empty($params['amount'])) {
$refundParams['amount'] = $this->getAmount($params);
}
try {
$refund = \Stripe\Refund::create($refundParams);
}
catch (Exception $e) {
$this->handleError($e->getCode(), $e->getMessage());
Throw new \Civi\Payment\Exception\PaymentProcessorException($e->getMessage());
}
}
/**
......@@ -770,74 +715,6 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
return $endDate->format('Ymd');
}
/**
* @deprecated This belongs in a separate extension / hook as it's non-standard CiviCRM behaviour
*
* This adds some support for CiviDiscount on recurring contributions and changes the default behavior to discounting
* only the first of a recurring contribution set instead of all. (Intro offer) The Stripe procedure for discounting the
* first payment of subscription entails creating a negative invoice item or negative balance first,
* then creating the subscription at 100% full price. The customers first Stripe invoice will reflect the
* discount. Subsequent invoices will be at the full undiscounted amount.
* NB: Civi currently won't send a $0 charge to a payproc extension, but it should in this case. If the discount is
* the cost of initial payment, we still send the whole discount (or giftcard) as a negative balance.
* Consider not selling giftards greater than your least expensive auto-renew membership until we can override this.
*
* @param $params
* @param $amount
* @param $stripeCustomer
*
* @return float|int
* @throws \CiviCRM_API3_Exception
*/
public function deprecatedHandleCiviDiscount(&$params, $amount, $stripeCustomer) {
if (!empty($params['discountcode'])) {
$discount_code = $params['discountcode'];
$discount_object = civicrm_api3('DiscountCode', 'get', array(
'sequential' => 1,
'return' => "amount,amount_type",
'code' => $discount_code,
));
// amount_types: 1 = percentage, 2 = fixed, 3 = giftcard
if ((!empty($discount_object['values'][0]['amount'])) && (!empty($discount_object['values'][0]['amount_type']))) {
$discount_type = $discount_object['values'][0]['amount_type'];
if ( $discount_type == 1 ) {
// Discount is a percentage. Avoid ugly math and just get the full price using price_ param.
foreach($params as $key=>$value){
if("price_" == substr($key,0,6)){
$price_param = $key;
$price_field_id = substr($key,strrpos($key,'_') + 1);
}
}
if (!empty($params[$price_param])) {
$priceFieldValue = civicrm_api3('PriceFieldValue', 'get', array(
'sequential' => 1,
'return' => "amount",
'id' => $params[$price_param],
'price_field_id' => $price_field_id,
));
}
if (!empty($priceFieldValue['values'][0]['amount'])) {
$priceset_amount = $priceFieldValue['values'][0]['amount'];
$full_price = $priceset_amount * 100;
$discount_in_cents = $full_price - $amount;
// Set amount to full price.
$amount = $full_price;
}
} else if ( $discount_type >= 2 ) {
// discount is fixed or a giftcard. (may be > amount).
$discount_amount = $discount_object['values'][0]['amount'];
$discount_in_cents = $discount_amount * 100;
// Set amount to full price.
$amount = $amount + $discount_in_cents;
}
}
// Apply the disount through a negative balance.
$stripeCustomer->account_balance = -$discount_in_cents;
$stripeCustomer->save();
}
return $amount;
}
/**
* Default payment instrument validation.
*
......@@ -863,15 +740,15 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
$contributionRecurId = $this->getRecurringContributionId($params);
try {
$contributionRecur = civicrm_api3('ContributionRecur', 'getsingle', array(
$contributionRecur = civicrm_api3('ContributionRecur', 'getsingle', [
'id' => $contributionRecurId,
));
]);
}
catch (Exception $e) {
return FALSE;
}
if (empty($contributionRecur['trxn_id'])) {
CRM_Core_Session::setStatus(ts('The recurring contribution cannot be cancelled (No reference (trxn_id) found).'), 'Smart Debit', 'error');
CRM_Core_Session::setStatus(E::ts('The recurring contribution cannot be cancelled (No reference (trxn_id) found).'), 'Smart Debit', 'error');
return FALSE;
}
......
<?php
/*
* @file
* Handle Stripe Webhooks for recurring payments.
/**
* https://civicrm.org/licensing
*/
/**
* Class CRM_Core_Payment_StripeIPN
*/
class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
use CRM_Core_Payment_StripeIPNTrait;
use CRM_Core_Payment_MJWIPNTrait;
/**
* @var \CRM_Core_Payment_Stripe Payment processor
*/
protected $_paymentProcessor;
/**
* Transaction ID is the contribution in the redirect flow and a random number in the on-site->POST flow
......@@ -16,7 +23,7 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
protected $transaction_id;
// By default, always retrieve the event from stripe to ensure we are
// not being fed garbage. However, allow an override so when we are
// not being fed garbage. However, allow an override so when we are
// testing, we can properly test a failed recurring contribution.
protected $verify_event = TRUE;
......@@ -39,7 +46,7 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
protected $frequency_unit = NULL;
protected $plan_name = NULL;
protected $plan_start = NULL;
// Derived properties.
protected $contribution_recur_id = NULL;
protected $event_id = NULL;
......@@ -47,16 +54,14 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
protected $receive_date = NULL;
protected $amount = NULL;
protected $fee = NULL;
protected $net_amount = NULL;
protected $previous_contribution = [];
protected $contribution = NULL;
protected $previous_contribution = NULL;
/**
* CRM_Core_Payment_StripeIPN constructor.
*
* @param $ipnData
* @param \stdClass $ipnData
* @param bool $verify
*
* @throws \CRM_Core_Exception
*/
public function __construct($ipnData, $verify = TRUE) {
$this->verify_event = $verify;
......@@ -64,53 +69,12 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
parent::__construct();
}
/**
* Set the value of is_email_receipt to use when a new contribution is received for a recurring contribution
* This is used for the API Stripe.Ipn function. If not set, we respect the value set on the ContributionRecur entity.
*
* @param int $sendReceipt The value of is_email_receipt
*/
public function setSendEmailReceipt($sendReceipt) {
switch ($sendReceipt) {
case 0:
$this->is_email_receipt = 0;
break;
case 1:
$this->is_email_receipt = 1;
break;
default:
$this->is_email_receipt = 0;
}
}
/**
* Get the value of is_email_receipt to use when a new contribution is received for a recurring contribution
* This is used for the API Stripe.Ipn function. If not set, we respect the value set on the ContributionRecur entity.
*
* @return int
* @throws \CiviCRM_API3_Exception
*/
public function getSendEmailReceipt() {
if (isset($this->is_email_receipt)) {
return (int) $this->is_email_receipt;
}
if (!empty($this->contribution_recur_id)) {
$this->is_email_receipt = civicrm_api3('ContributionRecur', 'getvalue', [
'return' => "is_email_receipt",
'id' => $this->contribution_recur_id,
]);
}
return (int) $this->is_email_receipt;
}
/**
* Store input array on the class.
* 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');
......@@ -119,14 +83,13 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
// Determine the proper Stripe Processor ID so we can get the secret key
// and initialize Stripe.
$this->getPaymentProcessor();
$this->_paymentProcessor->setAPIParams();
// Now re-retrieve the data from Stripe to ensure it's legit.
\Stripe\Stripe::setApiKey($this->_paymentProcessor['user_name']);
// Special case if this is the test webhook
if (substr($parameters->id, -15, 15) === '_00000000000000') {
http_response_code(200);
$test = (boolean) $this->_paymentProcessor['is_test'] ? '(Test processor)' : '(Live processor)';
$test = (boolean) $this->_paymentProcessor->getPaymentProcessor()['is_test'] ? '(Test processor)' : '(Live processor)';
echo "Test webhook from Stripe ({$parameters->id}) received successfully by CiviCRM {$test}.";
exit();
}
......@@ -155,168 +118,150 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
$value = CRM_Utils_Type::validate($value, $type, FALSE);
if ($abort && $value === NULL) {
echo "Failure: Missing Parameter<p>" . CRM_Utils_Type::escape($name, 'String');
$this->exception("Could not find an entry for $name");
echo "Failure: Missing or invalid parameter<p>" . CRM_Utils_Type::escape($name, 'String');
$this->exception("Missing or invalid parameter {$name}");
}
return $value;
}
/**
* @return bool
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
* @throws \Stripe\Error\Api
*/
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!
switch($this->event_type) {
// Successful recurring payment.
case 'invoice.payment_succeeded':
// Successful recurring payment. Either we are completing an existing contribution or it's the next one in a subscription
$this->setInfo();
if ($this->previous_contribution['contribution_status_id'] == $pendingStatusId) {
$this->completeContribution();
if ($this->contribution['contribution_status_id'] == $pendingStatusId) {
$params = [
'id' => $this->contribution['id'],
'trxn_date' => $this->receive_date,
'contribution_trxn_id' => $this->invoice_id,
'payment_trxn_id' => $this->charge_id,
'total_amount' => $this->amount,
'fee_amount' => $this->fee,
];
$this->updateContributionCompleted($params);
// Don't touch the contributionRecur as it's updated automatically by Contribution.completetransaction
}
elseif ($this->previous_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(
elseif ($this->contribution['trxn_id'] != $this->invoice_id) {
// Stripe has generated a new invoice (next payment in a subscription) so we
// create a new contribution in CiviCRM
$params = [
'contribution_recur_id' => $this->contribution_recur_id,
'contribution_status_id' => 'Completed',
'receive_date' => $this->receive_date,
'trxn_id' => $this->charge_id,
'contribution_trxn_id' => $this->invoice_id,
'payment_trxn_id' => $this->charge_id,
'total_amount' => $this->amount,
'fee_amount' => $this->fee,
'is_email_receipt' => $this->getSendEmailReceipt(),
));
'previous_contribution' => $this->previous_contribution,
];
$this->repeatContribution($params);
// Don't touch the contributionRecur as it's updated automatically by Contribution.repeattransaction
}
// Successful charge & more to come.
civicrm_api3('ContributionRecur', 'create', array(
'id' => $this->contribution_recur_id,
'failure_count' => 0,
'contribution_status_id' => 'In Progress'
));
$this->handleInstallmentsForSubscription();
return TRUE;
// Failed recurring payment.
case 'invoice.payment_failed':
// Failed recurring payment. Either we are failing an existing contribution or it's the next one in a subscription
$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'],
'contribution_status_id' => "Failed",
'receive_date' => $failDate,
'is_email_receipt' => 0,
));
$params = [
'id' => $this->contribution['id'],
'receive_date' => $this->receive_date,
'cancel_reason' => $this->retrieve('failure_message', 'String'),
'payment_trxn_id' => $this->charge_id,
];
$this->updateContributionFailed($params);
}
else {
$contributionParams = [
elseif ($this->contribution['trxn_id'] != $this->invoice_id) {
$params = [
'contribution_recur_id' => $this->contribution_recur_id,
'contribution_status_id' => 'Failed',
'receive_date' => $failDate,
'receive_date' => $this->receive_date,
'contribution_trxn_id' => $this->invoice_id,
'payment_trxn_id' => $this->charge_id,
'total_amount' => $this->amount,
'is_email_receipt' => 0,
'fee_amount' => $this->fee,
'previous_contribution' => $this->previous_contribution,
];
civicrm_api3('Contribution', 'repeattransaction', $contributionParams);
$this->repeatContribution($params);
// Don't touch the contributionRecur as it's updated automatically by Contribution.completetransaction
}
$failureCount = civicrm_api3('ContributionRecur', 'getvalue', array(
'id' => $this->contribution_recur_id,
'return' => 'failure_count',
));
$failureCount++;
// Change the status of the Recurring and update failed attempts.
civicrm_api3('ContributionRecur', 'create', array(
'id' => $this->contribution_recur_id,
'contribution_status_id' => "Failed",
'failure_count' => $failureCount,
'modified_date' => $failDate,
));
return TRUE;
// Subscription is cancelled
case 'customer.subscription.deleted':
// Subscription is cancelled
$this->setInfo();
// Cancel the recurring contribution
civicrm_api3('ContributionRecur', 'cancel', array(
'id' => $this->contribution_recur_id,
));
$this->updateRecurCancelled(['id' => $this->contribution_recur_id, 'cancel_date' => $this->retrieve('cancel_date', 'String', FALSE)]);
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]);
}
$this->setInfo();
$params = [
'id' => $this->contribution['id'],
'receive_date' => $this->receive_date,
'cancel_reason' => $this->retrieve('failure_message', 'String'),
'payment_trxn_id' => $this->charge_id,
];
$this->updateContributionFailed($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
]);
}
}
}
$this->setInfo();
$refunds = \Stripe\Refund::all(['charge' => $this->charge_id, 'limit' => 1]);
$params = [
'id' => $this->contribution['id'],
'total_amount' => $this->retrieve('amount_refunded', 'Float'),
'cancel_reason' => $refunds->data[0]->reason,
'cancel_date' => date('YmdHis', $refunds->data[0]->created),
];
$this->updateContributionRefund($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) {
$params = [
'id' => $this->contribution['id'],
'trxn_date' => $this->receive_date,
'contribution_trxn_id' => $this->charge_id,
'payment_trxn_id' => $this->charge_id,
'total_amount' => $this->amount,
'fee_amount' => $this->fee,
];
$this->updateContributionCompleted($params);
}
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'],
'total_amount' => $this->plan_amount,
'contribution_recur_id' => $this->contribution_recur_id,
]);
]);
return TRUE;
}
// Unhandled event type.
......@@ -324,42 +269,17 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
}
/**
* Complete a pending contribution and update associated entities (recur/membership)
*
* @throws \CiviCRM_API3_Exception
*/
public function completeContribution() {
// Update the contribution to include the fee.
civicrm_api3('Contribution', 'create', array(
'id' => $this->previous_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'],
'trxn_date' => $this->receive_date,
'trxn_id' => $this->charge_id,
'total_amount' => $this->amount,
'net_amount' => $this->net_amount,
'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
* as much as we can about this event and set that information as
* as much as we can about this event and set that information as
* properties to be used later.
*
* @throws \CRM_Core_Exception
*/
public function setInfo() {
$abort = FALSE;
$stripeObjectName = get_class($this->_inputParameters->data->object);
$this->customer_id = CRM_Stripe_Api::getObjectParam('customer_id', $this->_inputParameters->data->object);
if (empty($this->customer_id)) {
$this->exception('Missing customer_id!');
......@@ -377,34 +297,30 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
$this->plan_name = $this->retrieve('plan_name', 'String', $abort);
$this->plan_start = $this->retrieve('plan_start', 'String', $abort);
if (($stripeObjectName !== 'Stripe\Charge') && ($this->charge_id !== NULL)) {
$charge = \Stripe\Charge::retrieve($this->charge_id);
$balanceTransactionID = CRM_Stripe_Api::getObjectParam('balance_transaction', $charge);
}
else {
$charge = $this->_inputParameters->data->object;
$balanceTransactionID = CRM_Stripe_Api::getObjectParam('balance_transaction', $this->_inputParameters->data->object);
}
// Gather info about the amount and fee.
// Get the Stripe charge object if one exists. Null charge still needs processing.
if ($this->charge_id !== null) {
// If the transaction is declined, there won't be a balance_transaction_id.
$this->amount = 0;
$this->fee = 0;
if ($balanceTransactionID) {
try {
$charge = \Stripe\Charge::retrieve($this->charge_id);
$balance_transaction_id = $charge->balance_transaction;
// If the transaction is declined, there won't be a balance_transaction_id.
if ($balance_transaction_id) {
$balance_transaction = \Stripe\BalanceTransaction::retrieve($balance_transaction_id);
$this->amount = $charge->amount / 100;
$this->fee = $balance_transaction->fee / 100;
}
else {
$this->amount = 0;
$this->fee = 0;
}
$balanceTransaction = \Stripe\BalanceTransaction::retrieve($balanceTransactionID);
$this->amount = $charge->amount / 100;
$this->fee = $balanceTransaction->fee / 100;
}
catch(Exception $e) {
$this->exception('Cannot get contribution amounts');
$this->exception('Error retrieving balance transaction. ' . $e->getMessage());
}
} else {
// The customer had a credit on their subscription from a downgrade or gift card.
$this->amount = 0;
$this->fee = 0;
}
$this->net_amount = $this->amount - $this->fee;
// Additional processing of values is only relevant if there is a subscription id.
if ($this->subscription_id) {
// Get the recurring contribution record associated with the Stripe subscription.
......@@ -416,28 +332,81 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
$this->exception('Cannot find recurring contribution for subscription ID: ' . $this->subscription_id . '. ' . $e->getMessage());
}
}
// If a recurring contribution has been found, get the most recent contribution belonging to it.
if ($this->contribution_recur_id) {
if ($this->charge_id) {
try {
$this->contribution = civicrm_api3('Contribution', 'getsingle', [
'trxn_id' => $this->charge_id,
'contribution_test' => $this->_paymentProcessor->getIsTestMode(),
]);
}
catch (Exception $e) {
// Contribution not found - that's ok
}
}
if (!$this->contribution && $this->invoice_id) {
try {
$this->contribution = civicrm_api3('Contribution', 'getsingle', [
'trxn_id' => $this->invoice_id,
'contribution_test' => $this->_paymentProcessor->getIsTestMode(),
]);
}
catch (Exception $e) {
// Contribution not found - that's ok
}
}
if (!$this->contribution && $this->contribution_recur_id) {
// If a recurring contribution has been found, get the most recent contribution belonging to it.
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'),
));
'contribution_test' => $this->_paymentProcessor->getIsTestMode(),
'options' => ['limit' => 1, 'sort' => 'id DESC'],
]);
$this->previous_contribution = $contribution;
}
catch (Exception $e) {
$this->exception('Cannot find any contributions with recurring contribution ID: ' . $this->contribution_recur_id . '. ' . $e->getMessage());
}
}
if (!$this->contribution) {
$this->exception('No matching contributions for event ' . CRM_Stripe_Api::getParam('id', $this->_inputParameters));
}
}
public function exception($message) {
$errorMessage = 'StripeIPN Exception: Event: ' . $this->event_type . ' Error: ' . $message;
Civi::log()->debug($errorMessage);
http_response_code(400);
exit(1);
/**
* This allows us to end a subscription once:
* a) We've reached the end date / number of installments
* b) The recurring contribution is marked as completed
*
* @throws \CiviCRM_API3_Exception
*/
private function handleInstallmentsForSubscription() {
if ((!$this->contribution_recur_id) || (!$this->subscription_id)) {
return;
}
$contributionRecur = civicrm_api3('ContributionRecur', 'getsingle', [
'id' => $this->contribution_recur_id,
]);
if (empty($contributionRecur['installments']) && empty($contributionRecur['end_date'])) {
return;
}
$stripeSubscription = \Stripe\Subscription::retrieve($this->subscription_id);
// If we've passed the end date cancel the subscription
if (($stripeSubscription->current_period_end >= strtotime($contributionRecur['end_date']))
|| ($contributionRecur['contribution_status_id']
== CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_ContributionRecur', 'contribution_status_id', 'Completed'))) {
\Stripe\Subscription::update($this->subscription_id, ['cancel_at_period_end' => TRUE]);
$this->updateRecurCompleted(['id' => $this->contribution_recur_id]);
}
// There is no easy way of retrieving a count of all invoices for a subscription so we ignore the "installments"
// parameter for now and rely on checking end_date (which was calculated based on number of installments...)
// $stripeInvoices = \Stripe\Invoice::all(['subscription' => $this->subscription_id, 'limit' => 100]);
}
}
<?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 {
/**
* Generate the paymentIntent for civicrm_stripe.js
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
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 {
try {
$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,
]);
}
catch (Exception $e) {
CRM_Utils_JSON::output(['error' => ['message' => $e->getMessage()]]);
}
}
self::generatePaymentResponse($intent);
}
/**
* Generate the json response for civicrm_stripe.js
*
* @param \Stripe\PaymentIntent $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,
]);
} else if (($intent->status == 'requires_capture') || ($intent->status == 'requires_confirmation')) {
// 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']]);
}
}
}
......@@ -20,11 +20,17 @@ class CRM_Stripe_Api {
return (bool) $stripeObject->refunded;
case 'amount_refunded':
return (int) $stripeObject->amount_refunded / 100;
return (float) $stripeObject->amount_refunded / 100;
case 'customer_id':
return (string) $stripeObject->customer;
case 'balance_transaction':
return (string) $stripeObject->balance_transaction;
case 'receive_date':
return self::formatDate($stripeObject->created);
}
break;
......@@ -37,7 +43,7 @@ class CRM_Stripe_Api {
return (string) $stripeObject->id;
case 'receive_date':
return $stripeObject->date ? date("Y-m-d H:i:s", $stripeObject->date) : NULL;
return self::formatDate($stripeObject->created);
case 'subscription_id':
return (string) $stripeObject->subscription;
......@@ -64,10 +70,13 @@ class CRM_Stripe_Api {
case 'description':
return (string) $stripeObject->description;
case 'customer_id':
return (string) $stripeObject->customer;
case 'failure_message':
$stripeCharge = \Stripe\Charge::retrieve($stripeObject->charge);
return (string) $stripeCharge->failure_message;
}
break;
......@@ -93,7 +102,10 @@ class CRM_Stripe_Api {
return (string) $stripeObject->plan->name;
case 'plan_start':
return $stripeObject->start ? date("Y-m-d H:i:s", $stripeObject->start) : NULL;
return self::formatDate($stripeObject->start_date);
case 'cancel_date':
return self::formatDate($stripeObject->canceled_at);
case 'cycle_day':
return date("d", $stripeObject->billing_cycle_anchor);
......@@ -119,6 +131,16 @@ class CRM_Stripe_Api {
return NULL;
}
/**
* Return a formatted date from a stripe timestamp or NULL if not set
* @param int $stripeTimestamp
*
* @return string|null
*/
private static function formatDate($stripeTimestamp) {
return $stripeTimestamp ? date('YmdHis', $stripeTimestamp) : NULL;
}
public static function getParam($name, $stripeObject) {
// Common parameters
switch ($name) {
......@@ -128,6 +150,9 @@ class CRM_Stripe_Api {
case 'event_type':
return (string) $stripeObject->type;
case 'id':
return (string) $stripeObject->id;
case 'previous_plan_id':
if (preg_match('/\.updated$/', $stripeObject->type)) {
return (string) $stripeObject->data->previous_attributes->plan->id;
......
<?php
/**
* https://civicrm.org/licensing
*/
use CRM_Stripe_ExtensionUtil as E;
/**
* Class CRM_Stripe_Check
*/
class CRM_Stripe_Check {
const MIN_VERSION_MJWSHARED = 0.3;
public static function checkRequirements(&$messages) {
$extensions = civicrm_api3('Extension', 'get', [
'full_name' => "mjwshared",
]);
if (empty($extensions['id']) || ($extensions['values'][$extensions['id']]['status'] !== 'installed')) {
$messages[] = new CRM_Utils_Check_Message(
'stripe_requirements',
E::ts('The Stripe extension requires the mjwshared extension which is not installed (https://lab.civicrm.org/extensions/mjwshared).'),
E::ts('Stripe: Missing Requirements'),
\Psr\Log\LogLevel::ERROR,
'fa-money'
);
}
if (version_compare($extensions['values'][$extensions['id']]['version'], self::MIN_VERSION_MJWSHARED) === -1) {
$messages[] = new CRM_Utils_Check_Message(
'stripe_requirements',
E::ts('The Stripe extension requires the mjwshared extension version %1 or greater but your system has version %2.',
[
1 => self::MIN_VERSION_MJWSHARED,
2 => $extensions['values'][$extensions['id']]['version']
]),
E::ts('Stripe: Missing Requirements'),
\Psr\Log\LogLevel::ERROR,
'fa-money'
);
}
}
}
......@@ -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,39 +108,29 @@ 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) {
$requiredParams = ['contact_id', 'card_token', 'processor_id'];
// $optionalParams = ['email'];
public static function create($params, $stripe) {
$requiredParams = ['contact_id', 'processor_id'];
foreach ($requiredParams as $required) {
if (empty($params[$required])) {
throw new \Civi\Payment\Exception\PaymentProcessorException('Stripe Customer (create): Missing required parameter: ' . $required);
}
}
$contactDisplayName = civicrm_api3('Contact', 'getvalue', [
'return' => 'display_name',
'id' => $params['contact_id'],
]);
$stripeCustomerParams = [
'description' => $contactDisplayName . ' (CiviCRM)',
'card' => $params['card_token'],
'email' => CRM_Utils_Array::value('email', $params),
'metadata' => ['civicrm_contact_id' => $params['contact_id']],
];
$stripeCustomerParams = self::getStripeCustomerMetadata($params);
try {
$stripeCustomer = \Stripe\Customer::create($stripeCustomerParams);
}
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);
}
......@@ -155,6 +145,61 @@ class CRM_Stripe_Customer {
return $stripeCustomer;
}
/**
* @param array $params
* @param \CRM_Core_Payment_Stripe $stripe
* @param string $stripeCustomerID
*
* @return \Stripe\Customer
* @throws \CiviCRM_API3_Exception
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public static function updateMetadata($params, $stripe, $stripeCustomerID) {
$requiredParams = ['contact_id', 'processor_id'];
foreach ($requiredParams as $required) {
if (empty($params[$required])) {
throw new \Civi\Payment\Exception\PaymentProcessorException('Stripe Customer (updateMetadata): Missing required parameter: ' . $required);
}
}
$stripeCustomerParams = self::getStripeCustomerMetadata($params);
try {
$stripeCustomer = \Stripe\Customer::update($stripeCustomerID, $stripeCustomerParams);
}
catch (Exception $e) {
$err = CRM_Core_Payment_Stripe::parseStripeException('create_customer', $e, FALSE);
$errorMessage = $stripe->handleErrorNotification($err, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to update Stripe Customer: ' . $errorMessage);
}
return $stripeCustomer;
}
/**
* @param array $params
* Required: contact_id; Optional: email
*
* @return array
* @throws \CiviCRM_API3_Exception
*/
private static function getStripeCustomerMetadata($params) {
$contactDisplayName = civicrm_api3('Contact', 'getvalue', [
'return' => 'display_name',
'id' => $params['contact_id'],
]);
$stripeCustomerParams = [
'name' => $contactDisplayName,
'description' => 'CiviCRM: ' . civicrm_api3('Domain', 'getvalue', ['current_domain' => 1, 'return' => 'name']),
'email' => CRM_Utils_Array::value('email', $params),
'metadata' => [
'CiviCRM Contact ID' => $params['contact_id'],
'CiviCRM URL' => CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$params['contact_id']}", TRUE),
],
];
return $stripeCustomerParams;
}
/**
* Delete a Stripe customer from the CiviCRM database
*
......
<?php
/**
* Collection of upgrade steps.
* DO NOT USE a naming scheme other than upgrade_N, where N is an integer.
* Naming scheme upgrade_X_Y_Z is offically wrong!
* DO NOT USE a naming scheme other than upgrade_N, where N is an integer.
* Naming scheme upgrade_X_Y_Z is offically wrong!
* https://chat.civicrm.org/civicrm/pl/usx3pfjzjbrhzpewuggu1e6ftw
*/
class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
......@@ -56,7 +56,7 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
CRM_Core_DAO::executeQuery('ALTER TABLE civicrm_stripe_plans ADD COLUMN `processor_id` int(10) DEFAULT NULL COMMENT "ID from civicrm_payment_processor"');
CRM_Core_DAO::executeQuery('ALTER TABLE civicrm_stripe_subscriptions ADD COLUMN `processor_id` int(10) DEFAULT NULL COMMENT "ID from civicrm_payment_processor"');
}
return TRUE;
return TRUE;
}
......@@ -70,14 +70,14 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
$config = CRM_Core_Config::singleton();
$dbName = DB::connect($config->dsn)->_db;
$null_count = CRM_Core_DAO::executeQuery('SELECT COUNT(*) FROM civicrm_stripe_customers where processor_id IS NULL') +
$null_count = CRM_Core_DAO::executeQuery('SELECT COUNT(*) FROM civicrm_stripe_customers where processor_id IS NULL') +
CRM_Core_DAO::executeQuery('SELECT COUNT(*) FROM civicrm_stripe_plans where processor_id IS NULL') +
CRM_Core_DAO::executeQuery('SELECT COUNT(*) FROM civicrm_stripe_subscriptions where processor_id IS NULL');
if ( $null_count == 0 ) {
$this->ctx->log->info('Skipped civicrm_stripe update 5002. No nulls found in column processor_id in our tables.');
return TRUE;
}
else {
}
else {
try {
// Set processor ID if there's only one.
$processorCount = civicrm_api3('PaymentProcessorType', 'get', array(
......@@ -109,8 +109,8 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
return TRUE;
}
/**
/**
* Add subscription_id column to civicrm_stripe_subscriptions table.
*
* @return TRUE on success
......@@ -131,11 +131,11 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
CRM_Core_DAO::executeQuery('ALTER TABLE civicrm_stripe_subscriptions ADD COLUMN `subscription_id` varchar(255) DEFAULT NULL COMMENT "Subscription ID from Stripe" FIRST');
CRM_Core_DAO::executeQuery('ALTER TABLE `civicrm_stripe_subscriptions` ADD UNIQUE KEY(`subscription_id`)');
}
return TRUE;
}
/**
return TRUE;
}
/**
* Populates the subscription_id column in table civicrm_stripe_subscriptions.
*
* @return TRUE on success
......@@ -145,51 +145,43 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
$config = CRM_Core_Config::singleton();
$dbName = DB::connect($config->dsn)->_db;
$null_count = CRM_Core_DAO::executeQuery('SELECT COUNT(*) FROM civicrm_stripe_subscriptions where subscription_id IS NULL');
$null_count = CRM_Core_DAO::executeQuery('SELECT COUNT(*) FROM civicrm_stripe_subscriptions where subscription_id IS NULL');
if ( $null_count == 0 ) {
$this->ctx->log->info('Skipped civicrm_stripe update 5004. No nulls found in column subscription_id in our civicrm_stripe_subscriptions table.');
}
else {
$customer_infos = CRM_Core_DAO::executeQuery("SELECT customer_id,processor_id
}
else {
$customer_infos = CRM_Core_DAO::executeQuery("SELECT customer_id,processor_id
FROM `civicrm_stripe_subscriptions`;");
while ( $customer_infos->fetch() ) {
$processor_id = $customer_infos->processor_id;
$customer_id = $customer_infos->customer_id;
try {
$stripe_key = civicrm_api3('PaymentProcessor', 'getvalue', array(
'return' => 'user_name',
'id' => $processor_id,
));
}
catch (Exception $e) {
Civi::log()->debug('Update 5004 failed. Has Stripe been removed as a payment processor?', $out = false);
return;
}
try {
\Stripe\Stripe::setApiKey($stripe_key);
$subscription = \Stripe\Subscription::all(array(
'customer'=> $customer_id,
'limit'=>1,
));
}
catch (Exception $e) {
// Don't quit here. A missing customer in Stipe is OK. They don't exist, so they can't have a subscription.
Civi::log()->debug('Cannot find Stripe API key: ' . $e->getMessage());
}
if (!empty($subscription['data'][0]['id'])) {
$query_params = array(
1 => array($subscription['data'][0]['id'], 'String'),
2 => array($customer_id, 'String'),
);
CRM_Core_DAO::executeQuery('UPDATE civicrm_stripe_subscriptions SET subscription_id = %1 where customer_id = %2;', $query_params);
unset($subscription);
}
try {
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $processor_id]));
$processor->setAPIParams();
$subscription = \Stripe\Subscription::all(array(
'customer'=> $customer_id,
'limit'=>1,
));
}
catch (Exception $e) {
// Don't quit here. A missing customer in Stipe is OK. They don't exist, so they can't have a subscription.
Civi::log()->debug('Cannot find Stripe API key: ' . $e->getMessage());
}
if (!empty($subscription['data'][0]['id'])) {
$query_params = array(
1 => array($subscription['data'][0]['id'], 'String'),
2 => array($customer_id, 'String'),
);
CRM_Core_DAO::executeQuery('UPDATE civicrm_stripe_subscriptions SET subscription_id = %1 where customer_id = %2;', $query_params);
unset($subscription);
}
}
}
return TRUE;
return TRUE;
}
/**
/**
* Add contribution_recur_id column to civicrm_stripe_subscriptions table.
*
* @return TRUE on success
......@@ -213,10 +205,10 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
CRM_Core_DAO::executeQuery('ALTER TABLE `civicrm_stripe_subscriptions` ADD INDEX(`contribution_recur_id`);');
CRM_Core_DAO::executeQuery('ALTER TABLE `civicrm_stripe_subscriptions` ADD CONSTRAINT `FK_civicrm_stripe_contribution_recur_id` FOREIGN KEY (`contribution_recur_id`) REFERENCES `civicrm_contribution_recur`(`id`) ON DELETE SET NULL ON UPDATE RESTRICT;');
}
return TRUE;
return TRUE;
}
/**
/**
* Method 1 for populating the contribution_recur_id column in the civicrm_stripe_subscriptions table.
* ( A simple approach if that works if there have never been any susbcription edits in the Stripe UI. )
......@@ -230,93 +222,93 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
$subscriptions = CRM_Core_DAO::executeQuery("SELECT invoice_id,is_live
FROM `civicrm_stripe_subscriptions`;");
while ( $subscriptions->fetch() ) {
$test_mode = (int)!$subscriptions->is_live;
try {
// Fetch the recurring contribution Id.
$recur_id = civicrm_api3('Contribution', 'getvalue', array(
'sequential' => 1,
'return' => "contribution_recur_id",
'invoice_id' => $subscriptions->invoice_id,
'contribution_test' => $test_mode,
));
}
catch (CiviCRM_API3_Exception $e) {
// Don't quit here. If we can't find the recurring ID for a single customer, make a note in the error log and carry on.
Civi::log()->debug('Recurring contribution search: ' . $e->getMessage());
}
if (!empty($recur_id)) {
$p = array(
1 => array($recur_id, 'Integer'),
2 => array($subscriptions->invoice_id, 'String'),
);
CRM_Core_DAO::executeQuery('UPDATE civicrm_stripe_subscriptions SET contribution_recur_id = %1 WHERE invoice_id = %2;', $p);
}
while ( $subscriptions->fetch() ) {
$test_mode = (int)!$subscriptions->is_live;
try {
// Fetch the recurring contribution Id.
$recur_id = civicrm_api3('Contribution', 'getvalue', array(
'sequential' => 1,
'return' => "contribution_recur_id",
'invoice_id' => $subscriptions->invoice_id,
'contribution_test' => $test_mode,
));
}
catch (CiviCRM_API3_Exception $e) {
// Don't quit here. If we can't find the recurring ID for a single customer, make a note in the error log and carry on.
Civi::log()->debug('Recurring contribution search: ' . $e->getMessage());
}
return TRUE;
if (!empty($recur_id)) {
$p = array(
1 => array($recur_id, 'Integer'),
2 => array($subscriptions->invoice_id, 'String'),
);
CRM_Core_DAO::executeQuery('UPDATE civicrm_stripe_subscriptions SET contribution_recur_id = %1 WHERE invoice_id = %2;', $p);
}
}
return TRUE;
}
/**
/**
* Method 2 for populating the contribution_recur_id column in the civicrm_stripe_subscriptions table. Uncomment this and comment 5006.
* ( A more convoluted approach that works if there HAVE been susbcription edits in the Stripe UI. )
* @return TRUE on success. Please let users uncomment this as needed and increment past 5007 for the next upgrade.
* @throws Exception
*/
/*
public function upgrade_5007() {
$config = CRM_Core_Config::singleton();
$dbName = DB::connect($config->dsn)->_db;
$subscriptions = CRM_Core_DAO::executeQuery("SELECT customer_id,is_live,processor_id
FROM `civicrm_stripe_subscriptions`;");
while ( $subscriptions->fetch() ) {
$test_mode = (int)!$subscriptions->is_live;
$p = array(
1 => array($subscriptions->customer_id, 'String'),
2 => array($subscriptions->is_live, 'Integer'),
);
$customer = CRM_Core_DAO::executeQuery("SELECT email
FROM `civicrm_stripe_customers` WHERE id = %1 AND is_live = %2;", $p);
$customer->fetch();
// Try the billing email first, since that's what we send to Stripe.
try {
$contact = civicrm_api3('Email', 'get', array(
'sequential' => 1,
'return' => "contact_id",
'is_billing' => 1,
'email' => $customer->email,
'api.ContributionRecur.get' => array('return' => "id", 'contact_id' => "\$value.contact_id", 'contribution_status_id' => "In Progress"),
));
}
catch (CiviCRM_API3_Exception $e) {
// Uh oh, that didn't work. Try to retrieve the recurring id using the primary email.
$contact = civicrm_api3('Contact', 'get', array(
'sequential' => 1,
'return' => "id",
'email' => $customer->email,
'api.ContributionRecur.get' => array('sequential' => 1, 'return' => "id", 'contact_id' => "\$values.id", 'contribution_status_id' => "In Progress"),
));
}
/*
public function upgrade_5007() {
$config = CRM_Core_Config::singleton();
$dbName = DB::connect($config->dsn)->_db;
$subscriptions = CRM_Core_DAO::executeQuery("SELECT customer_id,is_live,processor_id
FROM `civicrm_stripe_subscriptions`;");
while ( $subscriptions->fetch() ) {
$test_mode = (int)!$subscriptions->is_live;
$p = array(
1 => array($subscriptions->customer_id, 'String'),
2 => array($subscriptions->is_live, 'Integer'),
);
$customer = CRM_Core_DAO::executeQuery("SELECT email
FROM `civicrm_stripe_customers` WHERE id = %1 AND is_live = %2;", $p);
$customer->fetch();
// Try the billing email first, since that's what we send to Stripe.
try {
$contact = civicrm_api3('Email', 'get', array(
'sequential' => 1,
'return' => "contact_id",
'is_billing' => 1,
'email' => $customer->email,
'api.ContributionRecur.get' => array('return' => "id", 'contact_id' => "\$value.contact_id", 'contribution_status_id' => "In Progress"),
));
}
catch (CiviCRM_API3_Exception $e) {
// Uh oh, that didn't work. Try to retrieve the recurring id using the primary email.
$contact = civicrm_api3('Contact', 'get', array(
'sequential' => 1,
'return' => "id",
'email' => $customer->email,
'api.ContributionRecur.get' => array('sequential' => 1, 'return' => "id", 'contact_id' => "\$values.id", 'contribution_status_id' => "In Progress"),
));
}
if (!empty($contact['values'][0]['api.ContributionRecur.get']['values'][0]['id'])) {
$recur_id = $contact['values'][0]['api.ContributionRecur.get']['values'][0]['id'];
$p = array(
1 => array($recur_id, 'Integer'),
2 => array($subscriptions->customer_id, 'String'),
);
CRM_Core_DAO::executeQuery('UPDATE civicrm_stripe_subscriptions SET contribution_recur_id = %1 WHERE customer_id = %2;', $p);
} else {
// Crap.
$this->ctx->log->info('Update 5007 failed. Consider adding recurring IDs manuallly to civicrm_stripe_subscriptions. ');
return;
if (!empty($contact['values'][0]['api.ContributionRecur.get']['values'][0]['id'])) {
$recur_id = $contact['values'][0]['api.ContributionRecur.get']['values'][0]['id'];
$p = array(
1 => array($recur_id, 'Integer'),
2 => array($subscriptions->customer_id, 'String'),
);
CRM_Core_DAO::executeQuery('UPDATE civicrm_stripe_subscriptions SET contribution_recur_id = %1 WHERE customer_id = %2;', $p);
} else {
// Crap.
$this->ctx->log->info('Update 5007 failed. Consider adding recurring IDs manuallly to civicrm_stripe_subscriptions. ');
return;
}
}
}
return TRUE;
}
*/
return TRUE;
}
*/
/**
/**
* Add change default NOT NULL to NULL in vestigial invoice_id column in civicrm_stripe_subscriptions table if needed. (issue #192)
*
* @return TRUE on success
......@@ -338,7 +330,7 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
MODIFY COLUMN `invoice_id` varchar(255) NULL default ""
COMMENT "Safe to remove this column if the update retrieving subscription IDs completed satisfactorily."');
}
return TRUE;
return TRUE;
}
/**
......@@ -407,4 +399,25 @@ class CRM_Stripe_Upgrader extends CRM_Stripe_Upgrader_Base {
return TRUE;
}
public function upgrade_5023() {
$this->ctx->log->info('Applying Stripe update 5023. Swap over public/secret key settings');
$stripeProcessors = civicrm_api3('PaymentProcessor', 'get', [
'payment_processor_type_id' => "Stripe",
]);
foreach ($stripeProcessors['values'] as $processor) {
if ((substr($processor['user_name'], 0, 3) === 'sk_')
&& (substr($processor['password'], 0, 3) === 'pk_')) {
// Need to switch over parameters
$createParams = [
'id' => $processor['id'],
'user_name' => $processor['password'],
'password' => $processor['user_name'],
];
civicrm_api3('PaymentProcessor', 'create', $createParams);
}
}
CRM_Utils_System::flushCache();
return TRUE;
}
}
......@@ -20,13 +20,16 @@ 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) {
$messageTexts = [];
$webhook_path = self::getWebhookPath($paymentProcessor['id']);
\Stripe\Stripe::setApiKey(CRM_Core_Payment_Stripe::getSecretKey($paymentProcessor));
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $paymentProcessor['id']]));
$processor->setAPIParams();
try {
$webhooks = \Stripe\WebhookEndpoint::all(["limit" => 100]);
}
......@@ -35,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'
);
......@@ -91,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,
]
);
}
}
......@@ -101,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'
);
......@@ -112,13 +112,30 @@ 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
*
* @param int $paymentProcessorId
*/
public static function createWebhook($paymentProcessorId) {
\Stripe\Stripe::setApiKey(CRM_Core_Payment_Stripe::getSecretKeyById($paymentProcessorId));
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $paymentProcessorId]));
$processor->setAPIParams();
$params = [
'enabled_events' => self::getDefaultEnabledEvents(),
......
<?php
/**
* This api allows you to replay Stripe events.
* This api allows you to replay Stripe events.
*
* You can either pass the id of an entry in the System Log (which can
* be populated with the Stripe.PopulateLog call) or you can pass a
* event id from Stripe directly.
*
* When processing an event, the event will always be re-fetched from the
* Stripe server first, so this will not work while offline or with
* Stripe server first, so this will not work while offline or with
* events that were not generated by the Stripe server.
*/
*/
/**
* Stripe.Ipn API specification
......@@ -57,12 +57,9 @@ function civicrm_api3_stripe_Ipn($params) {
throw new API_Exception('Please pass the payment processor id (ppid) if using evtid.', 3236);
}
$ppid = $params['ppid'];
$results = civicrm_api3('PaymentProcessor', 'getsingle', array('id' => $ppid));
// YES! I know, password and user are backwards. wtf??
$sk = $results['user_name'];
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $ppid]));
$processor->setAPIParams();
require_once ("vendor/stripe/stripe-php/init.php");
\Stripe\Stripe::setApiKey($sk);
$object = \Stripe\Event::retrieve($params['evtid']);
}
// Avoid a SQL error if this one has been processed already.
......@@ -74,7 +71,7 @@ function civicrm_api3_stripe_Ipn($params) {
return civicrm_api3_create_error("Ipn already processed.");
}
if (class_exists('CRM_Core_Payment_StripeIPN')) {
// The $_GET['processor_id'] value is normally set by
// The $_GET['processor_id'] value is normally set by
// CRM_Core_Payment::handlePaymentMethod
$_GET['processor_id'] = $ppid;
$ipnClass = new CRM_Core_Payment_StripeIPN($object);
......
......@@ -9,7 +9,7 @@
/**
* Stripe.ListEvents API specification
*
*
*
* @param array $spec description of fields supported by this API call
* @return void
......@@ -17,12 +17,13 @@
*/
function _civicrm_api3_stripe_ListEvents_spec(&$spec) {
$spec['ppid']['title'] = ts("Use the given Payment Processor ID");
$spec['ppid']['type'] = CRM_Utils_Type::T_INT;
$spec['ppid']['type'] = CRM_Utils_Type::T_INT;
$spec['ppid']['api.required'] = TRUE;
$spec['type']['title'] = ts("Limit to the given Stripe events type, defaults to invoice.payment_succeeded.");
$spec['type']['api.default'] = 'invoice.payment_succeeded';
$spec['limit']['title'] = ts("Limit number of results returned (100 is max)");
$spec['starting_after']['title'] = ts("Only return results after this event id.");
$spec['output']['api.default'] = 'brief';
$spec['output']['api.default'] = 'brief';
$spec['output']['title'] = ts("How to format the output, brief or raw. Defaults to brief.");
}
......@@ -122,18 +123,17 @@ function civicrm_api3_stripe_VerifyEventType($eventType) {
* Process parameters to determine ppid and sk.
*
* @param array $params
*
* @return array
* @throws \API_Exception
*/
function civicrm_api3_stripe_ProcessParams($params) {
$ppid = NULL;
$type = NULL;
$created = NULL;
$limit = NULL;
$starting_after = NULL;
$sk = NULL;
if (array_key_exists('ppid', $params) ) {
$ppid = $params['ppid'];
}
if (array_key_exists('created', $params) ) {
$created = $params['created'];
}
......@@ -144,29 +144,6 @@ function civicrm_api3_stripe_ProcessParams($params) {
$starting_after = $params['starting_after'];
}
// Select the right payment processor to use.
if ($ppid) {
$query_params = array('id' => $ppid);
}
else {
// By default, select the live stripe processor (we expect there to be
// only one).
$query_params = array('class_name' => 'Payment_Stripe', 'is_test' => 0);
}
try {
$results = civicrm_api3('PaymentProcessor', 'getsingle', $query_params);
// YES! I know, password and user are backwards. wtf??
$sk = $results['user_name'];
}
catch (CiviCRM_API3_Exception $e) {
if(preg_match('/Expected one PaymentProcessor but/', $e->getMessage())) {
throw new API_Exception("Expected one live Stripe payment processor, but found none or more than one. Please specify ppid=.", 1234);
}
else {
throw new API_Exception("Error getting the Stripe Payment Processor to use", 1235);
}
}
// Check to see if we should filter by type.
if (array_key_exists('type', $params) ) {
// Validate - since we will be appending this to an URL.
......@@ -185,7 +162,7 @@ function civicrm_api3_stripe_ProcessParams($params) {
throw new API_Exception("Created can only be passed in programatically as an array", 1237);
}
}
return array('sk' => $sk, 'type' => $type, 'created' => $created, 'limit' => $limit, 'starting_after' => $starting_after);
return ['type' => $type, 'created' => $created, 'limit' => $limit, 'starting_after' => $starting_after];
}
/**
......@@ -199,7 +176,6 @@ function civicrm_api3_stripe_ProcessParams($params) {
*/
function civicrm_api3_stripe_Listevents($params) {
$parsed = civicrm_api3_stripe_ProcessParams($params);
$sk = $parsed['sk'];
$type = $parsed['type'];
$created = $parsed['created'];
$limit = $parsed['limit'];
......@@ -218,9 +194,10 @@ function civicrm_api3_stripe_Listevents($params) {
if ($starting_after) {
$args['starting_after'] = $starting_after;
}
require_once ("vendor/stripe/stripe-php/init.php");
\Stripe\Stripe::setApiKey($sk);
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $params['ppid']]));
$processor->setAPIParams();
$data_list = \Stripe\Event::all($args);
if (array_key_exists('error', $data_list)) {
$err = $data_list['error'];
......
......@@ -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);
}
......
......@@ -149,9 +149,10 @@ function civicrm_api3_stripe_subscription_import($params) {
$paymentProcessor = \Civi\Payment\System::singleton()->getById($params['payment_processor_id'])->getPaymentProcessor();
// Now re-retrieve the data from Stripe to ensure it's legit.
\Stripe\Stripe::setApiKey($paymentProcessor['user_name']);
$processor = new CRM_Core_Payment_Stripe('', $paymentProcessor);
$processor->setAPIParams();
// Now re-retrieve the data from Stripe to ensure it's legit.
$stripeSubscription = \Stripe\Subscription::retrieve($params['subscription_id']);
// Create the stripe customer in CiviCRM
......
......@@ -8,16 +8,16 @@
"packages": [
{
"name": "stripe/stripe-php",
"version": "v6.40.0",
"version": "v6.43.1",
"source": {
"type": "git",
"url": "https://github.com/stripe/stripe-php.git",
"reference": "9c22ffab790ef4dae0f371929de50e8b53c9ec8d"
"reference": "42fcdaf99c44bb26937223f8eae1f263491d5ab8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/9c22ffab790ef4dae0f371929de50e8b53c9ec8d",
"reference": "9c22ffab790ef4dae0f371929de50e8b53c9ec8d",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/42fcdaf99c44bb26937223f8eae1f263491d5ab8",
"reference": "42fcdaf99c44bb26937223f8eae1f263491d5ab8",
"shasum": ""
},
"require": {
......@@ -60,7 +60,7 @@
"payment processing",
"stripe"
],
"time": "2019-06-27T23:24:51+00:00"
"time": "2019-08-29T16:56:12+00:00"
}
],
"packages-dev": [],
......
#card-element {
padding: 2%;
margin: 2% auto;
max-width: 800px;
background-color: ghostwhite;
-webkit-box-shadow: 10px 10px 7px -6px rgba(0,0,0,0.81);
-moz-box-shadow: 10px 10px 7px -6px rgba(0, 0, 0, 0.81);
box-shadow: 10px 10px 7px -6px rgba(0, 0, 0, 0.81);
}
#card-errors {
margin: 2%;
display: none;
}
......@@ -8,13 +8,21 @@ Latest releases can be found here: https://civicrm.org/extensions/stripe-payment
View this extension in the [Extension Directory](https://civicrm.org/extensions/stripe-payment-processor).
## Compatibility / Requirements
* CiviCRM 5.10+
* PHP 7.0+
* CiviCRM 5.13+
* PHP 7.1+
* Jquery 1.10 (Use jquery_update module on Drupal).
* Drupal 7 / Joomla / Wordpress (latest supported release). *Not currently tested with other CMS but it may work.*
* Stripe API version: 2019-02-19
* Stripe API version: 2019-09-09
* Drupal webform_civicrm 7.x-4.22+ (if using webform integration)
If using test mode with drupal webform_civicrm you need this patch: https://github.com/colemanw/webform_civicrm/pull/266
## Troubleshooting
Under *Administer->CiviContribute->Stripe Settings* you can find a setting:
* Enable Stripe Javascript debugging?
This can be switched on to output debug info to the browser console and can be used to debug problems with submitting your payments.
## Credits
Current Maintainer: Matthew Wire - https://www.mjwconsult.co.uk
......
## Release 6.0.alpha3
* Support recurring payments with paymentIntents/Elements. Cancel subscription with Stripe when we reach recurring end date
* **Update required Stripe API version to 2019-09-09**
* Handle confirmation pages properly for contribution pages (make sure we pass through paymentIntentID).
* Handle card declined on client side.
* Support creating recurring payment (subscription).
* Handle IPN events for charges / invoices (support cancel/refund etc).
* Add basic support for PaymentProcessor.refund API.
* Remove membership_type_tag from plan name.
## Release 6.0.alpha2
* Support Drupal Webform CiviCRM.
* Support Event Registration.
* Support Confirm/Thankyou pages on contribution pages / events.
* Support cards using 3dsecure and cards not using 3dsecure.
### Not Supported (should be in final 6.0 release):
* Recurring payments.
* Backend payments.
## Release 6.0.alpha1
* ONLY contribution pages with no confirm pages are supported.
## Release 6.0 (not yet released)
**This is a major new release. You cannot rollback once you've upgraded.**
**This extension REQUIRES the mjwshared extension available here: https://lab.civicrm.org/extensions/mjwshared**
* Use Stripe Elements: https://stripe.com/payments/elements.
* Use PaymentIntents and comply with the European SCA directive (https://stripe.com/docs/strong-customer-authentication).
* Require Stripe API Version: 2019-09-09 and ensure that all codepaths specify the API version.
* Switch publishable key/secret key in settings (upgrader does this automatically) so they are now "correct" per CiviCRM settings pages.
* Support cards using 3dsecure and cards not using 3dsecure (workflows with Stripe are slightly different but both are now handled).
## Release 5.4.1
* Don't overwrite system messages when performing webhook checks.
* Add form to handle creating/updating webhooks instead of automatically during system check (Thanks @artfulrobot)
......
<?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-07-21</releaseDate>
<version>5.4.1</version>
<develStage>stable</develStage>
<releaseDate>2019-09-10</releaseDate>
<version>6.0.alpha3</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>
......
......@@ -4,59 +4,103 @@
*/
CRM.$(function($) {
// Response from Stripe.createToken.
function stripeResponseHandler(status, response) {
$form = getBillingForm();
$submit = getBillingSubmit();
if (response.error) {
$('html, body').animate({scrollTop: 0}, 300);
// Show the errors on the form.
if ($(".messages.crm-error.stripe-message").length > 0) {
$(".messages.crm-error.stripe-message").slideUp();
$(".messages.crm-error.stripe-message:first").remove();
}
$form.prepend('<div class="messages alert alert-block alert-danger error crm-error stripe-message">'
+ '<strong>Payment Error Response:</strong>'
+ '<ul id="errorList">'
+ '<li>Error: ' + response.error.message + '</li>'
+ '</ul>'
+ '</div>');
removeCCDetails($form, true);
$form.data('submitted', false);
$submit.prop('disabled', false);
}
else {
var token = response['id'];
// Update form with the token & submit.
removeCCDetails($form, false);
$form.find("input#stripe-token").val(token);
var stripe;
var card;
var form;
var submitButton;
var stripeLoading = false;
function paymentIntentSuccessHandler(paymentIntent) {
debugging('paymentIntent confirmation success');
// Insert the token ID into the form so it gets submitted to the server
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'paymentIntentID');
hiddenInput.setAttribute('value', paymentIntent.id);
form.appendChild(hiddenInput);
// Submit the form
form.submit();
}
// Disable unload event handler
window.onbeforeunload = null;
function displayError(result) {
// Display error.message in your UI.
debugging('error: ' + result.error.message);
// Inform the user if there was an error
var errorElement = document.getElementById('card-errors');
errorElement.style.display = 'block';
errorElement.textContent = result.error.message;
document.querySelector('#billing-payment-block').scrollIntoView();
window.scrollBy(0, -50);
submitButton.removeAttribute('disabled');
}
// Restore any onclickAction that was removed.
$submit.attr('onclick', onclickAction);
function handleCardPayment() {
debugging('handle card payment');
stripe.createPaymentMethod('card', card).then(function (result) {
if (result.error) {
// Show error in payment form
displayError(result);
}
else {
// Send paymentMethod.id to server
var url = CRM.url('civicrm/stripe/confirm-payment');
$.post(url, {
payment_method_id: result.paymentMethod.id,
amount: getTotalAmount(),
currency: CRM.vars.stripe.currency,
id: CRM.vars.stripe.id,
}).then(function (result) {
// Handle server response (see Step 3)
handleServerResponse(result);
});
}
});
}
// This triggers submit without generating a submit event (so we don't run submit handler again)
$form.get(0).submit();
function handleServerResponse(result) {
debugging('handleServerResponse');
if (result.error) {
// Show error from server on payment form
displayError(result);
} else if (result.requires_action) {
// Use Stripe.js to handle required card action
handleAction(result);
} else {
// All good, we can submit the form
paymentIntentSuccessHandler(result.paymentIntent);
}
}
function handleAction(response) {
stripe.handleCardAction(
response.payment_intent_client_secret
).then(function(result) {
if (result.error) {
// Show error in payment form
displayError(result);
} else {
// The card action has been handled
// The PaymentIntent can be confirmed again on the server
paymentIntentSuccessHandler(result.paymentIntent);
}
});
}
// Prepare the form.
var onclickAction = null;
$(document).ready(function() {
// Disable the browser "Leave Page Alert" which is triggered because we mess with the form submit function.
window.onbeforeunload = null;
// Load Stripe onto the form.
loadStripeBillingBlock();
$submit = getBillingSubmit();
checkAndLoad();
// Store and remove any onclick Action currently assigned to the form.
// We will re-add it if the transaction goes through.
onclickAction = $submit.attr('onclick');
$submit.removeAttr('onclick');
//onclickAction = submitButton.getAttribute('onclick');
//submitButton.removeAttribute('onclick');
// Quickform doesn't add hidden elements via standard method. On a form where payment processor may
// be loaded via initial form load AND ajax (eg. backend live contribution page with payproc dropdown)
......@@ -82,7 +126,7 @@ CRM.$(function($) {
// There is. Check if the selected payment processor is different
// from the one we think we should be using.
var ppid = $('#payment_processor_id').val();
if (ppid != $('#stripe-id').val()) {
if (ppid != CRM.vars.stripe.id) {
debugging('payment processor changed to id: ' + ppid);
// It is! See if the new payment processor is also a Stripe
// Payment processor. First, find out what the stripe
......@@ -106,143 +150,156 @@ CRM.$(function($) {
if (pub_key) {
// It is a stripe payment processor, so update the key.
debugging("Setting new stripe key to: " + pub_key);
$('#stripe-pub-key').val(pub_key);
CRM.vars.stripe.publishableKey = pub_key;
}
else {
debugging("New payment processor is not Stripe, setting stripe-pub-key to null");
$('#stripe-pub-key').val(null);
CRM.vars.stripe.publishableKey = null;
}
// Now reload the billing block.
loadStripeBillingBlock();
checkAndLoad();
});
});
}
}
loadStripeBillingBlock();
checkAndLoad();
}
});
function loadStripeBillingBlock() {
// Setup Stripe.Js
var $stripePubKey = $('#stripe-pub-key');
function checkAndLoad() {
if (typeof CRM.vars.stripe === 'undefined') {
debugging('CRM.vars.stripe not defined!');
return;
}
if ($stripePubKey.length) {
if (!$().Stripe) {
$.getScript('https://js.stripe.com/v2/', function () {
Stripe.setPublishableKey($('#stripe-pub-key').val());
});
if (typeof Stripe === 'undefined') {
if (stripeLoading) {
return;
}
stripeLoading = true;
debugging('Stripe.js is not loaded!');
$.getScript("https://js.stripe.com/v3", function () {
debugging("Script loaded and executed.");
stripeLoading = false;
loadStripeBillingBlock();
});
}
else {
loadStripeBillingBlock();
}
}
// Get the form containing payment details
$form = getBillingForm();
if (!$form.length) {
function loadStripeBillingBlock() {
stripe = Stripe(CRM.vars.stripe.publishableKey);
var elements = stripe.elements();
var style = {
base: {
fontSize: '20px',
},
};
// Create an instance of the card Element.
card = elements.create('card', {style: style});
card.mount('#card-element');
// Hide the CiviCRM postcode field so it will still be submitted but will contain the value set in the stripe card-element.
document.getElementsByClassName('billing_postal_code-' + CRM.vars.stripe.billingAddressID + '-section')[0].setAttribute('hidden', true);
card.addEventListener('change', function(event) {
updateFormElementsFromCreditCardDetails(event);
});
// Get the form containing payment details
form = getBillingForm();
if (typeof form.length === 'undefined' || form.length === 0) {
debugging('No billing form!');
return;
}
$submit = getBillingSubmit();
submitButton = getBillingSubmit();
// If another submit button on the form is pressed (eg. apply discount)
// add a flag that we can set to stop payment submission
$form.data('submit-dont-process', '0');
form.dataset.submitdontprocess = false;
// Find submit buttons which should not submit payment
$form.find('[type="submit"][formnovalidate="1"], ' +
var nonPaymentSubmitButtons = form.querySelectorAll('[type="submit"][formnovalidate="1"], ' +
'[type="submit"][formnovalidate="formnovalidate"], ' +
'[type="submit"].cancel, ' +
'[type="submit"].webform-previous').click( function() {
debugging('adding submit-dont-process');
$form.data('submit-dont-process', 1);
});
'[type="submit"].webform-previous'), i;
for (i = 0; i < nonPaymentSubmitButtons.length; ++i) {
nonPaymentSubmitButtons[i].addEventListener('click', function () {
debugging('adding submitdontprocess');
form.dataset.submitdontprocess = true;
});
}
$submit.click( function(event) {
submitButton.addEventListener('click', function(event) {
// Take over the click function of the form.
debugging('clearing submit-dont-process');
$form.data('submit-dont-process', 0);
debugging('clearing submitdontprocess');
form.dataset.submitdontprocess = false;
// Run through our own submit, that executes Stripe submission if
// appropriate for this submit.
var ret = submit(event);
if (ret) {
// True means it's not our form. We are bailing and not trying to
// process Stripe.
// Restore any onclickAction that was removed.
$form = getBillingForm();
$submit = getBillingSubmit();
$submit.attr('onclick', onclickAction);
$form.get(0).submit();
return true;
}
// Otherwise, this is a stripe submission - don't handle normally.
// The code for completing the submission is all managed in the
// stripe handler (stripeResponseHandler) which gets execute after
// stripe finishes.
return false;
});
// Add a keypress handler to set flag if enter is pressed
$form.find('input#discountcode').keypress( function(e) {
if (e.which === 13) {
$form.data('submit-dont-process', 1);
}
return submit(event);
});
var isWebform = getIsWebform($form);
addSupportForCiviDiscount();
// For CiviCRM Webforms.
if (isWebform) {
if (getIsDrupalWebform()) {
// We need the action field for back/submit to work and redirect properly after submission
if (!($('#action').length)) {
$form.append($('<input type="hidden" name="op" id="action" />'));
}
var $actions = $form.find('[type=submit]');
$('[type=submit]').click(function() {
$('#action').val(this.value);
addDrupalWebformActionElement(this.value);
});
// If enter pressed, use our submit function
$form.keypress(function(event) {
if (event.which === 13) {
$('#action').val(this.value);
form.addEventListener('keydown', function (e) {
if (e.keyCode === 13) {
addDrupalWebformActionElement(this.value);
submit(event);
}
});
$('#billingcheckbox:input').hide();
$('label[for="billingcheckbox"]').hide();
}
else {
// As we use credit_card_number to pass token, make sure it is empty when shown
$form.find("input#credit_card_number").val('');
$form.find("input#cvv2").val('');
}
function submit(event) {
event.preventDefault();
debugging('submit handler');
if ($form.data('submitted') === true) {
if (form.dataset.submitted === true) {
debugging('form already submitted');
return false;
}
var isWebform = getIsWebform($form);
var stripeProcessorId;
var chosenProcessorId;
if (typeof CRM.vars.stripe !== 'undefined') {
stripeProcessorId = CRM.vars.stripe.id;
}
// Handle multiple payment options and Stripe not being chosen.
if (isWebform) {
var stripeProcessorId;
var chosenProcessorId;
stripeProcessorId = $('#stripe-id').val();
// @fixme this needs refactoring as some is not relevant anymore (with stripe 6.0)
if (getIsDrupalWebform()) {
stripeProcessorId = CRM.vars.stripe.id;
// this element may or may not exist on the webform, but we are dealing with a single (stripe) processor enabled.
if (!$('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]').length) {
chosenProcessorId = stripeProcessorId;
} else {
chosenProcessorId = $form.find('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]:checked').val();
chosenProcessorId = form.querySelector('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]:checked').val();
}
}
else {
// Most forms have payment_processor-section but event registration has credit_card_info-section
if (($form.find(".crm-section.payment_processor-section").length > 0)
|| ($form.find(".crm-section.credit_card_info-section").length > 0)) {
stripeProcessorId = $('#stripe-id').val();
chosenProcessorId = $form.find('input[name="payment_processor_id"]:checked').val();
if ((form.querySelector(".crm-section.payment_processor-section") !== null)
|| (form.querySelector(".crm-section.credit_card_info-section") !== null)) {
stripeProcessorId = CRM.vars.stripe.id;
if (form.querySelector('input[name="payment_processor_id"]:checked') !== null) {
chosenProcessorId = form.querySelector('input[name="payment_processor_id"]:checked').value;
}
}
}
......@@ -251,8 +308,8 @@ CRM.$(function($) {
// - Is the Stripe processor ID defined?
// - Is selected processor ID and stripe ID undefined? If we only have stripe ID, then there is only one (stripe) processor on the page
if ((chosenProcessorId === 0)
|| (stripeProcessorId == null)
|| ((chosenProcessorId == null) && (stripeProcessorId == null))) {
|| (stripeProcessorId == null)
|| ((chosenProcessorId == null) && (stripeProcessorId == null))) {
debugging('Not a Stripe transaction, or pay-later');
return true;
}
......@@ -260,22 +317,18 @@ CRM.$(function($) {
debugging('Stripe is the selected payprocessor');
}
$form = getBillingForm();
// Don't handle submits generated by non-stripe processors
if (!$('input#stripe-pub-key').length || !($('input#stripe-pub-key').val())) {
if (typeof CRM.vars.stripe.publishableKey === 'undefined') {
debugging('submit missing stripe-pub-key element or value');
return true;
}
// Don't handle submits generated by the CiviDiscount button.
if ($form.data('submit-dont-process')) {
if (form.dataset.submitdontprocess === true) {
debugging('non-payment submit detected - not submitting payment');
return true;
}
$submit = getBillingSubmit();
if (isWebform) {
if (getIsDrupalWebform()) {
// If we have selected Stripe but amount is 0 we don't submit via Stripe
if ($('#billing-payment-block').is(':hidden')) {
debugging('no payment processor on webform');
......@@ -292,111 +345,124 @@ CRM.$(function($) {
}
}
// This is ONLY triggered in the following circumstances on a CiviCRM contribution page:
// - With a priceset that allows a 0 amount to be selected.
// - When Stripe is the ONLY payment processor configured on the page.
if (typeof calculateTotalFee == 'function') {
var totalFee = calculateTotalFee();
if (totalFee == '0') {
debugging("Total amount is 0");
return true;
}
}
// If there's no credit card field, no use in continuing (probably wrong
// context anyway)
if (!$form.find('#credit_card_number').length) {
debugging('No credit card field');
var totalFee = getTotalAmount();
if (totalFee == '0') {
debugging("Total amount is 0");
return true;
}
// Lock to prevent multiple submissions
if ($form.data('submitted') === true) {
if (form.dataset.submitted === true) {
// Previously submitted - don't submit again
alert('Form already submitted. Please wait.');
return false;
} else {
// Mark it so that the next submit can be ignored
// ADDED requirement that form be valid
if($form.valid()) {
$form.data('submitted', true);
}
form.dataset.submitted = true;
}
// Disable the submit button to prevent repeated clicks
$submit.prop('disabled', true);
var cc_month = $form.find('#credit_card_exp_date_M').val();
var cc_year = $form.find('#credit_card_exp_date_Y').val();
Stripe.card.createToken({
name: $form.find('#billing_first_name')
.val() + ' ' + $form.find('#billing_last_name').val(),
address_zip: $form.find('#billing_postal_code-5').val(),
number: $form.find('#credit_card_number').val(),
cvc: $form.find('#cvv2').val(),
exp_month: cc_month,
exp_year: cc_year
}, stripeResponseHandler);
debugging('Created Stripe token');
return false;
submitButton.setAttribute('disabled', true);
// Create a token when the form is submitted.
handleCardPayment();
return true;
}
}
function getIsWebform(form) {
// Pass in the billingForm object
// If the form has the webform-client-form (drupal 7) or webform-submission-form (drupal 8) class then it's a drupal webform!
return form.hasClass('webform-client-form') || form.hasClass('webform-submission-form');
function getIsDrupalWebform() {
// form class for drupal webform: webform-client-form (drupal 7); webform-submission-form (drupal 8)
if (form !== null) {
return form.classList.contains('webform-client-form') || form.classList.contains('webform-submission-form');
}
return false;
}
function getBillingForm() {
// If we have a stripe billing form on the page
var $billingForm = $('input#stripe-pub-key').closest('form');
//if (!$billingForm.length && getIsWebform()) {
// If we are in a webform
// $billingForm = $('.webform-client-form');
//}
if (!$billingForm.length) {
var billingFormID = $('div#card-element').closest('form').prop('id');
if (!billingFormID.length) {
// If we have multiple payment processors to select and stripe is not currently loaded
$billingForm = $('input[name=hidden_processor]').closest('form');
billingFormID = $('input[name=hidden_processor]').closest('form').prop('id');
}
return $billingForm;
// We have to use document.getElementById here so we have the right elementtype for appendChild()
return document.getElementById(billingFormID);
}
function getBillingSubmit() {
$form = getBillingForm();
var isWebform = getIsWebform($form);
if (isWebform) {
$submit = $form.find('[type="submit"].webform-submit');
if (!$submit.length) {
var submit = null;
if (getIsDrupalWebform()) {
submit = form.querySelector('[type="submit"].webform-submit');
if (!submit) {
// drupal 8 webform
$submit = $form.find('[type="submit"].webform-button--submit');
submit = form.querySelector('[type="submit"].webform-button--submit');
}
}
else {
$submit = $form.find('[type="submit"].validate');
submit = form.querySelector('[type="submit"].validate');
}
return $submit;
return submit;
}
function removeCCDetails($form, $truncate) {
// Remove the "name" attribute so params are not submitted
var ccNumElement = $form.find("input#credit_card_number");
var cvv2Element = $form.find("input#cvv2");
if ($truncate) {
ccNumElement.val('');
cvv2Element.val('');
function getTotalAmount() {
var totalFee = null;
if (typeof calculateTotalFee == 'function') {
// This is ONLY triggered in the following circumstances on a CiviCRM contribution page:
// - With a priceset that allows a 0 amount to be selected.
// - When Stripe is the ONLY payment processor configured on the page.
totalFee = calculateTotalFee();
}
else {
var last4digits = ccNumElement.val().substr(12, 16);
ccNumElement.val('000000000000' + last4digits);
cvv2Element.val('000');
else if (getIsDrupalWebform()) {
// This is how webform civicrm calculates the amount in webform_civicrm_payment.js
$('.line-item:visible', '#wf-crm-billing-items').each(function() {
totalFee += parseFloat($(this).data('amount'));
});
}
return totalFee;
}
function updateFormElementsFromCreditCardDetails(event) {
if (!event.complete) {
return;
}
document.getElementById('billing_postal_code-' + CRM.vars.stripe.billingAddressID).value = event.value.postalCode;
}
function addSupportForCiviDiscount() {
// Add a keypress handler to set flag if enter is pressed
cividiscountElements = form.querySelectorAll('input#discountcode');
for (i = 0; i < cividiscountElements.length; ++i) {
cividiscountElements[i].addEventListener('keydown', function (e) {
if (e.keyCode === 13) {
e.preventDefault();
debugging('adding submitdontprocess');
form.dataset.submitdontprocess = true;
}
});
}
}
function debugging (errorCode) {
// Uncomment the following to debug unexpected returns.
//console.log(new Date().toISOString() + ' civicrm_stripe.js: ' + errorCode);
if ((typeof(CRM.vars.stripe) === 'undefined') || (Boolean(CRM.vars.stripe.jsDebug) === true)) {
console.log(new Date().toISOString() + ' civicrm_stripe.js: ' + errorCode);
}
}
function addDrupalWebformActionElement(submitAction) {
var hiddenInput = null;
if (document.getElementById('action') !== null) {
hiddenInput = document.getElementById('action');
}
else {
hiddenInput = document.createElement('input');
}
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'op');
hiddenInput.setAttribute('id', 'action');
hiddenInput.setAttribute('value', submitAction);
form.appendChild(hiddenInput);
}
});