StripePaymentMethodController.inc in Stripe 7
Stripe Payment controller class and helper code (classes and function).
File
stripe_payment/includes/StripePaymentMethodController.incView source
<?php
/**
* @file
* Stripe Payment controller class and helper code (classes and function).
*/
class StripePaymentValidationException extends PaymentValidationException {
}
class StripePaymentMethodController extends PaymentMethodController {
/**
* The function name of the payment configuration form elements.
*/
public $payment_configuration_form_elements_callback = 'stripe_payment_configuration_form_elements';
/**
* The function name of the payment method configuration form elements.
*/
public $payment_method_configuration_form_elements_callback = 'stripe_payment_method_configuration_form_elements';
/**
* Default values for the controller_data property of a PaymentMethod that
* uses this controller.
*/
public $controller_data_defaults = array(
'keys' => array(
'mode' => 0,
'secret' => '',
'publishable' => '',
),
);
/**
* Public constructor.
*/
public function __construct() {
static::loadLibrary();
$cache = cache_get('stripe_payment_currencies');
if ($cache && !empty($cache->data)) {
$this->currencies = $cache->data;
}
else {
try {
// Available payment currencies will vary based on
// the country in which the Stripe account is located.
$account = \Stripe\Account::retrieve();
$country_spec = \Stripe\CountrySpec::retrieve($account->country);
$currencies = $country_spec->supported_payment_currencies;
$currencies = array_combine(array_map('strtoupper', $currencies), $currencies);
cache_set('stripe_payment_currencies', $currencies, 'cache', CACHE_TEMPORARY);
$this->currencies = $currencies;
} catch (Exception $e) {
if ($e
->getMessage()) {
drupal_set_message($e
->getMessage(), 'error');
watchdog('stripe_payment', 'Stripe supported payment currencies load error: @message.', array(
'@message' => $e
->getMessage(),
), WATCHDOG_ERROR);
}
}
}
$this->title = t('Stripe Payment');
}
/**
* Wakeup.
*/
public function __wakeup() {
static::loadLibrary();
}
/**
* Ensure the Stripe API library is loaded.
*
* This method is intended to be invoked fron __constructor() and __wakeup()
* to ensure the Stripe API is loaded before the controller is used. Avoiding
* the need to load the Stripe API in every method.
*
* @throws Exception
* If the Stripe API library can not be loaded.
*/
protected static function loadLibrary() {
if (!stripe_load_library()) {
throw new Exception('Cannot load Stripe PHP Library.');
}
}
/**
* Validate a payment against a payment method and this controller.
*
* Don't call directly. Use PaymentMethod::validate() instead.
*
* @see PaymentMethod::validate()
*
* @param Payment $payment
* The validated payment.
* @param PaymentMethod $payment_method
* The payment method for the validated payment.
* @param boolean $strict
* Whether to validate everything a payment method needs or to validate the
* most important things only. Useful when finding available payment
* methods, for instance, which does not require unimportant things to be a
* 100% valid.
*
* @throws PaymentValidationException
*/
public function validate(Payment $payment, PaymentMethod $payment_method, $strict) {
parent::validate($payment, $payment_method, $strict);
// Confirm the payment's currency is supported (for the Stripe account).
if ($stripe_account = $this
->retrieveAccount($payment_method)) {
if (!in_array($payment->currency_code, array_keys($this->currencies))) {
throw new PaymentValidationUnsupportedCurrencyException(t('The currency is not supported by this payment method.'));
}
}
// Confirm we have a token, a customer or a card information.
if ($strict && !isset($payment->method_data['token']) && !isset($payment->method_data['card']) && !isset($payment->method_data['customer'])) {
throw new StripePaymentValidationException("A Stripe payment must have card or a customer to charge.");
}
}
/**
* Execute a payment.
*
* @param Payment $payment
* The executed payment.
*
* @return boolean
* Whether the payment was successfully executed or not.
*/
public function execute(Payment $payment) {
$api_key = !empty($payment->method->controller_data['keys']['mode']) ? $payment->method->controller_data['keys']['secret'] : stripe_get_key('secret');
try {
if (empty($payment->method_data['charge'])) {
// Create the Stripe Charge for the payment.
$data = array(
'amount' => $payment
->totalAmount(TRUE) * 100,
'currency' => $this->currencies[$payment->currency_code],
'description' => t($payment->description, $payment->description_arguments),
);
// Charge a token...
if (isset($payment->method_data['token'])) {
$data['card'] = $payment->method_data['token'];
}
elseif (isset($payment->method_data['customer'])) {
$data['customer'] = $payment->method_data['customer'];
}
elseif (isset($payment->method_data['card'])) {
$data['card'] = $payment->method_data['card'];
}
// Support execution of Payment with a non-captured charge. Currently
// not provided in the UI. Probably not usable without more work.
if (isset($payment->method_data['capture'])) {
$data['capture'] = $payment->method_data['capture'];
}
// Support for application fee. Currently not provided in the UI.
// Probably not usable without more work.
if (isset($payment->method_data['application_fee'])) {
$data['capture'] = $payment->method_data['application_fee'];
}
$charge = \Stripe\Charge::create($data, $api_key);
$payment
->setStatus($this
->statusFromCharge($charge));
$payment->method_data['charge'] = $charge->id;
watchdog('stripe_payment', 'Stripe Charge [id=@charge_id] created for Payment [pid=@pid].', array(
'@charge_id' => $payment->method_data['charge'],
'@pid' => $payment->pid,
), WATCHDOG_DEBUG, l(t('view'), "payment/{$payment->pid}", array(
'absolute' => TRUE,
)));
}
else {
// The Payment is already linked to a Stripe Charge. This should not
// happen. Let's update the payment status and pretend everything went
// fine.
watchdog('stripe_payment', 'Executing a payment already linked to a Stripe Charge [id=@charge_id].', array(
'@charge_id' => $payment->method_data['charge'],
), WATCHDOG_WARNING, url("payment/{$payment->pid}"));
// Do not use the StripePaymentMethodController::retrieveCharge() method
// since we want to catch Stripe exception.
$charge = \Stripe\Charge::retrieve($payment->method_data['charge'], $api_key);
$payment
->setStatus($this
->statusFromCharge($charge));
}
// If we get to this LoC, then the payment execution has succeeded.
return TRUE;
} catch (\Stripe\Error\Card $e) {
$payment
->setStatus($this
->statusFromCardErrorCode($e
->getCode()));
// Display human-readable message.
if ($e
->getMessage()) {
drupal_set_message($e
->getMessage(), 'error');
}
// Log error.
watchdog('stripe_payment', 'Stripe Card Error for Payment [pid=@pid]: @message.', array(
'@pid' => $payment->pid,
'@message' => $e
->getMessage(),
), WATCHDOG_ERROR, l(t('view'), "payment/{$payment->pid}"));
} catch (\Stripe\Error\InvalidRequest $e) {
$payment
->setStatus(new PaymentStatusItem(STRIPE_PAYMENT_STATUS_INVALID_REQUEST));
watchdog('stripe_payment', 'Invalid Stripe Request for Payment [pid=@pid]: @message.<br/>!json_body', array(
'@pid' => $payment->pid,
'@message' => $e
->getMessage(),
'!json_body' => '<?php ' . highlight_string(var_export($e
->getJsonBody(), TRUE), TRUE),
), WATCHDOG_ERROR, l(t('view'), "payment/{$payment->pid}"));
} catch (\Stripe\Error\Authentication $e) {
$payment
->setStatus(new PaymentStatusItem(STRIPE_PAYMENT_STATUS_AUTHENTICATION_ERROR));
if (payment_method_access('update', $payment->method)) {
drupal_set_message(t("Stripe Authentication Error for Payment [pid=@pid]: @message.", array(
'@pid' => $payment->pid,
'@message' => $e
->getMessage(),
)) . t("Please review <a href='@url'>configuration</a>.", array(
'@url' => url("admin/config/services/payment/method/{$payment->method->pmid}"),
)), 'error');
}
watchdog('stripe_payment', "Stripe Authentication Error for Payment [pid=@pid]: @message.", array(
'@pid' => $payment->pid,
'@message' => $e
->getMessage(),
), WATCHDOG_ERROR, l(t('edit'), "admin/config/services/payment/method/{$payment->method->pmid}"));
} catch (\Stripe\Error\Api $e) {
$payment
->setStatus(new PaymentStatusItem(STRIPE_PAYMENT_STATUS_API_ERROR));
watchdog('stripe_payment', 'Stripe API Error for Payment [pid=@pid]: @message.<br/>!json_body', array(
'@pid' => $payment->pid,
'@message' => $e
->getMessage(),
'!json_body' => '<?php ' . highlight_string(var_export($e
->getJsonBody(), TRUE), TRUE),
), WATCHDOG_ERROR, l(t('view'), "payment/{$payment->pid}"));
} catch (Exception $e) {
$payment
->setStatus(new PaymentStatusItem(STRIPE_PAYMENT_STATUS_UNKNOWN_ERROR));
watchdog('stripe_payment', 'Unknown Exception for Payment [pid=@pid]: @message.<br/><pre>!trace</pre>', array(
'@pid' => $payment->pid,
'@message' => $e
->getMessage(),
'!trace' => $e
->getTraceAsString(),
), WATCHDOG_ERROR, l(t('view'), "payment/{$payment->pid}"));
}
// The only way to reach this LoC is to exit the try block with an
// exception. Thus, the payment execution has failed.
return FALSE;
}
/**
* Retrieve the Stripe Account object for a given Payment Method.
*
* This method handles Stripe errors and logs them to Drupal's watchdog.
*
* Result for a given Payment Method is cached both statically and temporarily
* in Drupal's cache.
*
* @param PaymentMethod $payment_method
* A Payment Method.
*
* @return \Stripe\Account
* The Stripe Account object for the given payment method. Or NULL if the
* account could not be retrieved (errors are logged to Drupal's watchdog).
*/
public function retrieveAccount(PaymentMethod $payment_method) {
if ($payment_method->controller->name === 'StripePaymentMethodController') {
$accounts[] =& drupal_static(__METHOD__, array());
if (!isset($accounts[$payment_method->pmid])) {
$cid = "stripe_payment:{$payment_method->pmid}:account";
$cache = cache_get($cid);
if ($cache && !empty($cache->data)) {
$accounts[$payment_method->pmid] = $cache->data;
}
else {
$api_key = !empty($payment_method->controller_data['keys']['mode']) ? $payment_method->controller_data['keys']['secret'] : stripe_get_key('secret');
try {
$stripe_account = \Stripe\Account::retrieve($api_key);
cache_set($cid, $stripe_account, 'cache', CACHE_TEMPORARY);
$accounts[$payment_method->pmid] = $stripe_account;
} catch (\Stripe\Error $e) {
if (payment_method_access('update', $payment_method)) {
drupal_set_message(t("Unable to retrieve Stripe Account for <a href='@url'>Payment Method</a>: @message.", array(
'@message' => $e
->getMessage(),
'@url' => url("admin/config/services/payment/method/{$payment_method->pmid}"),
)), 'error');
}
watchdog('stripe_payment', "Unable to retrieve Stripe Account for <a href='@url'>Payment Method</a>: @message.", array(
'@message' => $e
->getMessage(),
'@url' => url("admin/config/services/payment/method/{$payment_method->pmid}"),
), WATCHDOG_ERROR, l(t('edit'), "admin/config/services/payment/method/{$payment_method->pmid}"));
$accounts[$payment_method->pmid] = NULL;
}
}
}
return $accounts[$payment_method->pmid];
}
else {
return NULL;
}
}
/**
* Retrieve the Stripe Charge object for a given Payment.
*
* This method handles Stripe errors and logs them to Drupal's watchdog. If
* the user has view access to the Payment object, errors are also displayed
* as error message.
*
* Result for a given Payment and its Payment Method is cached temporarily in
* Drupal's cache.
*
* @param Payment $payment
* A Payment object.
*
* @return \Stripe\Charge
* The Stripe Charge object for the given Payment. Or NULL if no Stripe
* Charge could be retrieved (errors are logged to Drupal's watchdog) or
* if the Payment does not use Stripe.
*/
public function retrieveCharge(Payment $payment) {
if ($payment->method->controller->name === 'StripePaymentMethodController' && isset($payment->method_data['charge'])) {
$cid = "stripe_payment:{$payment->method->pmid}:charge:{$payment->method_data['charge']}";
$cache = cache_get($cid);
if ($cache && !empty($cache->data)) {
return $cache->data;
}
else {
$api_key = !empty($payment->method->controller_data['keys']['mode']) ? $payment->method->controller_data['keys']['secret'] : stripe_get_key('secret');
try {
$charge = \Stripe\Charge::retrieve($payment->method_data['charge'], $api_key);
cache_set($cid, $charge, 'cache', CACHE_TEMPORARY);
return $charge;
} catch (\Stripe\Error $e) {
if (payment_access('view', $payment)) {
drupal_set_message(t('Unable to retrieve Stripe Charge for <a href="@url">Payment</a>: @message.', array(
'@url' => url("payment/{$payment->pid}"),
'@message' => $e
->getMessage(),
)), 'error');
}
watchdog('stripe_payment', 'Unable to retrieve Stripe Charge for <a href="@url">Payment</a>: @message.', array(
'@url' => url("payment/{$payment->pid}"),
'@message' => $e
->getMessage(),
), WATCHDOG_ERROR, l(t('view'), "payment/{$payment->pid}"));
}
}
}
return NULL;
}
/**
* Retrieve the Stripe Token object for a given Payment.
*
* This method handles Stripe errors and logs them to Drupal's watchdog. If
* the user as view access to the Payment object, errors are also displayed
* as error message.
*
* @param Payment $payment
* A Payment object.
*
* @return \Stripe\Token
* The Stripe Token object for the given Payment. Or NULL if no Stripe
* Token could be retrieved (errors are logged to Drupal's watchdog) or if
* the Payment does not use Stripe.
*/
public function retrieveToken(Payment $payment) {
if ($payment->method->controller->name === 'StripePaymentMethodController' && $payment->method_data['token']) {
$api_key = !empty($payment->method->controller_data['keys']['mode']) ? $payment->method->controller_data['keys']['secret'] : stripe_get_key('secret');
try {
return \Stripe\Token::retrieve($payment->method_data['token'], $api_key);
} catch (\Stripe\Error $e) {
if (payment_access('view', $payment)) {
drupal_set_message(t('Unable to retrieve Stripe Token for <a href="@url">Payment</a>: @message.', array(
'@url' => url("payment/{$payment->pid}"),
'@message' => $e
->getMessage(),
)), 'error');
}
watchdog('stripe_payment', 'Unable to retrieve Stripe Token for <a href="@url">Payment</a>: @message.', array(
'@url' => url("payment/{$payment->pid}"),
'@message' => $e
->getMessage(),
), WATCHDOG_ERROR, l(t('view'), "payment/{$payment->pid}"));
}
}
return NULL;
}
/**
* Map a card error code to a Payment status.
*
* @paran string $code
* A Stripe card error code.
*
* @return PaymentStatusItem
* The Payment status matching the error code.
*/
protected function statusFromCardErrorCode($code) {
if (empty($code)) {
return new PaymentStatusItem(STRIPE_PAYMENT_STATUS_UNKNOWN_ERROR);
}
$status = 'STRIPE_PAYMENT_STATUS_' . drupal_strtoupper($code);
if (defined($status)) {
return new PaymentStatusItem(constant($status));
}
else {
return new PaymentStatusItem(STRIPE_PAYMENT_STATUS_UNKNOWN_ERROR);
}
}
/**
* Map a Strip Charge object to a Payment status.
*
* @param \Stripe\Charge $charge
* A stripe charge object.
*
* TODO: Add support for refunded charge.
*
* @return PaymentStatusItem
* The Payment status matching the charge object.
*/
protected function statusFromCharge(\Stripe\Charge $charge) {
if ($charge->paid) {
return new PaymentStatusItem(STRIPE_PAYMENT_STATUS_PAID);
}
elseif (!$charge->captured) {
return new PaymentStatusItem(STRIPE_PAYMENT_STATUS_UNCAPTURED);
}
else {
return new PaymentStatusItem(PAYMENT_STATUS_UNKNOWN);
}
}
/**
* Builder for the method configuration form elements.
*
* @param array $element
* The parent element.
* @param array $form_state
* The form states.
*
* TODO: Allow admin to configure a payment method WITHOUT Stripe.js.
* The card information for the method's Payment will then be be stored
* in the Payment::method_data. Provide clear and visible warning with
* information regarding the security risk and PCI compliance.
* TODO: Add option to (not) collect card holder's name.
* TODO: Add option to collect card holder's address.
*
* @return array
* The forms elements to configure a payment method.
*/
public function payment_method_configuration_form_elements(array $element, array &$form_state) {
$controller_data = $form_state['payment_method']->controller_data + $this->controller_data_defaults;
$elements = array();
$elements['keys'] = array(
'#type' => 'fieldset',
'#title' => t('Authentication'),
'#tree' => TRUE,
'mode' => array(
'#type' => 'select',
'#options' => array(
0 => t('Use site-wide keys'),
1 => t('Use specific keys'),
),
'#attributes' => array(
'class' => array(
'keys-mode',
),
),
'#default_value' => $controller_data['keys']['mode'],
),
'site-wide' => array(
'#type' => 'item',
'#markup' => t('Using site-wide %status keys as configured on the <a href="@url">Stripe settings page</a>.', array(
'%status' => variable_get('stripe_key_status', 'test'),
'@url' => url('admin/config/stripe/settings'),
)),
'#states' => array(
'visible' => array(
':input.keys-mode' => array(
'value' => 0,
),
),
),
),
'secret' => array(
'#type' => 'textfield',
'#title' => t('Secret Key'),
'#states' => array(
'visible' => array(
':input.keys-mode' => array(
'value' => 1,
),
),
'required' => array(
':input.keys-mode' => array(
'value' => 1,
),
),
),
'#default_value' => $controller_data['keys']['secret'],
),
'publishable' => array(
'#type' => 'textfield',
'#title' => t('Publishable Key'),
'#states' => array(
'visible' => array(
':input.keys-mode' => array(
'value' => 1,
),
),
'required' => array(
':input.keys-mode' => array(
'value' => 1,
),
),
),
'#default_value' => $controller_data['keys']['publishable'],
),
);
return $elements;
}
/**
* Validate callback for the method configuration form elements.
*
* @param array $element
* The parent element.
* @param array $form_state
* The form states.
*/
public function payment_method_configuration_form_elements_validate(array $element, array &$form_state) {
$values = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
if ($values['keys']['mode']) {
foreach (array(
'secret' => 'sk',
'publishable' => 'pk',
) as $type => $prefix) {
if (empty($values['keys'][$type])) {
form_error($element['keys'][$type], t('!name field is required.', array(
'!name' => $element['keys'][$type]['#title'],
)));
}
elseif (strpos($values['keys'][$type], $prefix) !== 0) {
form_error($element['keys'][$type], t('!name should start with %prefix.', array(
'!name' => $element['keys'][$type]['#title'],
'%prefix' => $prefix,
)));
}
}
}
$form_state['payment_method']->controller_data['keys'] = $values['keys'];
}
/**
* Builder callback for the payment configuration form elements.
*
* TODO: Support form WITHOUT Stripe.js.
* (see TODO for payment_method_configuration_form_elements)
* TODO: Support option to (not) collect card holder's name.
* TODO: Support option to collect card holder's address.
*
* @param array $element
* The parent element.
* @param array $form_state
* The form states.
*
* @return array
* The forms elements to configure a payment.
*/
public function payment_configuration_form_elements(array $element, array &$form_state) {
$payment = $form_state['payment'];
$publishable_key = !empty($payment->method->controller_data['keys']['mode']) ? $payment->method->controller_data['keys']['publishable'] : stripe_get_key('publishable');
$element = array(
'#type' => 'stripe_payment',
'#publishable_key' => $publishable_key,
);
return array(
'stripe' => $element,
);
}
/**
* Validate callback for the payment configuration form elements.
*
* @param array $element
* The parent element.
* @param array $form_state
* The form states.
*/
public function payment_configuration_form_elements_validate(array $element, array &$form_state) {
$values = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
$payment =& $form_state['payment'];
$payment->method_data['token'] = $values['stripe']['stripe_token'];
}
}
Classes
Name | Description |
---|---|
StripePaymentMethodController | |
StripePaymentValidationException | @file Stripe Payment controller class and helper code (classes and function). |