From eb093d0cb60eca81ac81e053e681d071b1264d4d Mon Sep 17 00:00:00 2001 From: drastik <jwjoshuawalker@gmail.com> Date: Fri, 10 Aug 2012 05:05:47 -0700 Subject: [PATCH] Several changes. See README --- CRM/Core/Payment/Stripe.php | 86 +++++--- README.txt | 37 +++- civicrm_stripe.sql | 2 +- civicrm_templates/CRM/Core/BillingBlock.tpl | 221 ++++++++++++++++++++ 4 files changed, 305 insertions(+), 41 deletions(-) create mode 100644 civicrm_templates/CRM/Core/BillingBlock.tpl diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index b3121459..d51bc6a3 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -59,10 +59,14 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { function checkConfig() { $config = CRM_Core_Config::singleton(); $error = array(); - + if (empty($this->_paymentProcessor['user_name'])) { $error[] = ts('The "Secret Key" is not set in the Stripe Payment Processor settings.'); } + + if (empty($this->_paymentProcessor['password'])) { + $error[] = ts('The "Publishable Key" is not set in the Stripe Payment Processor settings.'); + } if (!empty($error)) { return implode('<p>', $error); @@ -85,12 +89,6 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { //Include Stripe library & Set API credentials. require_once("stripe-php/lib/Stripe.php"); Stripe::setApiKey($this->_paymentProcessor['user_name']); - - $cc_name = $params['first_name'] . " "; - if (strlen($params['middle_name']) > 0) { - $cc_name .= $params['middle_name'] . " "; - } - $cc_name .= $params['last_name']; //Stripe amount required in cents. $amount = $params['amount'] * 100; @@ -98,12 +96,27 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { $amount = number_format($amount, 0, '', ''); //Check for existing customer, create new otherwise. - $stripe_customer_id = ""; $email = $params['email']; - $customer_query = "SELECT id FROM civicrm_stripe_customers WHERE email = '$email'"; - $customer_query_res = CRM_Core_DAO::singleValueQuery($customer_query); - + $customer_query = CRM_Core_DAO::singleValueQuery("SELECT id FROM civicrm_stripe_customers WHERE email = '$email'"); + + //Use Stripe.js instead of raw card details. + if(isset($params['stripe_token'])) { + $card_details = $params['stripe_token']; + } else { + CRM_Core_Error::fatal(ts('Stripe.js token was not passed! Have you turned on the CiviCRM-Stripe CMS module?')); + } + + /**** + * If for some reason you cannot use Stripe.js and you are aware of PCI Compliance issues, here is the alternative to Stripe.js: + ****/ //Prepare Card details in advance to use for new Stripe Customer object if we need. +/* + $cc_name = $params['first_name'] . " "; + if (strlen($params['middle_name']) > 0) { + $cc_name .= $params['middle_name'] . " "; + } + $cc_name .= $params['last_name']; + $card_details = array( 'number' => $params['credit_card_number'], 'exp_month' => $params['month'], @@ -113,28 +126,26 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { 'address_line1' => $params['street_address'], 'address_state' => $params['state_province'], 'address_zip' => $params['postal_code'], - //'address_country' => $params['country'] ); + */ //Create a new Customer in Stripe - if(!isset($customer_query_res)) { + if(!isset($customer_query)) { $stripe_customer = Stripe_Customer::create(array( - 'description' => 'Donor from CiviCRM', + 'description' => 'Payment from CiviCRM', 'card' => $card_details, 'email' => $email, )); //Store the relationship between CiviCRM's email address for the Contact & Stripe's Customer ID if(isset($stripe_customer)) { - $stripe_customer_id = $stripe_customer->id; - CRM_Core_DAO::executeQuery("INSERT INTO civicrm_stripe_customers (email, id) VALUES ('$email', '$stripe_customer_id')"); + CRM_Core_DAO::executeQuery("INSERT INTO civicrm_stripe_customers (email, id) VALUES ('$email', '$stripe_customer->id')"); } else { CRM_Core_Error::fatal(ts('There was an error saving new customer within Stripe. Is Stripe down?')); } } else { - $stripe_customer = Stripe_Customer::retrieve($customer_query_res); + $stripe_customer = Stripe_Customer::retrieve($customer_query); if(!empty($stripe_customer)) { - $stripe_customer_id = $customer_query_res; $stripe_customer->card = $card_details; $stripe_customer->save(); } else { @@ -147,9 +158,8 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { //Somehow a customer ID saved in the system no longer pairs with a Customer within Stripe. (Perhaps deleted using Stripe interface?) //Store the relationship between CiviCRM's email address for the Contact & Stripe's Customer ID if(isset($stripe_customer)) { - $stripe_customer_id = $stripe_customer->id; CRM_Core_DAO::executeQuery("DELETE FROM civicrm_stripe_customers WHERE email = '$email'"); - CRM_Core_DAO::executeQuery("INSERT INTO civicrm_stripe_customers (email, id) VALUES ('$email', '$stripe_customer_id')"); + CRM_Core_DAO::executeQuery("INSERT INTO civicrm_stripe_customers (email, id) VALUES ('$email', '$stripe_customer->id')"); } else { CRM_Core_Error::fatal(ts('There was an error saving new customer within Stripe. Is Stripe down?')); } @@ -164,15 +174,15 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { ); //Use Stripe Customer if we have a valid one. Otherwise just use the card. - if(!empty($stripe_customer_id)) { - $stripe_charge['customer'] = $stripe_customer_id; + if(!empty($stripe_customer->id)) { + $stripe_charge['customer'] = $stripe_customer->id; } else { $stripe_charge['card'] = $card_details; } //Handle recurring payments in doRecurPayment(). if (CRM_Utils_Array::value('is_recur', $params) && $params['contributionRecurID']) { - return $this->doRecurPayment($params, $amount, $stripe_customer, $card_details); + return $this->doRecurPayment($params, $amount, $stripe_customer); } //Fire away! @@ -182,15 +192,14 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { return $params; } - function doRecurPayment(&$params, $amount, $stripe_customer, $card_details) { + function doRecurPayment(&$params, $amount, $stripe_customer) { $frequency = $params['frequency_unit']; $installments = $params['installments']; $plan_id = "$frequency-$amount"; - $stripe_plan_query = "SELECT plan_id FROM civicrm_stripe_plans WHERE plan_id = '$plan_id'"; - $stripe_plan_query_res = CRM_Core_DAO::singleValueQuery($stripe_plan_query); - - if(!isset($stripe_plan_query_res)) { + $stripe_plan_query = CRM_Core_DAO::singleValueQuery("SELECT plan_id FROM civicrm_stripe_plans WHERE plan_id = '$plan_id'"); + + if(!isset($stripe_plan_query)) { $formatted_amount = "$" . number_format(($amount / 100), 2); //Create a new Plan $stripe_plan = Stripe_Plan::create(array( @@ -199,19 +208,28 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { "name" => "CiviCRM $frequency" . 'ly ' . $formatted_amount, "currency" => "usd", "id" => $plan_id)); - $new_plan_insert = "INSERT INTO civicrm_stripe_plans (plan_id) VALUES ('$plan_id')"; - CRM_Core_DAO::executeQuery($new_plan_insert); + CRM_Core_DAO::executeQuery("INSERT INTO civicrm_stripe_plans (plan_id) VALUES ('$plan_id')"); } //Attach the Subscription to the Stripe Customer - $stripe_response = $stripe_customer->updateSubscription(array('prorate' => FALSE, 'plan' => $plan_id, 'card' => $card_details)); + $stripe_response = $stripe_customer->updateSubscription(array('prorate' => FALSE, 'plan' => $plan_id)); + $existing_subscription_query = CRM_Core_DAO::singleValueQuery("SELECT invoice_id FROM civicrm_stripe_subscriptions WHERE customer_id = '$stripe_customer->id'"); + if(!empty($existing_subscription_query)) { + //Cancel existing Recurring Contribution in CiviCRM + $cancel_date = date("Y-m-d H:i:s"); + CRM_Core_DAO::executeQuery("UPDATE civicrm_contribution_recur SET cancel_date = '$cancel_date', contribution_status_id = '3' WHERE invoice_id = '$existing_subscription_query'"); + //Delete the Stripe Subscription from our cron watch list. + CRM_Core_DAO::executeQuery("DELETE FROM civicrm_stripe_subscriptions WHERE invoice_id = '$existing_subscription_query'"); + } + //Calculate timestamp for the last installment $end_time = strtotime("+$installments $frequency"); - $new_subscription_insert = "INSERT INTO civicrm_stripe_subscriptions (customer_id, plan_id, end_time) VALUES ('$stripe_customer->id', '$plan_id', '$end_time')"; - CRM_Core_DAO::executeQuery($new_subscription_insert); + $invoice_id = $params['invoiceID']; + CRM_Core_DAO::executeQuery("INSERT INTO civicrm_stripe_subscriptions (customer_id, invoice_id, end_time) VALUES ('$stripe_customer->id', '$invoice_id', '$end_time')"); - $params['trxn_id'] = $plan_id . ' ' . $stripe_response->start; + $trxn_id = $stripe_customer->id . '-' . $end_time; + $params['trxn_id'] = $trxn_id; return $params; } diff --git a/README.txt b/README.txt index 39b5aeab..1fab60f0 100644 --- a/README.txt +++ b/README.txt @@ -1,13 +1,38 @@ +------------ +Important Note: + +This version is for CiviCRM 4.1 and prior. +It will work for CiviCRM 4.2+ but there will be a new version to utilize all the new features surrounding Payment Processors in CiviCRM 4.2. +This currently includes everything you need minus a cron file to cancel recurring contributions. Do not allow recurring just yet! + +You also need a corresponding module for your CMS. Here is where the modules can be found: +Drupal: git clone --recursive --branch master http://git.drupal.org/sandbox/drastik/1719796.git civicrm_stripe +Joomla: TBD +WordPress: TBD + +------------ + Installing Stripe as a payment processor in CiviCRM 4.x -Folder structure is left in tact, but there is only 1 file and this is where it goes: -Stripe.php in civicrm/CRM/Core/Payment/Stripe.php +Folder structure is left in tact. +Place Stripe.php in civicrm/CRM/Core/Payment/Stripe.php + +Place civicrm_templates folder anywhere and inform CiviCRM of your "Custom Templates" location in this admin page: site.com/civicrm/admin/setting/path Copy Stripe's PHP library folder 'stripe-php' to civicrm/packages/stripe-php You can get Stripe's PHP library here: https://github.com/stripe/stripe-php -Run the included SQL file "civicrm_stripe.sql" to: +Run the included SQL file "civicrm_stripe.sql" to handle the DB-related needs. It will: Insert Stripe into civicrm_payment_processor_type (makes it available as an option within CiviCRM's payment processor settings) -Create table civicrm_stripe_customers -Create table civicrm_stripe_plans -Create table civicrm_stripe_subscriptions \ No newline at end of file +It will create the required tables: +civicrm_stripe_customers +civicrm_stripe_plans +civicrm_stripe_subscriptions + +------------ +Note: + +This will be packaged as a "CiviCRM Extension" shortly for an alternative installation method. +In <CiviCRM 4.2, you will need to create a cron job in order for recurring contributions to be properly ended. + +------------ \ No newline at end of file diff --git a/civicrm_stripe.sql b/civicrm_stripe.sql index 08876861..75cc2b9d 100644 --- a/civicrm_stripe.sql +++ b/civicrm_stripe.sql @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS `civicrm_stripe_plans` ( CREATE TABLE IF NOT EXISTS `civicrm_stripe_subscriptions` ( `customer_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL, - `plan_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `invoice_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `end_time` int(11) NOT NULL DEFAULT '0', KEY `end_time` (`end_time`) ) DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; diff --git a/civicrm_templates/CRM/Core/BillingBlock.tpl b/civicrm_templates/CRM/Core/BillingBlock.tpl new file mode 100644 index 00000000..96a007b2 --- /dev/null +++ b/civicrm_templates/CRM/Core/BillingBlock.tpl @@ -0,0 +1,221 @@ +{* + +--------------------------------------------------------------------+ + | CiviCRM version 4.1 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2011 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License and the CiviCRM Licensing Exception along | + | with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ +*} +{if $form.credit_card_number or $form.bank_account_number} +<!-- START Stripe --> + {if $paymentProcessor.payment_processor_type == 'Stripe'} + + <script type="text/javascript" src="https://js.stripe.com/v1/"></script> + <script type="text/javascript"> + var stripe_publishable_key = '{$paymentProcessor.password}'; + + {literal} + cj(function() { + cj(document).ready(function(){ + //Identify the payment form. Don't reference by form#id since it changes between payment pages (Contribution / Event / etc). + cj("#crm-container>form").addClass('stripe-payment-form'); + Stripe.setPublishableKey(stripe_publishable_key); + cj("form.stripe-payment-form").submit(function(event) { + // disable the submit button to prevent repeated clicks + cj('form.stripe-payment-form input.form-submit').attr("disabled", "disabled"); + + Stripe.createToken({ + number: cj('#credit_card_number').val(), + cvc: cj('#cvv2').val(), + exp_month: cj('#credit_card_exp_date\\[M\\]').val(), + exp_year: cj('#credit_card_exp_date\\[Y\\]').val() + }, stripeResponseHandler); + + // prevent the form from submitting with the default action + return false; + }); + }); + + //Response from Stripe.createToken. + function stripeResponseHandler(status, response) { + if (response.error) { + // show the errors on the form + cj("form.stripe-payment-form").prepend('<div class="messages crm-error">' + +'<strong>Payment Error Response:</strong>' + +'<ul id="errorList">' + +'<li>Error: ' + response.error.message + '</li>' + +'</ul>' + +'</div>'); + + cj('form.stripe-payment-form input.form-submit').removeAttr("disabled"); + + } else { + var token = response['id']; + // Update form with the token & submit + cj("input#stripe-token").val(token); + cj("form.stripe-payment-form").get(0).submit(); + } + } + }); + {/literal} + </script> + {/if} +<!-- END Stripe --> + <div id="payment_information"> + <fieldset class="billing_mode-group {if $paymentProcessor.payment_type & 2}direct_debit_info-group{else}credit_card_info-group{/if}"> + <legend> + {if $paymentProcessor.payment_type & 2} + {ts}Direct Debit Information{/ts} + {else} + {ts}Credit Card Information{/ts} + {/if} + </legend> + {if $paymentProcessor.billing_mode & 2 and !$hidePayPalExpress } + <div class="crm-section no-label paypal_button_info-section"> + <div class="content description"> + {ts}If you have a PayPal account, you can click the PayPal button to continue. Otherwise, fill in the credit card and billing information on this form and click <strong>Continue</strong> at the bottom of the page.{/ts} + </div> + </div> + <div class="crm-section no-label {$form.$expressButtonName.name}-section"> + <div class="content description"> + {$form.$expressButtonName.html} + <div class="description">Save time. Checkout securely. Pay without sharing your financial information. </div> + </div> + </div> + {/if} + + {if $paymentProcessor.billing_mode & 1} + <div class="crm-section billing_mode-section {if $paymentProcessor.payment_type & 2}direct_debit_info-section{else}credit_card_info-section{/if}"> + {if $paymentProcessor.payment_type & 2} + <div class="crm-section {$form.account_holder.name}-section"> + <div class="label">{$form.account_holder.label}</div> + <div class="content">{$form.account_holder.html}</div> + <div class="clear"></div> + </div> + <div class="crm-section {$form.bank_account_number.name}-section"> + <div class="label">{$form.bank_account_number.label}</div> + <div class="content">{$form.bank_account_number.html}</div> + <div class="clear"></div> + </div> + <div class="crm-section {$form.bank_identification_number.name}-section"> + <div class="label">{$form.bank_identification_number.label}</div> + <div class="content">{$form.bank_identification_number.html}</div> + <div class="clear"></div> + </div> + <div class="crm-section {$form.bank_name.name}-section"> + <div class="label">{$form.bank_name.label}</div> + <div class="content">{$form.bank_name.html}</div> + <div class="clear"></div> + </div> + {else} + <div class="crm-section {$form.credit_card_type.name}-section"> + <div class="label">{$form.credit_card_type.label}</div> + <div class="content">{$form.credit_card_type.html}</div> + <div class="clear"></div> + </div> + <div class="crm-section {$form.credit_card_number.name}-section"> + <div class="label">{$form.credit_card_number.label}</div> + <div class="content">{$form.credit_card_number.html} + <div class="description">{ts}Enter numbers only, no spaces or dashes.{/ts}</div> + </div> + <div class="clear"></div> + </div> + <div class="crm-section {$form.cvv2.name}-section"> + <div class="label">{$form.cvv2.label}</div> + <div class="content"> + {$form.cvv2.html} + <img src="{$config->resourceBase}i/mini_cvv2.gif" alt="{ts}Security Code Location on Credit Card{/ts}" style="vertical-align: text-bottom;" /> + <div class="description">{ts}Usually the last 3-4 digits in the signature area on the back of the card.{/ts}</div> + </div> + <div class="clear"></div> + </div> + <div class="crm-section {$form.credit_card_exp_date.name}-section"> + <div class="label">{$form.credit_card_exp_date.label}</div> + <div class="content">{$form.credit_card_exp_date.html}</div> + <div class="clear"></div> + </div> + {/if} + </div> + </fieldset> + + <fieldset class="billing_name_address-group"> + <legend>{ts}Billing Name and Address{/ts}</legend> + <div class="crm-section billing_name_address-section"> + <div class="crm-section billingNameInfo-section"> + <div class="content description"> + {if $paymentProcessor.payment_type & 2} + {ts}Enter the name of the account holder, and the corresponding billing address.{/ts} + {else} + {ts}Enter the name as shown on your credit or debit card, and the billing address for this card.{/ts} + {/if} + </div> + </div> + <div class="crm-section {$form.billing_first_name.name}-section"> + <div class="label">{$form.billing_first_name.label}</div> + <div class="content">{$form.billing_first_name.html}</div> + <div class="clear"></div> + </div> + <div class="crm-section {$form.billing_middle_name.name}-section"> + <div class="label">{$form.billing_middle_name.label}</div> + <div class="content">{$form.billing_middle_name.html}</div> + <div class="clear"></div> + </div> + <div class="crm-section {$form.billing_last_name.name}-section"> + <div class="label">{$form.billing_last_name.label}</div> + <div class="content">{$form.billing_last_name.html}</div> + <div class="clear"></div> + </div> + {assign var=n value=billing_street_address-$bltID} + <div class="crm-section {$form.$n.name}-section"> + <div class="label">{$form.$n.label}</div> + <div class="content">{$form.$n.html}</div> + <div class="clear"></div> + </div> + {assign var=n value=billing_city-$bltID} + <div class="crm-section {$form.$n.name}-section"> + <div class="label">{$form.$n.label}</div> + <div class="content">{$form.$n.html}</div> + <div class="clear"></div> + </div> + {assign var=n value=billing_country_id-$bltID} + <div class="crm-section {$form.$n.name}-section"> + <div class="label">{$form.$n.label}</div> + <div class="content">{$form.$n.html|crmReplace:class:big}</div> + <div class="clear"></div> + </div> + {assign var=n value=billing_state_province_id-$bltID} + <div class="crm-section {$form.$n.name}-section"> + <div class="label">{$form.$n.label}</div> + <div class="content">{$form.$n.html|crmReplace:class:big}</div> + <div class="clear"></div> + </div> + {assign var=n value=billing_postal_code-$bltID} + <div class="crm-section {$form.$n.name}-section"> + <div class="label">{$form.$n.label}</div> + <div class="content">{$form.$n.html}</div> + <div class="clear"></div> + </div> + </div> + </fieldset> + {else} + </fieldset> + {/if} + </div> +{/if} \ No newline at end of file -- GitLab