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 (22)
Showing
with 933 additions and 1096 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-08-14';
/**
* 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,6 +136,20 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
return FALSE;
}
public function supportsRecurring() {
// @fixme: Test and make this work for stripe elements / 6.0
return FALSE;
}
/**
* Does this payment processor support refund?
*
* @return bool
*/
public function supportsRefund() {
return TRUE;
}
/**
* We can configure a start date for a smartdebit mandate
* @return bool
......@@ -173,14 +192,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 +228,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 +246,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 +271,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 +283,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 +317,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 +357,23 @@ 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');
}
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,32 +384,25 @@ 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
......@@ -484,126 +413,87 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
// 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);
}
}
}
// Prepare the charge array, minus Customer/Card details.
if (empty($params['description'])) {
$params['description'] = ts('Backend Stripe contribution');
$params['description'] = E::ts('Backend Stripe contribution');
}
// This is where we actually charge the customer
try {
$intent = \Stripe\PaymentIntent::retrieve($paymentIntentID);
$intent->description = $params['description'] . ' # Invoice ID: ' . CRM_Utils_Array::value('invoiceID', $params);
$intent->customer = $stripeCustomer->id;
switch ($intent->status) {
case 'requires_confirmation':
$intent->confirm();
case 'requires_capture':
$intent->capture(['amount_to_capture' => $this->getAmount($params)]);
break;
}
}
catch (Exception $e) {
$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);
}
// Stripe charge.
$stripeChargeParams = [
'amount' => $amount,
'currency' => strtolower($params['currencyID']),
'description' => $params['description'] . ' # Invoice ID: ' . CRM_Utils_Array::value('invoiceID', $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;
}
try {
$stripeCharge = \Stripe\Charge::create($stripeChargeParams);
}
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);
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;
$this->setPaymentProcessorOrderID($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 +504,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 +514,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 +526,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,6 +533,7 @@ 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);
......@@ -677,6 +567,38 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
return $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());
}
}
/**
* Calculate the end_date for a recurring contribution based on the number of installments
* @param $params
......@@ -770,74 +692,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 +717,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,8 +54,8 @@ 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.
......@@ -110,7 +117,7 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
* We override base because our input parameter is an object
*
* @param array $parameters
*/
*/
public function setInputParameters($parameters) {
if (!is_object($parameters)) {
$this->exception('Invalid input parameters');
......@@ -119,14 +126,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,8 +161,8 @@ 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;
}
......@@ -168,7 +174,6 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
public function main() {
// Collect and determine all data about this event.
$this->event_type = CRM_Stripe_Api::getParam('event_type', $this->_inputParameters);
$pendingStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
// NOTE: If you add an event here make sure you add it to the webhook or it will never be received!
......@@ -176,14 +181,14 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
// Successful recurring payment.
case 'invoice.payment_succeeded':
$this->setInfo();
if ($this->previous_contribution['contribution_status_id'] == $pendingStatusId) {
if ($this->contribution['contribution_status_id'] == $pendingStatusId) {
$this->completeContribution();
}
elseif ($this->previous_contribution['trxn_id'] != $this->charge_id) {
elseif ($this->contribution['trxn_id'] != $this->charge_id) {
// The first contribution was completed, so create a new one.
// api contribution repeattransaction repeats the appropriate contribution if it is given
// simply the recurring contribution id. It also updates the membership for us.
civicrm_api3('Contribution', 'repeattransaction', array(
$repeatParams = [
'contribution_recur_id' => $this->contribution_recur_id,
'contribution_status_id' => 'Completed',
'receive_date' => $this->receive_date,
......@@ -191,15 +196,19 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
'total_amount' => $this->amount,
'fee_amount' => $this->fee,
'is_email_receipt' => $this->getSendEmailReceipt(),
));
];
if ($this->previous_contribution) {
$repeatParams['original_contribution_id'] = $this->previous_contribution['id'];
}
civicrm_api3('Contribution', 'repeattransaction', $repeatParams);
}
// Successful charge & more to come.
civicrm_api3('ContributionRecur', 'create', array(
// Successful charge & more to come.
civicrm_api3('ContributionRecur', 'create', [
'id' => $this->contribution_recur_id,
'failure_count' => 0,
'contribution_status_id' => 'In Progress'
));
]);
return TRUE;
// Failed recurring payment.
......@@ -207,14 +216,14 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
$this->setInfo();
$failDate = date('YmdHis');
if ($this->previous_contribution['contribution_status_id'] == $pendingStatusId) {
if ($this->contribution['contribution_status_id'] == $pendingStatusId) {
// If this contribution is Pending, set it to Failed.
civicrm_api3('Contribution', 'create', array(
'id' => $this->previous_contribution['id'],
civicrm_api3('Contribution', 'create', [
'id' => $this->contribution['id'],
'contribution_status_id' => "Failed",
'receive_date' => $failDate,
'is_email_receipt' => 0,
));
]);
}
else {
$contributionParams = [
......@@ -227,96 +236,98 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
civicrm_api3('Contribution', 'repeattransaction', $contributionParams);
}
$failureCount = civicrm_api3('ContributionRecur', 'getvalue', array(
'id' => $this->contribution_recur_id,
'return' => 'failure_count',
));
$failureCount = civicrm_api3('ContributionRecur', 'getvalue', [
'id' => $this->contribution_recur_id,
'return' => 'failure_count',
]);
$failureCount++;
// Change the status of the Recurring and update failed attempts.
civicrm_api3('ContributionRecur', 'create', array(
civicrm_api3('ContributionRecur', 'create', [
'id' => $this->contribution_recur_id,
'contribution_status_id' => "Failed",
'failure_count' => $failureCount,
'modified_date' => $failDate,
));
]);
return TRUE;
// Subscription is cancelled
case 'customer.subscription.deleted':
$this->setInfo();
// Cancel the recurring contribution
civicrm_api3('ContributionRecur', 'cancel', array(
civicrm_api3('ContributionRecur', 'cancel', [
'id' => $this->contribution_recur_id,
));
]);
return TRUE;
// One-time donation and per invoice payment.
case 'charge.failed':
$chargeId = $this->retrieve('charge_id', 'String');
$failureCode = $this->retrieve('failure_code', 'String');
$failureMessage = $this->retrieve('failure_message', 'String');
$contribution = civicrm_api3('Contribution', 'getsingle', ['trxn_id' => $chargeId]);
$failedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Failed');
if ($contribution['contribution_status_id'] != $failedStatusId) {
$note = $failureCode . ' : ' . $failureMessage;
civicrm_api3('Contribution', 'create', ['id' => $contribution['id'], 'contribution_status_id' => $failedStatusId, 'note' => $note]);
$chargeId = $this->retrieve('charge_id', 'String');
// @fixme: Check if "note" param actually does anything!
try {
$contribution = civicrm_api3('Contribution', 'getsingle', [
'trxn_id' => $chargeId,
'contribution_test' => $this->_paymentProcessor->getIsTestMode(),
'return' => 'id'
]);
}
catch (Exception $e) {
// No failed contribution found, we won't record in CiviCRM for now
return TRUE;
}
$params = [
'note' => "{$failureCode} : {$failureMessage}",
'contribution_id' => $contribution['id'],
];
$this->recordFailed($params);
return TRUE;
case 'charge.refunded':
$chargeId = $this->retrieve('charge_id', 'String');
$refunded = $this->retrieve('refunded', 'Boolean');
$refundAmount = $this->retrieve('amount_refunded', 'Integer');
$contribution = civicrm_api3('Contribution', 'getsingle', ['trxn_id' => $chargeId]);
if ($refunded) {
$refundedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Refunded');
if ($contribution['contribution_status_id'] != $refundedStatusId) {
civicrm_api3('Contribution', 'create', [
'id' => $contribution['id'],
'contribution_status_id' => $refundedStatusId
]);
}
elseif ($refundAmount > 0) {
$partiallyRefundedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Partially Refunded');
if ($contribution['contribution_status_id'] != $partiallyRefundedStatusId) {
civicrm_api3('Contribution', 'create', [
'id' => $contribution['id'],
'contribution_status_id' => $refundedStatusId
]);
}
}
}
$refunds = \Stripe\Refund::all(['charge' => $chargeId, 'limit' => 1]);
$params = [
'contribution_id' => civicrm_api3('Contribution', 'getvalue', [
'trxn_id' => $chargeId,
'contribution_test' => $this->_paymentProcessor->getIsTestMode(),
'return' => 'id'
]),
'total_amount' => $this->retrieve('amount_refunded', 'Float'),
'cancel_reason' => $refunds->data[0]->reason,
'cancel_date' => date('YmdHis', $refunds->data[0]->created),
];
$this->recordRefund($params);
return TRUE;
case 'charge.succeeded':
$this->setInfo();
if ($this->previous_contribution['contribution_status_id'] == $pendingStatusId) {
$this->completeContribution();
if ($this->contribution['contribution_status_id'] == $pendingStatusId) {
$this->recordCompleted(['contribution_id' => $this->contribution['id']]);
}
return TRUE;
case 'customer.subscription.updated':
$this->setInfo();
if (empty($this->previous_plan_id)) {
// Not a plan change...don't care.
return TRUE;
}
civicrm_api3('ContributionRecur', 'create', [
'id' => $this->contribution_recur_id,
$this->setInfo();
if (empty($this->previous_plan_id)) {
// Not a plan change...don't care.
return TRUE;
}
civicrm_api3('ContributionRecur', 'create', [
'id' => $this->contribution_recur_id,
'amount' => $this->plan_amount,
'auto_renew' => 1,
'created_date' => $this->plan_start,
'frequency_unit' => $this->frequency_unit,
'frequency_interval' => $this->frequency_interval,
]);
]);
civicrm_api3('Contribution', 'create', [
'id' => $this->previous_contribution['id'],
civicrm_api3('Contribution', 'create', [
'id' => $this->contribution['id'],
'total_amount' => $this->plan_amount,
'contribution_recur_id' => $this->contribution_recur_id,
]);
]);
return TRUE;
}
// Unhandled event type.
......@@ -330,36 +341,35 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
*/
public function completeContribution() {
// Update the contribution to include the fee.
civicrm_api3('Contribution', 'create', array(
'id' => $this->previous_contribution['id'],
civicrm_api3('Contribution', 'create', [
'id' => $this->contribution['id'],
'total_amount' => $this->amount,
'fee_amount' => $this->fee,
'net_amount' => $this->net_amount,
));
]);
// The last one was not completed, so complete it.
civicrm_api3('Contribution', 'completetransaction', array(
'id' => $this->previous_contribution['id'],
civicrm_api3('Contribution', 'completetransaction', [
'id' => $this->contribution['id'],
'trxn_date' => $this->receive_date,
'trxn_id' => $this->charge_id,
'total_amount' => $this->amount,
'net_amount' => $this->net_amount,
'fee_amount' => $this->fee,
'payment_processor_id' => $this->_paymentProcessor['id'],
'payment_processor_id' => $this->_paymentProcessor->getPaymentProcessor()['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 +387,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,16 +422,28 @@ 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 yet created?
}
}
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) {
......@@ -434,10 +452,4 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
}
}
public function exception($message) {
$errorMessage = 'StripeIPN Exception: Event: ' . $this->event_type . ' Error: ' . $message;
Civi::log()->debug($errorMessage);
http_response_code(400);
exit(1);
}
}
<?php
/**
* Shared payment IPN functions that should one day be migrated to CiviCRM core
* Version: 20190304
*/
trait CRM_Core_Payment_StripeIPNTrait {
/**
* @var array Payment processor
*/
private $_paymentProcessor;
/**
* Get the payment processor
* The $_GET['processor_id'] value is set by CRM_Core_Payment::handlePaymentMethod.
*/
protected function getPaymentProcessor() {
$paymentProcessorId = (int) CRM_Utils_Array::value('processor_id', $_GET);
if (empty($paymentProcessorId)) {
$this->exception('Failed to get payment processor id');
}
try {
$this->_paymentProcessor = \Civi\Payment\System::singleton()->getById($paymentProcessorId)->getPaymentProcessor();
}
catch(Exception $e) {
$this->exception('Failed to get payment processor');
}
}
/**
* Mark a contribution as cancelled and update related entities
*
* @param array $params [ 'id' -> contribution_id, 'payment_processor_id' -> payment_processor_id]
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function canceltransaction($params) {
return $this->incompletetransaction($params, 'cancel');
}
/**
* Mark a contribution as failed and update related entities
*
* @param array $params [ 'id' -> contribution_id, 'payment_processor_id' -> payment_processor_id]
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function failtransaction($params) {
return $this->incompletetransaction($params, 'fail');
}
/**
* Handler for failtransaction and canceltransaction - do not call directly
*
* @param array $params
* @param string $mode
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function incompletetransaction($params, $mode) {
$requiredParams = ['id', 'payment_processor_id'];
foreach ($requiredParams as $required) {
if (!isset($params[$required])) {
$this->exception('canceltransaction: Missing mandatory parameter: ' . $required);
}
}
if (isset($params['payment_processor_id'])) {
$input['payment_processor_id'] = $params['payment_processor_id'];
}
$contribution = new CRM_Contribute_BAO_Contribution();
$contribution->id = $params['id'];
if (!$contribution->find(TRUE)) {
throw new CiviCRM_API3_Exception('A valid contribution ID is required', 'invalid_data');
}
if (!$contribution->loadRelatedObjects($input, $ids, TRUE)) {
throw new CiviCRM_API3_Exception('failed to load related objects');
}
$input['trxn_id'] = !empty($params['trxn_id']) ? $params['trxn_id'] : $contribution->trxn_id;
if (!empty($params['fee_amount'])) {
$input['fee_amount'] = $params['fee_amount'];
}
$objects['contribution'] = &$contribution;
$objects = array_merge($objects, $contribution->_relatedObjects);
$transaction = new CRM_Core_Transaction();
switch ($mode) {
case 'cancel':
return $this->cancelled($objects, $transaction);
case 'fail':
return $this->failed($objects, $transaction);
default:
throw new CiviCRM_API3_Exception('Unknown incomplete transaction type: ' . $mode);
}
}
}
<?php
/**
* Shared payment functions that should one day be migrated to CiviCRM core
*/
trait CRM_Core_Payment_StripeTrait {
/**********************
* Version 20190313
*********************/
/**
* Get the billing email address
*
* @param array $params
* @param int $contactId
*
* @return string|NULL
*/
protected function getBillingEmail($params, $contactId) {
$billingLocationId = CRM_Core_BAO_LocationType::getBilling();
$emailAddress = CRM_Utils_Array::value("email-{$billingLocationId}", $params,
CRM_Utils_Array::value('email-Primary', $params,
CRM_Utils_Array::value('email', $params, NULL)));
if (empty($emailAddress) && !empty($contactId)) {
// Try and retrieve an email address from Contact ID
try {
$emailAddress = civicrm_api3('Email', 'getvalue', array(
'contact_id' => $contactId,
'return' => ['email'],
));
}
catch (CiviCRM_API3_Exception $e) {
return NULL;
}
}
return $emailAddress;
}
/**
* Get the contact id
*
* @param array $params
*
* @return int ContactID
*/
protected function getContactId($params) {
// contactID is set by: membership payment workflow
$contactId = CRM_Utils_Array::value('contactID', $params,
CRM_Utils_Array::value('contact_id', $params,
CRM_Utils_Array::value('cms_contactID', $params,
CRM_Utils_Array::value('cid', $params, NULL
))));
if (!empty($contactId)) {
return $contactId;
}
// FIXME: Ref: https://lab.civicrm.org/extensions/stripe/issues/16
// The problem is that when registering for a paid event, civicrm does not pass in the
// contact id to the payment processor (civicrm version 5.3). So, I had to patch your
// getContactId to check the session for a contact id. It's a hack and probably should be fixed in core.
// The code below is exactly what CiviEvent does, but does not pass it through to the next function.
$session = CRM_Core_Session::singleton();
return $session->get('transaction.userID', NULL);
}
/**
* Get the contribution ID
*
* @param $params
*
* @return mixed
*/
protected function getContributionId($params) {
/*
* contributionID is set in the contribution workflow
* We do NOT have a contribution ID for event and membership payments as they are created after payment!
* See: https://github.com/civicrm/civicrm-core/pull/13763 (for events)
*/
return CRM_Utils_Array::value('contributionID', $params);
}
/**
* Get the recurring contribution ID from parameters passed in to cancelSubscription
* Historical the data passed to cancelSubscription is pretty poor and doesn't include much!
*
* @param array $params
*
* @return int|null
*/
protected function getRecurringContributionId($params) {
// Not yet passed, but could be added via core PR
$contributionRecurId = CRM_Utils_Array::value('contribution_recur_id', $params);
if (!empty($contributionRecurId)) {
return $contributionRecurId;
}
// Not yet passed, but could be added via core PR
$contributionId = CRM_Utils_Array::value('contribution_id', $params);
try {
return civicrm_api3('Contribution', 'getvalue', ['id' => $contributionId, 'return' => 'contribution_recur_id']);
}
catch (Exception $e) {
$subscriptionId = CRM_Utils_Array::value('subscriptionId', $params);
if (!empty($subscriptionId)) {
try {
return civicrm_api3('ContributionRecur', 'getvalue', ['processor_id' => $subscriptionId, 'return' => 'id']);
}
catch (Exception $e) {
return NULL;
}
}
return NULL;
}
}
/**
*
* @param array $params ['name' => payment instrument name]
*
* @return int|null
* @throws \CiviCRM_API3_Exception
*/
public static function createPaymentInstrument($params) {
$mandatoryParams = ['name'];
foreach ($mandatoryParams as $value) {
if (empty($params[$value])) {
Civi::log()->error('createPaymentInstrument: Missing mandatory parameter: ' . $value);
return NULL;
}
}
// Create a Payment Instrument
// See if we already have this type
$paymentInstrument = civicrm_api3('OptionValue', 'get', array(
'option_group_id' => "payment_instrument",
'name' => $params['name'],
));
if (empty($paymentInstrument['count'])) {
// Otherwise create it
try {
$financialAccount = civicrm_api3('FinancialAccount', 'getsingle', [
'financial_account_type_id' => "Asset",
'name' => "Payment Processor Account",
]);
}
catch (Exception $e) {
$financialAccount = civicrm_api3('FinancialAccount', 'getsingle', [
'financial_account_type_id' => "Asset",
'name' => "Payment Processor Account",
'options' => ['limit' => 1, 'sort' => "id ASC"],
]);
}
$paymentParams = [
'option_group_id' => "payment_instrument",
'name' => $params['name'],
'description' => $params['name'],
'financial_account_id' => $financialAccount['id'],
];
$paymentInstrument = civicrm_api3('OptionValue', 'create', $paymentParams);
$paymentInstrumentId = $paymentInstrument['values'][$paymentInstrument['id']]['value'];
}
else {
$paymentInstrumentId = $paymentInstrument['id'];
}
return $paymentInstrumentId;
}
}
<?php
/**
* https://civicrm.org/licensing
*/
/**
* Class CRM_Stripe_AJAX
*/
class CRM_Stripe_AJAX {
public static function getClientSecret() {
$amount = CRM_Utils_Request::retrieveValue('amount', 'Money', NULL, TRUE);
$currency = CRM_Utils_Request::retrieveValue('currency', 'String', CRM_Core_Config::singleton()->defaultCurrency);
$processorID = CRM_Utils_Request::retrieveValue('id', 'Integer', NULL, TRUE);
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $processorID]));
$processor->setAPIParams();
$intent = \Stripe\PaymentIntent::create([
'amount' => $processor->getAmount(['amount' => $amount]),
'currency' => $currency,
]);
CRM_Utils_JSON::output(['client_secret' => $intent->client_secret]);
}
public static function confirmPayment() {
$paymentMethodID = CRM_Utils_Request::retrieveValue('payment_method_id', 'String', NULL, TRUE);
$paymentIntentID = CRM_Utils_Request::retrieveValue('payment_intent_id', 'String');
$amount = CRM_Utils_Request::retrieveValue('amount', 'Money', NULL, TRUE);
$currency = CRM_Utils_Request::retrieveValue('currency', 'String', CRM_Core_Config::singleton()->defaultCurrency);
$processorID = CRM_Utils_Request::retrieveValue('id', 'Integer', NULL, TRUE);
$processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $processorID]));
$processor->setAPIParams();
if ($paymentIntentID) {
$intent = \Stripe\PaymentIntent::retrieve($paymentIntentID);
$intent->confirm([
'payment_method' => $paymentMethodID,
]);
}
else {
$intent = \Stripe\PaymentIntent::create([
'payment_method' => $paymentMethodID,
'amount' => $processor->getAmount(['amount' => $amount]),
'currency' => $currency,
'confirmation_method' => 'manual',
'capture_method' => 'manual', // authorize the amount but don't take from card yet
'setup_future_usage' => 'off_session', // Setup the card to be saved and used later
'confirm' => TRUE,
]);
}
self::generatePaymentResponse($intent);
}
private static function generatePaymentResponse($intent) {
if ($intent->status == 'requires_action' &&
$intent->next_action->type == 'use_stripe_sdk') {
// Tell the client to handle the action
CRM_Utils_JSON::output([
'requires_action' => true,
'payment_intent_client_secret' => $intent->client_secret,
//'payment_method_id' => $intent->payment_method,
]);
} else if ($intent->status == 'requires_capture') {
// The payment intent has been confirmed, we just need to capture the payment
// Handle post-payment fulfillment
CRM_Utils_JSON::output([
'success' => true,
'paymentIntent' => ['id' => $intent->id],
]);
} else {
// Invalid status
CRM_Utils_JSON::output(['error' => ['message' => 'Invalid PaymentIntent status']]);
}
}
}
......@@ -20,11 +20,14 @@ 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;
}
break;
......@@ -64,7 +67,7 @@ class CRM_Stripe_Api {
case 'description':
return (string) $stripeObject->description;
case 'customer_id':
return (string) $stripeObject->customer;
......
<?php
/**
* https://civicrm.org/licensing
*/
use CRM_Stripe_ExtensionUtil as E;
/**
* Class CRM_Stripe_Check
*/
class CRM_Stripe_Check {
const MIN_VERSION_MJWSHARED = 0.2;
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,13 +108,14 @@ class CRM_Stripe_Customer {
}
/**
* @param $params
* @param array $params
* @param \CRM_Core_Payment_Stripe $stripe
*
* @return \Stripe\ApiResource
* @throws \CiviCRM_API3_Exception
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public static function create($params) {
public static function create($params, $stripe) {
$requiredParams = ['contact_id', 'card_token', 'processor_id'];
// $optionalParams = ['email'];
foreach ($requiredParams as $required) {
......@@ -130,7 +131,6 @@ class CRM_Stripe_Customer {
$stripeCustomerParams = [
'description' => $contactDisplayName . ' (CiviCRM)',
'card' => $params['card_token'],
'email' => CRM_Utils_Array::value('email', $params),
'metadata' => ['civicrm_contact_id' => $params['contact_id']],
];
......@@ -140,7 +140,7 @@ class CRM_Stripe_Customer {
}
catch (Exception $e) {
$err = CRM_Core_Payment_Stripe::parseStripeException('create_customer', $e, FALSE);
$errorMessage = CRM_Core_Payment_Stripe::handleErrorNotification($err, $params['stripe_error_url']);
$errorMessage = $stripe->handleErrorNotification($err, $params['stripe_error_url']);
throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to create Stripe Customer: ' . $errorMessage);
}
......
<?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-08-14
* 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.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-08-14 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-06</releaseDate>
<version>6.0.alpha2</version>
<develStage>alpha</develStage>
<compatibility>
<ver>5.13</ver>
</compatibility>
<comments>Original Author: Joshua Walker (drastik) - Drastik by Design.
Jamie Mcclelland (ProgressiveTech) did a lot of the 5.x compatibility work.
</comments>
<requires>
<ext>mjwshared</ext>
</requires>
<civix>
<namespace>CRM/Stripe</namespace>
</civix>
......
This diff is collapsed.