1164 lines
38 KiB
Plaintext
1164 lines
38 KiB
Plaintext
<?php
|
||
|
||
/**
|
||
* @file
|
||
* A stripe.js PCI-compliant payment gateway
|
||
* Forked from Bitcookie's work (thanks!) which was posted at
|
||
* http://bitcookie.com/blog/pci-compliant-ubercart-and-stripe-js
|
||
* from discussion in the uc_stripe issue queue,
|
||
* https://www.drupal.org/node/1467886
|
||
*/
|
||
|
||
/**
|
||
* Implements hook_libraries_info() to define what files should be loaded.
|
||
*
|
||
* @return mixed
|
||
*/
|
||
|
||
module_load_include('inc', 'uc_stripe', 'uc_stripe.mail');
|
||
|
||
function uc_stripe_libraries_info() {
|
||
$libraries['stripe'] = array(
|
||
'name' => 'Stripe PHP Library',
|
||
'vendor url' => 'http://stripe.com',
|
||
'download url' => 'https://github.com/stripe/stripe-php/releases',
|
||
'download file url' => 'https://github.com/stripe/stripe-php/archive/v6.38.0.tar.gz',
|
||
'version arguments' => array(
|
||
'file' => 'VERSION',
|
||
'pattern' => '/(\d+\.\d+\.\d+)/',
|
||
),
|
||
'versions' => array(
|
||
'6.38.0' => array(
|
||
'files' => array(
|
||
'php' => array(
|
||
'init.php',
|
||
)
|
||
),
|
||
'stripe_api_version' => '2019-05-16'
|
||
)
|
||
),
|
||
);
|
||
|
||
return $libraries;
|
||
}
|
||
|
||
/**
|
||
* Implements hook_menu().
|
||
*/
|
||
function uc_stripe_menu() {
|
||
$items = array();
|
||
|
||
$items['uc_stripe/ajax/confirm_payment'] = array(
|
||
'access callback' => true,
|
||
'page callback' => '_uc_stripe_confirm_payment',
|
||
'delivery callback' => 'drupal_json_output',
|
||
);
|
||
|
||
$items['stripe/authenticate-payment/%'] = array(
|
||
'access callback' => true,
|
||
'page callback' => 'drupal_get_form',
|
||
'page arguments' => array('uc_stripe_authenticate_payment_form', 2),
|
||
'file' => 'uc_stripe.pages.inc'
|
||
);
|
||
|
||
|
||
return $items;
|
||
}
|
||
|
||
/**
|
||
* Implements hook_payment_gateway to register this payment gateway
|
||
* @return array
|
||
*/
|
||
function uc_stripe_uc_payment_gateway() {
|
||
$gateways = array();
|
||
$gateways[] = array(
|
||
'id' => 'uc_stripe',
|
||
'title' => t('Stripe Gateway'),
|
||
'description' => t('Process card payments using Stripe JS.'),
|
||
'settings' => 'uc_stripe_settings_form',
|
||
'credit' => 'uc_stripe_charge',
|
||
);
|
||
return $gateways;
|
||
}
|
||
|
||
/**
|
||
* Implements hook_recurring_info() to integrate with uc_recurring
|
||
*
|
||
* @return mixed
|
||
*/
|
||
function uc_stripe_recurring_info() {
|
||
$items['uc_stripe'] = array(
|
||
'name' => t('Stripe'),
|
||
'payment method' => 'credit',
|
||
'module' => 'uc_recurring',
|
||
'fee handler' => 'uc_stripe',
|
||
'process callback' => 'uc_stripe_process',
|
||
'renew callback' => 'uc_stripe_renew',
|
||
'cancel callback' => 'uc_stripe_cancel',
|
||
'own handler' => FALSE,
|
||
'menu' => array(
|
||
'charge' => UC_RECURRING_MENU_DEFAULT,
|
||
'edit' => UC_RECURRING_MENU_DEFAULT,
|
||
'cancel' => UC_RECURRING_MENU_DEFAULT,
|
||
),
|
||
);
|
||
return $items;
|
||
}
|
||
|
||
/**
|
||
* Implements hook_form_FORMID_alter() to do JS Stripe processing when processing
|
||
* from the order review page
|
||
*
|
||
* @param unknown $form
|
||
* @param unknown $form_state
|
||
* @param unknown $form_id
|
||
*
|
||
*/
|
||
function uc_stripe_form_uc_cart_checkout_review_form_alter(&$form, &$form_state, $form_id){
|
||
|
||
//This alter hook should only take action when payment method is credit.
|
||
if($form_state['uc_order']->payment_method != 'credit'){
|
||
return;
|
||
}
|
||
|
||
// If payment method is not found, hide submit button, and show error to user
|
||
if (empty($_SESSION['stripe']['payment_method'])) {
|
||
$form['actions']['submit']['#type'] = 'hidden';
|
||
$fail_message = variable_get('uc_credit_fail_message', t('We were unable to process your credit card payment. Please verify your details and try again. If the problem persists, contact us to complete your order.'));
|
||
watchdog('uc_stripe', 'Stripe charge failed for order @order, message: @message', array('@order' => $form_state['uc_order']->order_id, '@message' => 'Payment method not found'));
|
||
drupal_set_message($fail_message, 'error');
|
||
return;
|
||
}
|
||
|
||
// When a payment fails, remove the Submit Order button because it will most
|
||
// likely fail again. Instead, the customer should hit back to try again.
|
||
if(isset($_SESSION['stripe']['payment_failed'])){
|
||
$form['actions']['submit']['#type'] = 'hidden';
|
||
}
|
||
|
||
$stripe_payment_method_id = $_SESSION['stripe']['payment_method'];
|
||
$order_id = $form_state['uc_order']->order_id;
|
||
$apikey = variable_get('uc_stripe_testmode', TRUE) ? check_plain(variable_get('uc_stripe_api_key_test_publishable', '')) : check_plain(variable_get('uc_stripe_api_key_live_publishable', ''));
|
||
$settings = array('methodId' => $stripe_payment_method_id, 'apikey' => $apikey, 'orderId' => $order_id);
|
||
|
||
//Attach Stripe v3 JS library and JS for processing payment
|
||
$form['#attached']['js']['https://js.stripe.com/v3/'] = array('type' => 'external');
|
||
$form['#attached']['js'][] = array('data' => array('uc_stripe' => $settings), 'type' => 'setting');
|
||
$form['#attached']['js'][] = drupal_get_path('module', 'uc_stripe') . '/js/uc_stripe_process_payment.js';
|
||
}
|
||
|
||
/**
|
||
* Implements hook_form_FORMID_alter() to change the checkout form
|
||
* All work as a result is done in JS, the ordinary post does not happen.
|
||
*
|
||
* @param $form
|
||
* @param $form_state
|
||
* @param $form_id
|
||
*/
|
||
function uc_stripe_form_uc_cart_checkout_form_alter(&$form, &$form_state) {
|
||
|
||
$form['panes']['payment-stripe']['#attached']['css'][] = array(
|
||
'data' => '#payment-stripe-pane { display: none; }',
|
||
'type' => 'inline',
|
||
);
|
||
|
||
$form['panes']['payment-stripe']['#states'] = array(
|
||
'visible' => array(
|
||
':input[name="panes[payment][payment_method]"]' => array(
|
||
'value' => 'credit'
|
||
)
|
||
)
|
||
);
|
||
|
||
$stripe_payment_form = &$form['panes']['payment-stripe']['details'];
|
||
$payment_form = &$form['panes']['payment']['details'];
|
||
|
||
// Markup text will not be displayed when JS and stripe are functioning properly
|
||
// since Stripe Elements will replace the contents of this div
|
||
$stripe_payment_form['stripe_card_element'] = array(
|
||
'#prefix' => '<div id="stripe-card-element">',
|
||
'#weight' => - 10,
|
||
'#markup' => '<div class="stripe-warning">' . t('Sorry, for security reasons your card cannot be processed. Please refresh this page and try again. If the problem persists please check that Javascript is enabled your browser.') . '</div>',
|
||
'#suffix' => '</div>',
|
||
);
|
||
|
||
// Powered by Stripe (logo from https://stripe.com/about/resources)
|
||
if (variable_get('uc_stripe_poweredby', FALSE)) {
|
||
$payment_form['field_message'] = array(
|
||
'#type' => 'item',
|
||
'#markup' => "<a target='_blank' href='http://stripe.com'><img src=" . '/' . drupal_get_path('module', 'uc_stripe') . '/images/solid-dark.svg' . " alt='Powered by Stripe'></a>",
|
||
'#weight' => 1,
|
||
);
|
||
}
|
||
|
||
// Used for payment method Id when retrieved from stripe.
|
||
$stripe_payment_form['stripe_payment_method'] = array(
|
||
'#type' => 'hidden',
|
||
'#default_value' => 'default',
|
||
'#attributes' => array(
|
||
'id' => 'edit-panes-stripe-payment-details-stripe-payment-method',
|
||
),
|
||
);
|
||
|
||
if (empty($form['actions']['continue']['#attributes'])) {
|
||
$form['actions']['continue']['#attributes'] = array();
|
||
}
|
||
$form['actions']['continue']['#attributes']['disabled'] = 'disabled';
|
||
|
||
$apikey = variable_get('uc_stripe_testmode', TRUE) ? check_plain(variable_get('uc_stripe_api_key_test_publishable', '')) : check_plain(variable_get('uc_stripe_api_key_live_publishable', ''));
|
||
|
||
// Add custom JS and CSS
|
||
$settings = array('apikey' => $apikey);
|
||
$form['#attached']['js']['https://js.stripe.com/v3/'] = array('type' => 'external');
|
||
$form['#attached']['js'][] = array('data' => array('uc_stripe' => $settings), 'type' => 'setting');
|
||
$form['#attached']['js'][] = drupal_get_path('module', 'uc_stripe') . '/js/uc_stripe.js';
|
||
$form['#attached']['css'][] = drupal_get_path('module', 'uc_stripe') . '/css/uc_stripe.css';
|
||
|
||
// hide cc fields and set defaults since we rely fully on stripe's dynamic cc fields
|
||
$payment_form['cc_number']['#type'] = 'hidden';
|
||
$payment_form['cc_number']['#default_value'] = '';
|
||
$payment_form['cc_number']['#attributes']['id'] = 'edit-panes-payment-details-cc-number';
|
||
|
||
$payment_form['cc_cvv']['#type'] = 'hidden';
|
||
$payment_form['cc_cvv']['#default_value'] = '';
|
||
$payment_form['cc_cvv']['#attributes']['id'] = 'edit-panes-payment-details-cc-cvv';
|
||
|
||
$payment_form['cc_exp_year']['#type'] = 'hidden';
|
||
$payment_form['cc_exp_year']['#attributes']['id'] = 'edit-panes-payment-details-cc-exp-year';
|
||
|
||
//Stripe CC expiration can be up to 50 years in future. The normal ubercart select
|
||
// options only go up to 20 years in the future.
|
||
$min = intval(date('Y'));
|
||
$max = intval(date('Y')) + 50;
|
||
$default = intval(date('Y'));
|
||
|
||
$payment_form['cc_exp_year']['#options'] = drupal_map_assoc(range($min, $max));
|
||
$payment_form['cc_exp_year']['#default_value'] = $default;
|
||
|
||
$payment_form['cc_exp_month']['#type'] = 'hidden';
|
||
$payment_form['cc_exp_month']['#default_value'] = 1;
|
||
$payment_form['cc_exp_month']['#attributes']['id'] = 'edit-panes-payment-details-cc-exp-month';
|
||
|
||
// Add custom submit which will do saving away of token during submit.
|
||
$form['#submit'][] = 'uc_stripe_checkout_form_customsubmit';
|
||
|
||
// Add a section for stripe.js error messages (CC validation, etc.)
|
||
$form['panes']['messages'] = array(
|
||
'#markup' => "<div id='uc_stripe_messages' class='messages error hidden'></div>",
|
||
);
|
||
|
||
//Clear any previous card payment failures
|
||
if(isset($_SESSION['stripe']['payment_failed'])){
|
||
unset($_SESSION['stripe']['payment_failed']);
|
||
}
|
||
|
||
if (uc_credit_default_gateway() == 'uc_stripe') {
|
||
if (variable_get('uc_stripe_testmode', TRUE)) {
|
||
$form['panes']['testmode'] = array(
|
||
'#prefix' => "<div class='messages' style='background-color:#BEEBBF'>",
|
||
'#markup' => t("Test mode is <strong>ON</strong> for the Stripe Payment Gateway. Your card will not be charged. To change this setting, edit the !link", array('!link' => l("Stripe settings", "admin/store/settings/payment/method/credit"))),
|
||
'#suffix' => "</div>",
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Implements hook_order_pane to provide the stripe customer info
|
||
*
|
||
* @return array
|
||
*/
|
||
function uc_stripe_uc_order_pane() {
|
||
$panes[] = array(
|
||
'id' => 'uc_stripe',
|
||
'callback' => 'uc_stripe_order_pane_stripe',
|
||
'title' => t('Stripe Customer Info'),
|
||
'desc' => t("Stripe Information"),
|
||
'class' => 'pos-left',
|
||
'weight' => 3,
|
||
'show' => array('view', 'edit'),
|
||
);
|
||
|
||
return $panes;
|
||
}
|
||
|
||
/**
|
||
* Implements hook_uc_checkout_pane to add checkout pane for stripe payment details
|
||
*
|
||
* @return array
|
||
*/
|
||
function uc_stripe_uc_checkout_pane() {
|
||
$panes['payment-stripe'] = array(
|
||
'callback' => '_uc_stripe_payment_pane_callback',
|
||
'title' => t('Payment Information'),
|
||
'desc' => t("Accept stripe payment from customer."),
|
||
'weight' => 6,
|
||
'process' => FALSE,
|
||
'collapsible' => FALSE,
|
||
);
|
||
return $panes;
|
||
}
|
||
|
||
/**
|
||
* Implements uc_checkout_pane_callback() specified in 'callback' of
|
||
* uc_stripe_uc_checkout_pane()
|
||
*
|
||
* Provides empty pane for stripe elements to be added
|
||
* @param $op
|
||
* @param $order
|
||
* @param $form
|
||
* @param $form_state
|
||
* @return array
|
||
*/
|
||
function _uc_stripe_payment_pane_callback($op, $order, $form = NULL, &$form_state = NULL) {
|
||
// Create separate payment pane for stripe because the normal payment pane is refreshed many times
|
||
// by ajax, by country changes, etc.. Refreshing the payment section triggers Stripe Api's security feature
|
||
// and destroys the Stripe Element in the DOM.
|
||
// Emtpy values needed so that pane still appears.
|
||
switch ($op) {
|
||
case 'view':
|
||
$description = t('');
|
||
$contents['stripe_card_element'] = array(
|
||
'#markup' => '',
|
||
);
|
||
|
||
return array('description' => $description, 'contents' => $contents);
|
||
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Implements hook_uc_checkout_complete()
|
||
*
|
||
* Saves stripe customer_id into the user->data object
|
||
*
|
||
* @param $order
|
||
* @param $account
|
||
*/
|
||
function uc_stripe_uc_checkout_complete($order, $account) {
|
||
|
||
if ($order->payment_method == "credit") {
|
||
// Pull the stripe payment method ID from the session.
|
||
// It got there in uc_stripe_checkout_form_customsubmit()
|
||
$stripe_payment_id = $_SESSION['stripe']['payment_method'];
|
||
|
||
$stripe_customer_id = $order->data['stripe_customer_id'];
|
||
|
||
$loaded_user = user_load($account->uid);
|
||
user_save($loaded_user, array('data' => array('uc_stripe_customer_id' => $stripe_customer_id)));
|
||
user_save($loaded_user, array('data' => array('uc_stripe_payment_id' => $stripe_payment_id)));
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Implements uc_order_pane_callback() specified in 'callback' of
|
||
* uc_stripe_uc_order_pane()
|
||
*
|
||
* Returns text for customer id for order pane.
|
||
*
|
||
* @param $op
|
||
* @param $order
|
||
* @param $form
|
||
* @param $form_state
|
||
* @return array
|
||
*/
|
||
function uc_stripe_order_pane_stripe($op, $order, &$form = NULL, &$form_state = NULL) {
|
||
switch ($op) {
|
||
case 'view':
|
||
$stripe_customer_id = _uc_stripe_get_customer_id($order->uid);
|
||
$output = t("Customer ID: ") . $stripe_customer_id;
|
||
return array('#markup' => $output);
|
||
default:
|
||
return;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Provide configuration form for uc_stripe
|
||
*
|
||
* @return mixed
|
||
*/
|
||
function uc_stripe_settings_form() {
|
||
$form['uc_stripe_settings'] = array(
|
||
'#type' => 'fieldset',
|
||
'#title' => t('Stripe settings'),
|
||
);
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_api_key_test_secret'] = array(
|
||
'#type' => 'textfield',
|
||
'#title' => t('Test Secret Key'),
|
||
'#default_value' => variable_get('uc_stripe_api_key_test_secret', ''),
|
||
'#description' => t('Your Development Stripe API Key. Must be the "secret" key, not the "publishable" one.'),
|
||
);
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_api_key_test_publishable'] = array(
|
||
'#type' => 'textfield',
|
||
'#title' => t('Test Publishable Key'),
|
||
'#default_value' => variable_get('uc_stripe_api_key_test_publishable', ''),
|
||
'#description' => t('Your Development Stripe API Key. Must be the "publishable" key, not the "secret" one.'),
|
||
);
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_api_key_live_secret'] = array(
|
||
'#type' => 'textfield',
|
||
'#title' => t('Live Secret Key'),
|
||
'#default_value' => variable_get('uc_stripe_api_key_live_secret', ''),
|
||
'#description' => t('Your Live Stripe API Key. Must be the "secret" key, not the "publishable" one.'),
|
||
);
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_api_key_live_publishable'] = array(
|
||
'#type' => 'textfield',
|
||
'#title' => t('Live Publishable Key'),
|
||
'#default_value' => variable_get('uc_stripe_api_key_live_publishable', ''),
|
||
'#description' => t('Your Live Stripe API Key. Must be the "publishable" key, not the "secret" one.'),
|
||
);
|
||
|
||
$email_text = _uc_stripe_get_authentication_required_email_text();
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_authenticaiton_required_email'] = array(
|
||
'#type' => 'textarea',
|
||
'#title' => t('Email for Recurring payment authentication'),
|
||
'#default_value' => variable_get('uc_stripe_authenticaiton_required_email', $email_text),
|
||
'#description' => t('If your site uses recurring payments, some transactions will require the customer to return to the site and authenticate before the subscrption payment can be processed.')
|
||
);
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_testmode'] = array(
|
||
'#type' => 'checkbox',
|
||
'#title' => t('Test mode'),
|
||
'#description' => 'Testing Mode: Stripe will use the development API key to process the transaction so the card will not actually be charged.',
|
||
'#default_value' => variable_get('uc_stripe_testmode', TRUE),
|
||
);
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_poweredby'] = array(
|
||
'#type' => 'checkbox',
|
||
'#title' => t('Powered by Stripe'),
|
||
'#description' => 'Show "powered by Stripe" in checkout.',
|
||
'#default_value' => variable_get('uc_stripe_poweredby', FALSE),
|
||
);
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_metadata_titles'] = array(
|
||
'#type' => 'checkbox',
|
||
'#title' => t('Metadata: Title'),
|
||
'#description' => t('Include order item title(s) in Stripe metadata.'),
|
||
'#default_value' => variable_get('uc_stripe_metadata_titles', FALSE),
|
||
);
|
||
|
||
$form['uc_stripe_settings']['uc_stripe_metadata_models'] = array(
|
||
'#type' => 'checkbox',
|
||
'#title' => t('Metadata: Model'),
|
||
'#description' => t('Include item model(s) (SKU(s)) in Stripe metadata.'),
|
||
'#default_value' => variable_get('uc_stripe_metadata_models', FALSE),
|
||
);
|
||
|
||
return $form;
|
||
}
|
||
|
||
/**
|
||
* Implements hook_form_FORMID_alter()
|
||
*
|
||
* Add validation function for stripe settings
|
||
*
|
||
* @param $form
|
||
* @param $form_state
|
||
*/
|
||
function uc_stripe_form_uc_payment_method_settings_form_alter(&$form, &$form_state) {
|
||
$form['#validate'][] = 'uc_stripe_settings_form_validate';
|
||
}
|
||
|
||
/**
|
||
* Validation function and normalize keys (trim spaces)
|
||
*
|
||
* @param $form
|
||
* @param $form_state
|
||
*/
|
||
function uc_stripe_settings_form_validate($form, &$form_state) {
|
||
$elements = array('uc_stripe_api_key_test_secret', 'uc_stripe_api_key_test_publishable', 'uc_stripe_api_key_live_secret', 'uc_stripe_api_key_live_publishable', );
|
||
|
||
if ($form_state['values']['uc_pg_uc_stripe_enabled']) {
|
||
foreach ($elements as $element_name) {
|
||
$form_state['values'][$element_name] = _uc_stripe_sanitize_key($form_state['values'][$element_name]);
|
||
if (!_uc_stripe_validate_key($form_state['values'][$element_name])) {
|
||
form_set_error($element_name, t('@name does not appear to be a valid stripe key', array('@name' => $element_name)));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Make sure they haven't tried to validate credit card numbers, as uc_stripe will not provide a real one.
|
||
if (!empty($form_state['values']['uc_credit_validate_numbers'])) {
|
||
form_set_error('uc_credit_validate_numbers', t('When used with Ubercart Stripe, "Validate credit card number at checkout" must be unchecked.'));
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Sanitize and strip whitespace from Stripe keys
|
||
*
|
||
* @param $key
|
||
*/
|
||
function _uc_stripe_sanitize_key($key) {
|
||
$key = trim($key);
|
||
$key = check_plain($key);
|
||
return $key;
|
||
}
|
||
|
||
/**
|
||
* Validate Stripe key
|
||
*
|
||
* @param $key
|
||
* @return boolean
|
||
*/
|
||
function _uc_stripe_validate_key($key) {
|
||
$valid = preg_match('/^[a-zA-Z0-9_]+$/', $key);
|
||
return $valid;
|
||
}
|
||
|
||
/**
|
||
* Custom submit function to store the stripe token
|
||
*
|
||
* Since we don't have a user account at this step, we're going to store the token
|
||
* in the session. We'll grab the token in the charge callback and use it to charge
|
||
*
|
||
* @param $form
|
||
* @param $form_state
|
||
*/
|
||
function uc_stripe_checkout_form_customsubmit($form, &$form_state) {
|
||
// This submit may be entered on another payment type, so don't set session in that case.
|
||
if (!empty($form_state['values']['panes']['payment-stripe']['details']['stripe_payment_method'])) {
|
||
$_SESSION['stripe']['payment_method'] = $form_state['values']['panes']['payment-stripe']['details']['stripe_payment_method'];
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Generic "charge" callback that runs on checkout and via the order's "card" terminal
|
||
*
|
||
* @param $order_id
|
||
* @param $amount
|
||
* @param $data
|
||
* @return array
|
||
*/
|
||
function uc_stripe_charge($order_id, $amount, $data) {
|
||
global $user;
|
||
|
||
// Load the stripe PHP API
|
||
|
||
if (!_uc_stripe_prepare_api()) {
|
||
$result = array(
|
||
'success' => FALSE,
|
||
'comment' => t('Stripe API not found.'),
|
||
'message' => t('Stripe API not found. Contact the site administrator.'),
|
||
'uid' => $user->uid,
|
||
'order_id' => $order_id,
|
||
);
|
||
return $result;
|
||
}
|
||
|
||
$order = uc_order_load($order_id);
|
||
|
||
$context = array(
|
||
'revision' => 'formatted-original',
|
||
'type' => 'amount',
|
||
);
|
||
|
||
$options = array(
|
||
'sign' => FALSE,
|
||
'thou' => FALSE,
|
||
'dec' => FALSE,
|
||
'prec' => 2,
|
||
);
|
||
|
||
// Format the amount in cents, which is what Stripe wants
|
||
$amount = uc_currency_format($amount, FALSE, FALSE, FALSE);
|
||
|
||
// Charge the stripe customer the amount in the order
|
||
|
||
//--Handle transactions for $0
|
||
|
||
// Stripe can't handle transactions < $0.50, but $0 is a common value
|
||
// so we will just return a positive result when the amount is $0.
|
||
if ($amount == 0) {
|
||
$result = array(
|
||
'success' => TRUE,
|
||
'message' => t('Payment of $0 approved'),
|
||
'uid' => $user->uid,
|
||
'trans_id' => md5(uniqid(rand())),
|
||
);
|
||
uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
|
||
return $result;
|
||
}
|
||
|
||
$stripe_customer_id = null;
|
||
|
||
if(key_exists('stripe_customer_id', $order->data)){
|
||
$stripe_customer_id = $order->data['stripe_customer_id'];
|
||
}
|
||
|
||
// Rexamine Payment Intent and Record payment or failure to the customer
|
||
try {
|
||
|
||
if(!key_exists('payment_intent_id', $order->data)){
|
||
throw new Exception('The payment Intent has failed.');
|
||
}
|
||
|
||
//Bail if there's no customer ID
|
||
if (empty($stripe_customer_id) || is_null($stripe_customer_id)) {
|
||
throw new Exception('No customer ID found');
|
||
}
|
||
|
||
//Bail if there's no payment method
|
||
if (empty($_SESSION['stripe']['payment_method'])) {
|
||
throw new Exception('Token not found');
|
||
}
|
||
$stripe_payment_method_id = $_SESSION['stripe']['payment_method'];
|
||
|
||
//Get item titles and models
|
||
foreach($order->products as $item){
|
||
$titles[] = $item->title;
|
||
$models[] = $item->model;
|
||
}
|
||
$metadata = array();
|
||
|
||
if (!empty($models)) {
|
||
$metadata['models'] = implode(";", $models);
|
||
}
|
||
if (!empty($titles)) {
|
||
$metadata['titles'] = implode(";", $titles);
|
||
}
|
||
$params = array(
|
||
"amount" => $amount,
|
||
"currency" => strtolower($order->currency),
|
||
"customer" => $stripe_customer_id,
|
||
"description" => t("Order #@order_id", array("@order_id" => $order_id)),
|
||
"metadata" => $metadata,
|
||
"payment_method" => $stripe_payment_method_id,
|
||
"payment_method_types" => ['card'],
|
||
"confirm" => true,
|
||
|
||
);
|
||
|
||
drupal_set_message(print_r($metadata, true), 'status');
|
||
|
||
$intent_id = $order->data['payment_intent_id'];
|
||
|
||
if (!empty($shipping_info)) {
|
||
$params['shipping'] = $shipping_info;
|
||
\Stripe\PaymentIntent::update($intent_id, ['shipping' => $shipping_info]);
|
||
}
|
||
|
||
// charge the Customer the amount in the order
|
||
$payment_intent = \Stripe\PaymentIntent::retrieve($intent_id);
|
||
|
||
if ($payment_intent->status != 'succeeded') {
|
||
throw new Exception($payment_intent['last_payment_error']['message']);
|
||
}
|
||
|
||
$charge_id = $payment_intent->charges->data[0]['id'];
|
||
|
||
$formatted_amount = $amount / 100;
|
||
$formatted_amount = number_format($formatted_amount, 2);
|
||
|
||
// $payment_method = \Stripe\PaymentMethod::retrieve($payment_intent->payment_method);
|
||
// $payment_method->attach(['customer' => $stripe_customer_id]);
|
||
|
||
$result = array(
|
||
'success' => TRUE,
|
||
'message' => t('Payment of @amount processed successfully, Stripe transaction id @transaction_id.', array('@amount' => $formatted_amount, '@transaction_id' => $charge_id)),
|
||
'comment' => t('Stripe transaction ID: @transaction_id', array('@transaction_id' => $charge_id)),
|
||
'uid' => $user->uid,
|
||
);
|
||
|
||
uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
|
||
uc_order_comment_save($order_id, $user->uid, $result['message'], 'order', 'completed', FALSE);
|
||
|
||
return $result;
|
||
|
||
} catch (Exception $e) {
|
||
$result = array(
|
||
'success' => FALSE,
|
||
'comment' => $e->getCode(),
|
||
'message' => t("Stripe Charge Failed for order !order: !message", array(
|
||
"!order" => $order_id,
|
||
"!message" => $e->getMessage()
|
||
)),
|
||
'uid' => $user->uid,
|
||
'order_id' => $order_id,
|
||
);
|
||
uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
|
||
watchdog('uc_stripe', 'Stripe charge failed for order @order, message: @message', array('@order' => $order_id, '@message' => $result['message']));
|
||
|
||
$_SESSION['stripe']['payment_failed'] = TRUE;
|
||
|
||
return $result;
|
||
}
|
||
|
||
// Default / Fallback procedure to fail if the above conditions aren't met
|
||
|
||
$result = array(
|
||
'success' => FALSE,
|
||
'comment' => "Stripe Gateway Error",
|
||
'message' => "Stripe Gateway Error",
|
||
'uid' => $user->uid,
|
||
'order_id' => $order_id,
|
||
);
|
||
|
||
uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
|
||
|
||
watchdog('uc_stripe', 'Stripe gateway error for order @order_id', array('order_id' => $order_id));
|
||
|
||
$_SESSION['stripe']['payment_failed'] = TRUE;
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Handle renewing a recurring fee, called by uc_recurring
|
||
*
|
||
* Runs when the subscription interval is hit. So once a month or whatever.
|
||
* This just charges the stripe customer whatever amount ubercart wants. It does
|
||
* not use the Stripe subscription feature.
|
||
*
|
||
* @param $order
|
||
* @param $fee
|
||
* @return bool
|
||
*/
|
||
function uc_stripe_renew($order, &$fee) {
|
||
|
||
try {
|
||
|
||
//Load the API
|
||
_uc_stripe_prepare_api();
|
||
|
||
//Get the customer ID
|
||
$stripe_customer_id = _uc_stripe_get_customer_id($order->uid);
|
||
$stripe_payment_id = _uc_stripe_get_payment_id($order->uid);
|
||
|
||
if (empty($stripe_customer_id)) {
|
||
throw new Exception('No stripe customer ID found');
|
||
}
|
||
|
||
$amount = $fee->fee_amount;
|
||
$amount = $amount * 100;
|
||
|
||
//create intent Array
|
||
$intent_params = array(
|
||
'amount' => $amount,
|
||
'currency' => strtolower($order->currency),
|
||
'payment_method_types' => ['card'],
|
||
'customer' => $stripe_customer_id,
|
||
'off_session' => true,
|
||
'confirm' => true,
|
||
);
|
||
|
||
// Payment methods added with Stripe PaymentIntent API will be saved to customer
|
||
// object in drupal. Payment cards saved with 2.x tokens will not have a value
|
||
// saved to customer object, but the payment Intent will still continue because
|
||
// Stipe will use default payment in those situations.
|
||
if($stripe_payment_id){
|
||
$intent_params['payment_method'] = $stripe_payment_id;
|
||
}
|
||
|
||
$payment_intent = \Stripe\PaymentIntent::create($intent_params);
|
||
|
||
uc_payment_enter($order->order_id, $order->payment_method, $order->order_total, $fee->uid, $payment_intent, "Success");
|
||
|
||
$formatted_amount = number_format($fee->fee_amount, 2);
|
||
$message = t('Card renewal payment of @amount processed successfully.', array('@amount' => $formatted_amount));
|
||
uc_order_comment_save($fee->order_id, $order->uid, $message, 'order', 'completed', FALSE);
|
||
uc_order_comment_save($fee->order_id, $order->uid, $message, 'admin');
|
||
|
||
return TRUE;
|
||
|
||
} catch (\Stripe\Error\Card $e) {
|
||
|
||
if ($e->getDeclineCode() === 'authentication_required') {
|
||
$NOT_COMPLETED = 0;
|
||
// Create and store hash so that we can prompt user to authenticate payment.
|
||
$hash = drupal_hmac_base64(REQUEST_TIME . $order->order_id, drupal_get_hash_salt() . $stripe_payment_id);
|
||
db_insert('uc_stripe_pending_auth')->fields(array(
|
||
'order_id' => $order->order_id,
|
||
'completed' => $NOT_COMPLETED,
|
||
'rfee_id' => $fee->rfid,
|
||
'hash' => $hash
|
||
))->execute();
|
||
|
||
// Prepare email to alert user that authentication is required.
|
||
$params['body'] = variable_get('uc_stripe_authenticaiton_required_email', _uc_stripe_get_authentication_required_email_text());
|
||
$params['user'] = user_load($order->uid);
|
||
$params['hash'] = $hash;
|
||
drupal_mail('uc_stripe', 'authentication_required', $params['user']->mail,language_default(), $params);
|
||
|
||
};
|
||
|
||
$result = array(
|
||
'success' => FALSE,
|
||
'comment' => $e->getCode(),
|
||
'message' => t("Renewal Failed for order !order: !message", array(
|
||
"!order" => $order->order_id,
|
||
"!message" => $e->getMessage()
|
||
)),
|
||
);
|
||
|
||
|
||
uc_order_comment_save($order->order_id, $order->uid, $result['message'], 'admin');
|
||
|
||
watchdog('uc_stripe', 'Renewal failed for order @order_id, code=@code, message: @message', array('@order_id' => $order->order_id, '@code' => $e->getCode(), '@message' => $e->getMessage()));
|
||
|
||
return FALSE;
|
||
} catch (Exception $e) {
|
||
watchdog('uc_stripe', 'Renewal failed for order @order_id, code=@code, message: @message', array('@order_id' => $order->order_id, '@code' => $e->getCode(), '@message' => $e->getMessage()));
|
||
return FALSE;
|
||
}
|
||
|
||
|
||
}
|
||
|
||
/**
|
||
* UC Recurring: Process a new recurring fee.
|
||
* This runs when subscriptions are "set up" for the first time.
|
||
* There is no action to be taken here except returning TRUE because the customer
|
||
* ID is already stored with the user, where it can be accessed when next charge
|
||
* takes place.
|
||
*
|
||
* @param $order
|
||
* @param $fee
|
||
* @return bool
|
||
*/
|
||
function uc_stripe_process($order, &$fee) {
|
||
return TRUE;
|
||
}
|
||
|
||
/**
|
||
* UC Recurring: Cancel a recurring fee.
|
||
* This runs when subscriptions are cancelled
|
||
* Since we're handling charge intervals in ubercart, this doesn't need to do anything.
|
||
*
|
||
* @param $order
|
||
* @param $op
|
||
* @return bool
|
||
*/
|
||
function uc_stripe_cancel($order, $op) {
|
||
$message = t("Subscription Canceled");
|
||
uc_order_comment_save($order->order_id, $order->uid, $message, 'order', 'completed', FALSE);
|
||
return TRUE;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Load stripe API
|
||
*
|
||
* @return bool
|
||
*/
|
||
function _uc_stripe_prepare_api() {
|
||
|
||
module_load_include('install', 'uc_stripe');
|
||
if (!_uc_stripe_load_library()) {
|
||
return FALSE;
|
||
}
|
||
|
||
if (!_uc_stripe_check_api_keys()) {
|
||
watchdog('uc_stripe', 'Stripe API keys are not configured. Payments cannot be made without them.', array(), WATCHDOG_ERROR);
|
||
return FALSE;
|
||
}
|
||
|
||
$secret_key = variable_get('uc_stripe_testmode', TRUE) ? variable_get('uc_stripe_api_key_test_secret', '') : variable_get('uc_stripe_api_key_live_secret', '');
|
||
try {
|
||
$library = libraries_load('stripe');
|
||
\Stripe\Stripe::setApiKey($secret_key);
|
||
\Stripe\Stripe::setApiVersion($library['stripe_api_version']);
|
||
} catch (Exception $e) {
|
||
watchdog('uc_stripe', 'Error setting the Stripe API Key. Payments will not be processed: %error', array('%error' => $e->getMessage()));
|
||
}
|
||
try{
|
||
$module_info = system_get_info('module', "uc_stripe");
|
||
$uc_stripe_version = is_null($module_info['version']) ? 'dev-unknown' : $module_info['version'];
|
||
\Stripe\Stripe::setAppInfo("Drupal Ubercart Stripe", $uc_stripe_version, "https://www.drupal.org/project/uc_stripe");
|
||
} catch (Exception $e){
|
||
watchdog('uc_stripe', 'Error setting Stripe plugin information: %error', array('%error' => $e->getMessage()));
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
/**
|
||
* Check that all API keys are configured.
|
||
*
|
||
* @return bool
|
||
* TRUE if all 4 keys have a value.
|
||
*/
|
||
function _uc_stripe_check_api_keys() {
|
||
return (variable_get('uc_stripe_api_key_live_publishable', FALSE) &&
|
||
variable_get('uc_stripe_api_key_live_secret', FALSE) &&
|
||
variable_get('uc_stripe_api_key_test_publishable', FALSE) &&
|
||
variable_get('uc_stripe_api_key_test_secret', FALSE));
|
||
}
|
||
|
||
/**
|
||
* Retrieve the Stripe customer id for a user
|
||
*
|
||
* @param $uid
|
||
* @return bool
|
||
*/
|
||
function _uc_stripe_get_customer_id($uid) {
|
||
|
||
$account = user_load($uid);
|
||
|
||
$id = !empty($account->data['uc_stripe_customer_id']) ? $account->data['uc_stripe_customer_id'] : FALSE;
|
||
return $id;
|
||
}
|
||
|
||
/**
|
||
* Retrieve the Stripe payment id for a user
|
||
*
|
||
* @param $uid
|
||
* @return bool
|
||
*/
|
||
function _uc_stripe_get_payment_id($uid) {
|
||
|
||
$account = user_load($uid);
|
||
|
||
$id = !empty($account->data['uc_stripe_payment_id']) ? $account->data['uc_stripe_payment_id'] : FALSE;
|
||
return $id;
|
||
}
|
||
|
||
|
||
/**
|
||
* Implements hook_theme_registry_alter() to make sure that we render
|
||
* the entire credit form, including the key returned by JS.
|
||
*
|
||
* @param $theme_registry
|
||
*/
|
||
function uc_stripe_theme_registry_alter(&$theme_registry) {
|
||
if (!empty($theme_registry['uc_payment_method_credit_form'])) {
|
||
$theme_registry['uc_payment_method_credit_form']['function'] = 'uc_stripe_uc_payment_method_credit_form';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Replace uc_credit's form themeing with our own - adds stripe_token.
|
||
* @param $form
|
||
* @return string
|
||
*/
|
||
function uc_stripe_uc_payment_method_credit_form($form) {
|
||
$output .= drupal_render($form['config_error']);
|
||
$output .= theme('uc_payment_method_credit_form',$form);
|
||
$output .= drupal_render($form['stripe_payment_method']);
|
||
$output .= drupal_render($form['dummy_image_load']);
|
||
|
||
return $output;
|
||
}
|
||
|
||
/**
|
||
* Used to return the appropriate response after checking Stripe Payment Intent
|
||
* status
|
||
* @param Object $intent
|
||
* @return string response
|
||
*/
|
||
function _generatePaymentResponse($intent) {
|
||
|
||
if ($intent->status == 'requires_action' &&
|
||
$intent->next_action->type == 'use_stripe_sdk') {
|
||
# Tell the client to handle the action
|
||
$response = [
|
||
'requires_action' => true,
|
||
'payment_intent_client_secret' => $intent->client_secret
|
||
];
|
||
} else if ($intent->status == 'succeeded') {
|
||
# The payment didn’t need any additional actions and completed!
|
||
# Handle post-payment fulfillment
|
||
$response = ['success' => true];
|
||
} else {
|
||
# Invalid status
|
||
http_response_code(500);
|
||
$response = ['error' => 'Invalid PaymentIntent status'];
|
||
}
|
||
|
||
return $response;
|
||
}
|
||
|
||
/**
|
||
* Ajax page callback for callback uc_stripe/ajax/confirm_payment page
|
||
* This is used to send payment and intent status back to JS client
|
||
* @return string Json response
|
||
*/
|
||
function _uc_stripe_confirm_payment(){
|
||
|
||
global $user;
|
||
|
||
# retrieve json from POST body
|
||
$received_json = file_get_contents("php://input", TRUE);
|
||
$data = drupal_json_decode($received_json, TRUE);
|
||
|
||
$order_id = $data['order_id'];
|
||
$order = uc_order_load($order_id);
|
||
|
||
if (!_uc_stripe_prepare_api()) {
|
||
|
||
$message = 'Stripe API not found.';
|
||
|
||
watchdog('uc_stripe', 'Error in Stripe API: @message', array('@message' => $message));
|
||
return ['error' => $message];
|
||
}
|
||
// Format the amount in cents, which is what Stripe wants
|
||
$amount = uc_currency_format($order->order_total, FALSE, FALSE, FALSE);
|
||
|
||
|
||
$stripe_customer_id = False;
|
||
$order_has_stripe_id = key_exists('stripe_customer_id', $order->data) ? True : False;
|
||
|
||
// Check various places to get the stripe_customer_id. If not found we'll create
|
||
// a new stripe user.
|
||
if($order_has_stripe_id){
|
||
$stripe_customer_id = $order->data['stripe_customer_id'];
|
||
}
|
||
else if ($user->uid != $order->uid) {
|
||
$stripe_customer_id = _uc_stripe_get_customer_id($order->uid);
|
||
} else {
|
||
$stripe_customer_id = _uc_stripe_get_customer_id($user->uid);
|
||
}
|
||
|
||
// In the case where the stored customer_id is not a valid customer in Stripe
|
||
// then we'll need to create a new stripe customer. see #3071712
|
||
if($stripe_customer_id && !_uc_stripe_is_stripe_id_valid($stripe_customer_id)){
|
||
watchdog('uc_stripe', 'Stripe customer: @customer is not valid in this instance of Stripe. A new customer will be created.', array('@customer' => $stripe_customer_id));
|
||
$stripe_customer_id = false;
|
||
}
|
||
|
||
$intent = null;
|
||
try {
|
||
if (isset($data['payment_method_id'])) {
|
||
|
||
$params = array(
|
||
'payment_method' => $data['payment_method_id'],
|
||
"description" => t("Order #@order_id", array("@order_id" => $order_id)),
|
||
"metadata" => $metadata,
|
||
'amount' => $amount,
|
||
'currency' => strtolower($order->currency),
|
||
'confirmation_method' => 'manual',
|
||
'confirm' => true,
|
||
'setup_future_usage' => 'off_session',
|
||
'save_payment_method' => true,
|
||
);
|
||
|
||
if(!$stripe_customer_id) {
|
||
$customer = _uc_stripe_create_stripe_customer($order, $data['payment_method_id']);
|
||
if(!$customer){
|
||
$message = 'Customer creation failed.';
|
||
$_SESSION['stripe']['payment_failed'] = TRUE;
|
||
return ['error' => $message];
|
||
}
|
||
$stripe_customer_id = $customer->id;
|
||
}
|
||
|
||
$params['customer'] = $stripe_customer_id;
|
||
|
||
# Create the PaymentIntent
|
||
$intent = \Stripe\PaymentIntent::create($params);
|
||
|
||
if(!$order_has_stripe_id){
|
||
$order->data['stripe_customer_id'] = $stripe_customer_id;
|
||
}
|
||
|
||
$order->data['payment_intent_id'] = $intent->id;
|
||
uc_order_save($order);
|
||
}
|
||
if (isset($data['payment_intent_id'])) {
|
||
$intent = \Stripe\PaymentIntent::retrieve(
|
||
$data['payment_intent_id']
|
||
);
|
||
$intent->confirm();
|
||
|
||
$order->data['payment_intent_id'] = $data['payment_intent_id'];
|
||
uc_order_save($order);
|
||
}
|
||
return _generatePaymentResponse($intent);
|
||
} catch (Exception $e) {
|
||
watchdog('uc_stripe', 'Payment could not be processed: @message', array('@message' => $e->getMessage()));
|
||
return ['error' => $e->getMessage()];
|
||
}
|
||
}
|
||
|
||
function _uc_stripe_create_stripe_customer($order, $payment_method_id = NULL){
|
||
|
||
$stripe_customer_id = FALSE;
|
||
|
||
try {
|
||
// If the token is not in the user's session, we can't set up a new customer
|
||
|
||
$shipping_info = array();
|
||
if (!empty($order->delivery_postal_code)) {
|
||
$shipping_info = array(
|
||
'name' => @"{$order->delivery_first_name} {$order->delivery_last_name}",
|
||
'phone' => @$order->delivery_phone,
|
||
);
|
||
|
||
$delivery_country = uc_get_country_data(array('country_id' => $order->delivery_country));
|
||
if ($delivery_country === FALSE) {
|
||
$delivery_country = array(0 => array('country_iso_code_2' => 'US'));
|
||
}
|
||
|
||
$shipping_info['address'] = array(
|
||
'city' => @$order->delivery_city,
|
||
'country' => @$delivery_country[0]['country_iso_code_2'],
|
||
'line1' => @$order->delivery_street1,
|
||
'line2' => @$order->delivery_street2,
|
||
'postal_code' => @$order->delivery_postal_code,
|
||
);
|
||
}
|
||
|
||
$params = array(
|
||
'description' => "OrderID: {$order->order_id}",
|
||
"metadata" => $metadata,
|
||
'email' => "$order->primary_email"
|
||
);
|
||
|
||
if (!empty($shipping_info)) {
|
||
$params['shipping'] = $shipping_info;
|
||
}
|
||
//Create the customer in stripe
|
||
$customer = \Stripe\Customer::create($params);
|
||
|
||
return $customer;
|
||
|
||
} catch (Exception $e) {
|
||
$message = t("Stripe Customer Creation Failed for order !order: !message", array(
|
||
"!order" => $order_id,
|
||
"!message" => $e->getMessage()
|
||
));
|
||
|
||
uc_order_comment_save($order_id, $user->uid, $message, 'admin');
|
||
|
||
watchdog('uc_stripe', 'Failed stripe customer creation: @message', array('@message' => $message));
|
||
|
||
return false;
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param string $stripe_id
|
||
* @return boolean result - if stripe_id is valid based on stripe api customer call
|
||
*/
|
||
function _uc_stripe_is_stripe_id_valid($stripe_id){
|
||
try{
|
||
|
||
if (!_uc_stripe_prepare_api()) {
|
||
|
||
$message = 'Stripe API not found.';
|
||
|
||
watchdog('uc_stripe', 'Error in Stripe API: @message', array('@message' => $message));
|
||
return false;
|
||
}
|
||
|
||
$customer = \Stripe\Customer::retrieve($stripe_id);
|
||
|
||
// Count deleted stripe customers as invalid
|
||
return !$customer->deleted;
|
||
|
||
} catch (Exception $e) {
|
||
// IF customer is not found, an exception is thrown.
|
||
return false;
|
||
}
|
||
}
|