You are here

commerce_paypal_checkout.module in Commerce PayPal 7.2

Implements PayPal Checkout in Drupal Commerce checkout.

File

modules/checkout/commerce_paypal_checkout.module
View source
<?php

/**
 * @file
 * Implements PayPal Checkout in Drupal Commerce checkout.
 */

/**
 * Implements hook_menu().
 */
function commerce_paypal_checkout_menu() {
  return array(
    // Add a menu item for capturing authorizations.
    'admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/paypal-checkout-capture' => array(
      'title' => 'Capture',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'commerce_paypal_checkout_capture_form',
        3,
        5,
      ),
      'access callback' => 'commerce_paypal_checkout_capture_void_access',
      'access arguments' => array(
        3,
        5,
      ),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'context' => MENU_CONTEXT_INLINE,
      'weight' => 2,
      'file' => 'includes/commerce_paypal_checkout.admin.inc',
    ),
    // Add a menu item for voiding authorizations.
    'admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/paypal-checkout-void' => array(
      'title' => 'Void',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'commerce_paypal_checkout_void_form',
        3,
        5,
      ),
      'access callback' => 'commerce_paypal_checkout_capture_void_access',
      'access arguments' => array(
        3,
        5,
      ),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'context' => MENU_CONTEXT_INLINE,
      'weight' => 4,
      'file' => 'includes/commerce_paypal_checkout.admin.inc',
    ),
    // Add a menu item for refunding settled transactions.
    'admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/paypal-checkout-refund' => array(
      'title' => 'Refund',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'commerce_paypal_checkout_refund_form',
        3,
        5,
      ),
      'access callback' => 'commerce_paypal_checkout_refund_access',
      'access arguments' => array(
        3,
        5,
      ),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'context' => MENU_CONTEXT_INLINE,
      'weight' => 4,
      'file' => 'includes/commerce_paypal_checkout.admin.inc',
    ),
    'commerce-paypal-checkout/create-order/%commerce_order/%commerce_paypal_checkout_method_instance' => array(
      'page callback' => 'commerce_paypal_checkout_create_order',
      'page arguments' => array(
        2,
        3,
      ),
      'access callback' => 'commerce_checkout_access',
      'access arguments' => array(
        2,
      ),
      'type' => MENU_CALLBACK,
    ),
    'commerce-paypal-checkout/approve-order/%commerce_order/%commerce_paypal_checkout_method_instance/%' => array(
      'page callback' => 'commerce_paypal_checkout_approve_order',
      'page arguments' => array(
        2,
        3,
        4,
      ),
      'access callback' => 'commerce_checkout_access',
      'access arguments' => array(
        2,
      ),
      'type' => MENU_CALLBACK,
    ),
  );
}

/**
 * Determines access to the prior authorization capture form or void form for
 * Paypal Checkout transactions.
 *
 * @param $order
 *   The order the transaction is on.
 * @param $transaction
 *   The payment transaction object to be captured.
 *
 * @return
 *   TRUE or FALSE indicating access.
 */
function commerce_paypal_checkout_capture_void_access($order, $transaction) {

  // Return FALSE if the transaction isn't for Paypal Checkout or isn't
  // awaiting capture.
  if ($transaction->payment_method != 'paypal_checkout' || strtolower($transaction->remote_status) != 'created') {
    return FALSE;
  }

  // Return FALSE if the transaction is not pending.
  if ($transaction->status != COMMERCE_PAYMENT_STATUS_PENDING) {
    return FALSE;
  }

  // Return FALSE if it is more than 29 days past the original authorization.
  if (REQUEST_TIME - $transaction->created > 86400 * 29) {
    return FALSE;
  }

  // Allow access if the user can update payments on this transaction.
  return commerce_payment_transaction_access('update', $transaction);
}

/**
 * Determines access to the refund form for Paypal Checkout transactions.
 *
 * @param $order
 *   The order the transaction is on.
 * @param $transaction
 *   The payment transaction object to be captured.
 *
 * @return
 *   TRUE or FALSE indicating access.
 */
function commerce_paypal_checkout_refund_access($order, $transaction) {

  // Return FALSE if the transaction isn't Completed.
  if ($transaction->payment_method != 'paypal_checkout' || strtolower($transaction->remote_status) != 'completed') {
    return FALSE;
  }

  // Return FALSE if the transaction was not a success.
  if ($transaction->status != COMMERCE_PAYMENT_STATUS_SUCCESS) {
    return FALSE;
  }

  // Return FALSE if it is more than 60 days since the original transaction.
  if (REQUEST_TIME - $transaction->created > 86400 * 60) {
    return FALSE;
  }

  // Allow access if the user can update payments on this transaction.
  return commerce_payment_transaction_access('update', $transaction);
}

/**
 * Load the given PayPal checkout payment method instance.
 *
 * @param $rule_name
 *   The enabling's rule name.
 *
 * @return
 *   The payment method instance object which is identical to the payment method
 *   object with the addition of the settings array.
 */
function commerce_paypal_checkout_method_instance_load($rule_name) {
  return commerce_payment_method_instance_load("paypal_checkout|{$rule_name}");
}

/**
 * Implements hook_commerce_checkout_page_info().
 */
function commerce_paypal_checkout_commerce_checkout_page_info() {
  $checkout_pages = array();
  $checkout_pages['paypal_checkout'] = array(
    'title' => t('Confirm order'),
    'help' => t('Confirm your order information and use the button at the bottom of the page to finalize your payment.'),
    'status_cart' => FALSE,
    'locked' => TRUE,
    'buttons' => FALSE,
    'weight' => 30,
  );
  return $checkout_pages;
}

/**
 * Implements hook_commerce_checkout_pane_info().
 */
function commerce_paypal_checkout_commerce_checkout_pane_info() {
  $checkout_panes = array();
  $checkout_panes['paypal_checkout_review'] = array(
    'title' => t('Review and confirm your order'),
    'name' => t('PayPal Checkout review and confirm (only to be used on the confirm order page)'),
    'file' => 'includes/commerce_paypal_checkout.checkout_pane.inc',
    'base' => 'commerce_paypal_checkout_review_pane',
    'page' => 'paypal_checkout',
    'fieldset' => FALSE,
  );
  return $checkout_panes;
}

/**
 * Implements hook_commerce_checkout_router().
 */
function commerce_paypal_checkout_commerce_checkout_router($order, $checkout_page) {

  // If the current page is the PayPal Checkout page but the current order did
  // not use the shortcut Checkout flow...
  if ($checkout_page['page_id'] == 'paypal_checkout' && (empty($order->data['commerce_paypal_checkout']['flow']) || $order->data['commerce_paypal_checkout']['flow'] != 'shortcut')) {

    // Update the order status to the next checkout page.
    $next_page = $checkout_page['next_page'];
    $order = commerce_order_status_update($order, 'checkout_' . $next_page, FALSE, FALSE);

    // Inform modules of checkout completion if the next page is Completed.
    if ($next_page == 'complete') {
      commerce_checkout_complete($order);
    }

    // Redirect to the URL for the new checkout page.
    $target_uri = commerce_checkout_order_uri($order);
    return drupal_goto($target_uri);
  }
}

/**
 * Returns the checkout pane IDs of checkout panes that should be embedded in
 * the PayPal Checkout review and confirm page.
 */
function commerce_paypal_checkout_embedded_checkout_panes() {
  return array_filter(variable_get('commerce_paypal_checkout_review_embedded_panes', array()));
}

/**
 * Implements hook_commerce_payment_method_info().
 */
function commerce_paypal_checkout_commerce_payment_method_info() {
  $payment_methods = array();
  $payment_methods['paypal_checkout'] = array(
    'base' => 'commerce_paypal_checkout',
    'title' => t('PayPal Checkout'),
    'display_title' => t('PayPal'),
    'short_title' => t('PayPal Checkout'),
    'description' => t('PayPal Checkout'),
    'terminal' => FALSE,
    'offsite' => TRUE,
    'offsite_autoredirect' => FALSE,
  );
  return $payment_methods;
}

/**
 * Returns the default settings for the PayPal Checkout payment method.
 */
function commerce_paypal_checkout_default_settings() {
  $default_settings = array(
    'client_id' => '',
    'secret' => '',
    'server' => 'sandbox',
    'intent' => 'capture',
    'disable_funding' => array(),
    'disable_card' => array(),
    'shipping_preference' => 'get_from_file',
    'update_billing_profiles' => TRUE,
    'style' => array(),
    'enable_on_cart' => TRUE,
  );
  if (module_exists('commerce_shipping')) {
    $default_settings['update_shipping_profiles'] = TRUE;
  }
  return $default_settings;
}

/**
 * Payment method callback: settings form.
 */
function commerce_paypal_checkout_settings_form($settings = array()) {
  $form = array();

  // Merge default settings into the stored settings array.
  $settings = (array) $settings + commerce_paypal_checkout_default_settings();
  $shipping_module_enabled = module_exists('commerce_shipping');
  $form['client_id'] = array(
    '#type' => 'textfield',
    '#title' => t('Client ID'),
    '#maxlength' => 255,
    '#default_value' => $settings['client_id'],
    '#required' => TRUE,
  );
  $form['secret'] = array(
    '#type' => 'textfield',
    '#title' => t('Secret'),
    '#maxlength' => 255,
    '#default_value' => $settings['secret'],
    '#required' => TRUE,
  );
  $form['server'] = array(
    '#type' => 'radios',
    '#title' => t('PayPal server'),
    '#options' => array(
      'sandbox' => 'Sandbox - use for testing, requires a PayPal Sandbox account',
      'live' => 'Live - use for processing real transactions',
    ),
    '#default_value' => $settings['server'],
  );
  $form['intent'] = array(
    '#type' => 'radios',
    '#title' => t('Transaction type'),
    '#options' => array(
      'capture' => t('Capture'),
      'authorize' => t('Authorize'),
    ),
    '#default_value' => $settings['intent'],
  );
  $form['disable_funding'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Disable funding sources'),
    '#description' => t('The disabled funding sources for the transaction. Any funding sources passed are not displayed in the Smart payment buttons. By default, funding source eligibility is smartly decided based on a variety of factors.'),
    '#options' => array(
      'card' => t('Credit or Debit Cards'),
      'credit' => t('PayPal Credit'),
      'sepa' => t('SEPA-Lastschrift'),
    ),
    '#default_value' => $settings['disable_funding'],
    '#element_validate' => array(
      'commerce_paypal_checkout_disable_funding_validate',
    ),
  );
  $form['disable_card'] = array(
    '#title' => t('Disable card types'),
    '#description' => t('The disabled cards for the transaction. Any cards passed do not display in the Smart payment buttons. By default, card eligibility is smartly decided based on a variety of factors.'),
    '#type' => 'checkboxes',
    '#options' => array(
      'visa' => t('Visa'),
      'mastercard' => t('Mastercard'),
      'amex' => t('American Express'),
      'discover' => t('Discover'),
      'dinersclub' => t('Diners Club'),
      'unionpay' => t('UnionPay'),
      'jcb' => t('JCB'),
      'elo' => t('Elo'),
      'hiper' => t('Hiper'),
    ),
    '#default_value' => $settings['disable_card'],
    '#element_validate' => array(
      'commerce_paypal_checkout_disable_card_validate',
    ),
  );
  $form['shipping_preference'] = array(
    '#type' => 'radios',
    '#title' => t('Shipping address collection'),
    '#description' => t('PayPal Checkout will only request a shipping address if the Shipping module is enabled to store the address in the order.'),
    '#options' => array(
      'no_shipping' => t('Do not ask for a shipping address at PayPal.'),
    ),
    '#default_value' => 'no_shipping',
  );
  if ($shipping_module_enabled) {
    $form['shipping_preference']['#options'] += array(
      'get_from_file' => t('Ask for a shipping address at PayPal even if the order already has one.'),
      'set_provided_address' => t('Ask for a shipping address at PayPal if the order does not have one yet.'),
    );
    $form['shipping_preference']['#default_value'] = $settings['shipping_preference'];
  }
  $form['update_billing_profiles'] = array(
    '#type' => 'checkbox',
    '#title' => t('Update billing customer profiles with address information the customer enters at PayPal.'),
    '#default_value' => $settings['update_billing_profiles'],
  );
  if ($shipping_module_enabled) {
    $form['update_shipping_profiles'] = array(
      '#type' => 'checkbox',
      '#title' => t('Update shipping customer profiles with address information the customer enters at PayPal.'),
      '#default_value' => $settings['update_shipping_profiles'],
    );
  }
  $form['customize_buttons'] = array(
    '#type' => 'checkbox',
    '#title' => t('Smart button style'),
    '#default_value' => !empty($settings['style']),
    '#title_display' => 'before',
    '#field_suffix' => t('Customize'),
    '#description_display' => 'before',
    '#element_validate' => array(
      'commerce_paypal_checkout_customize_buttons_validate',
    ),
  );
  $form['style'] = array(
    '#type' => 'fieldset',
    '#title' => t('Settings'),
    '#description' => t('For more information, please visit <a href="@url" target="_blank">customize the PayPal buttons</a>.', array(
      '@url' => 'https://developer.paypal.com/docs/checkout/integration-features/customize-button/#layout',
    )),
    '#states' => array(
      'visible' => array(
        ':input[name="parameter[payment_method][settings][payment_method][settings][customize_buttons]"]' => array(
          'checked' => TRUE,
        ),
      ),
    ),
  );

  // Define some default values for the style configuration.
  $settings['style'] += [
    'layout' => 'vertical',
    'color' => 'gold',
    'shape' => 'rect',
    'label' => 'paypal',
    'tagline' => FALSE,
  ];
  $form['style']['layout'] = array(
    '#type' => 'select',
    '#title' => t('Layout'),
    '#default_value' => $settings['style']['layout'],
    '#options' => array(
      'vertical' => t('Vertical (Recommended)'),
      'horizontal' => t('Horizontal'),
    ),
  );
  $form['style']['color'] = array(
    '#type' => 'select',
    '#title' => t('Color'),
    '#options' => array(
      'gold' => t('Gold (Recommended)'),
      'blue' => t('Blue'),
      'silver' => t('Silver'),
    ),
    '#default_value' => $settings['style']['color'],
  );
  $form['style']['shape'] = array(
    '#type' => 'select',
    '#title' => t('Shape'),
    '#options' => array(
      'rect' => t('Rect (Default)'),
      'pill' => t('Pill'),
    ),
    '#default_value' => $settings['style']['shape'],
  );
  $form['style']['label'] = array(
    '#type' => 'select',
    '#title' => t('Label'),
    '#options' => array(
      'paypal' => t('Displays the PayPal logo (Default)'),
      'checkout' => t('Displays the PayPal Checkout button'),
      'pay' => t('Displays the Pay With PayPal button and initializes the checkout flow'),
    ),
    '#default_value' => $settings['style']['label'],
  );
  $form['style']['tagline'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display tagline'),
    '#default_value' => $settings['style']['tagline'],
    '#states' => array(
      'visible' => array(
        ':input[name="parameter[payment_method][settings][payment_method][settings][style][layout]"]' => array(
          'value' => 'horizontal',
        ),
      ),
    ),
  );
  $form['enable_on_cart'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show the Smart payment buttons on the cart form.'),
    '#default_value' => $settings['enable_on_cart'],
  );
  return $form;
}

/**
 * Element validate callback for the customize_buttons payment method setting.
 */
function commerce_paypal_checkout_customize_buttons_validate($element, &$form_state, $form) {

  // Make sure no style is saved when the the "customize_buttons" checkbox
  // is unchecked.
  $parents = $element['#parents'];
  array_pop($parents);
  $parents[] = 'style';
  if (empty($element['#value'])) {
    drupal_array_set_nested_value($form_state['values'], $parents, array(), TRUE);
  }
  else {
    $style = drupal_array_get_nested_value($form_state['values'], $parents);

    // The tagline is not allowed for the vertical layout.
    if ($style['layout'] == 'vertical') {
      unset($style['tagline']);
      drupal_array_set_nested_value($form_state['values'], $parents, $style, TRUE);
    }
  }
}

/**
 * Element validate callback for the disable_funding payment method setting.
 */
function commerce_paypal_checkout_disable_funding_validate($element, &$form_state, $form) {
  form_set_value($element, array_filter($element['#value']), $form_state);
}

/**
 * Element validate callback for the disable_card payment method setting.
 */
function commerce_paypal_checkout_disable_card_validate($element, &$form_state, $form) {
  $parents = $element['#parents'];
  array_pop($parents);
  $parents[] = 'disable_funding';
  $disable_funding = drupal_array_get_nested_value($form_state['values'], $parents);

  // When the "card" funding source is disabled, the "disable_card" setting
  // cannot be specified.
  if (isset($disable_funding['card'])) {
    $element['#value'] = array();
  }
  form_set_value($element, array_filter($element['#value']), $form_state);
}

/**
 * Page callback: Provide the createOrder() callback expected by the SDK.
 */
function commerce_paypal_checkout_create_order($order, $payment_method) {
  $settings = $payment_method['settings'];
  $api_client = commerce_paypal_checkout_api_client($settings);
  if (!$api_client) {
    drupal_json_output(array());
    drupal_exit();
  }
  try {
    $request_body = commerce_paypal_checkout_prepare_order_request($order, $payment_method['settings']);
    drupal_alter('commerce_paypal_checkout_create_order_request', $request_body, $order);
    $json = $api_client
      ->createOrder($request_body);
    drupal_json_output(array(
      'id' => $json['id'],
    ));
    drupal_exit();
  } catch (\Exception $exception) {
    watchdog_exception('commerce_paypal_checkout', $exception);
  }
  drupal_json_output(array());
  drupal_exit();
}

/**
 * Page callback: Provide the onApprove() callback expected by the SDK.
 */
function commerce_paypal_checkout_approve_order($order, $payment_method, $flow) {
  $data = drupal_json_decode(file_get_contents('php://input'));
  if (!in_array($flow, array(
    'shortcut',
    'mark',
  )) || !isset($data['id'])) {
    drupal_json_output(array());
    drupal_exit();
  }

  // Store the PayPal order ID, and the "flow" used ("shortcut"|"mark").
  // Note that we don't perform any validation here, that happens inside
  // commerce_paypal_checkout_redirect_form_validate().
  $order->data['commerce_paypal_checkout'] = array(
    'flow' => $flow,
    'remote_id' => $data['id'],
  );

  // The payment_redirect key is required in the payment return url.
  if (empty($order->data['payment_redirect_key'])) {
    $order->data['payment_redirect_key'] = drupal_hash_base64(time());
  }

  // We have to manually set the payment method if empty, it's also required
  // by the payment redirect form validate callback.
  if (empty($order->data['payment_method'])) {
    $order->data['payment_method'] = $payment_method['instance_id'];
  }

  // Update the order status to the payment page for the shortcut flow.
  if ($flow == 'shortcut') {
    commerce_order_status_update($order, 'checkout_payment', FALSE, NULL, t('Customer clicked the Smart payment buttons on the cart page.'));
  }
  else {
    commerce_order_save($order);
  }
  $return_url = url('checkout/' . $order->order_id . '/payment/return/' . $order->data['payment_redirect_key']);
  drupal_json_output(array(
    'redirectUri' => $return_url,
  ));
  drupal_exit();
}

/**
 * Prepare the request parameters for the create/update order request.
 *
 * @param $order
 *   The order to prepare the request for.
 * @param array $settings
 *   The payment method settings.
 */
function commerce_paypal_checkout_prepare_order_request($order, $settings) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $product_line_item_types = commerce_product_line_item_types();
  $item_total = 0;
  $discount_total = 0;
  $shipping_total = 0;
  $items = array();
  $order_total = $order_wrapper->commerce_order_total
    ->value();
  $currency_code = $order_total['currency_code'];
  foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
    if (!$line_item_wrapper
      ->value()) {
      continue;
    }
    $unit_price = $line_item_wrapper->commerce_unit_price
      ->value();
    if ($line_item_wrapper
      ->getBundle() == 'shipping') {
      $shipping_total += $unit_price['amount'];
    }
    elseif ($line_item_wrapper
      ->getBundle() == 'commerce_discount') {
      $discount_total += -$unit_price['amount'];
    }
    elseif ($unit_price['amount'] >= 0) {
      $item_total += $line_item_wrapper->commerce_total->amount
        ->value();
      $item = array(
        'name' => mb_substr(strip_tags(commerce_line_item_title($line_item_wrapper
          ->value())), 0, 127),
        'unit_amount' => array(
          'currency_code' => $currency_code,
          'value' => commerce_paypal_checkout_price_amount($unit_price['amount'], $unit_price['currency_code']),
        ),
        'quantity' => intval($line_item_wrapper->quantity
          ->value()),
      );

      // Pass the "SKU" for product line items.
      if (in_array($line_item_wrapper
        ->getBundle(), $product_line_item_types)) {
        $item['sku'] = $line_item_wrapper->commerce_product->sku
          ->value();
      }
      $items[] = $item;
    }
  }

  // @todo: Support passing discount in the breakdown when
  // https://github.com/paypal/paypal-checkout-components/issues/1016 is fixed.
  $breakdown = array(
    'item_total' => array(
      'currency_code' => $currency_code,
      'value' => commerce_paypal_checkout_price_amount($item_total, $currency_code),
    ),
  );
  if ($shipping_total) {
    $breakdown['shipping'] = array(
      'currency_code' => $currency_code,
      'value' => commerce_paypal_checkout_price_amount($shipping_total, $currency_code),
    );
  }
  if (module_exists('commerce_tax')) {
    $tax_total = commerce_round(COMMERCE_ROUND_HALF_UP, commerce_tax_total_amount($order_total['data']['components'], FALSE, $currency_code));
    if ($tax_total) {
      $breakdown['tax_total'] = array(
        'currency_code' => $currency_code,
        'value' => commerce_paypal_checkout_price_amount($tax_total, $currency_code),
      );
    }
  }
  if ($discount_total) {
    $breakdown['discount'] = array(
      'currency_code' => $currency_code,
      'value' => commerce_paypal_checkout_price_amount($discount_total, $currency_code),
    );
  }
  $payer = array();
  if (!empty($order->mail)) {
    $payer['email_address'] = $order->mail;
  }

  // If we have a billing address, pass it to PayPal.
  if (isset($order_wrapper->commerce_customer_billing) && !empty($order_wrapper->commerce_customer_billing->commerce_customer_address)) {
    $address = $order_wrapper->commerce_customer_billing->commerce_customer_address
      ->value();
    $payer += commerce_paypal_checkout_format_address($address, 'billing');
  }
  $request_body = array(
    'intent' => strtoupper($settings['intent']),
    'purchase_units' => array(
      array(
        'reference_id' => 'default',
        'custom_id' => $order->order_id,
        'invoice_id' => $order->order_id . '-' . time(),
        'amount' => array(
          'currency_code' => $currency_code,
          'value' => commerce_paypal_checkout_price_amount($order_total['amount'], $order_total['currency_code']),
          'breakdown' => $breakdown,
        ),
        'items' => $items,
      ),
    ),
    'application_context' => array(
      'brand_name' => mb_substr(variable_get('site_name', ''), 0, 127),
    ),
  );

  // Send the payer if not empty.
  if ($payer) {
    $request_body['payer'] = $payer;
  }
  $shipping_exists = module_exists('commerce_shipping');
  $shipping_address = FALSE;

  // If the shipping module is enabled...
  if ($shipping_exists) {

    // If we have a shipping address, pass it to PayPal.
    if (isset($order_wrapper->commerce_customer_shipping) && !empty($order_wrapper->commerce_customer_shipping->commerce_customer_address)) {
      $address = $order_wrapper->commerce_customer_shipping->commerce_customer_address
        ->value();
      $shipping_address = commerce_paypal_checkout_format_address($address, 'shipping');
    }
  }
  $shipping_preference = $settings['shipping_preference'];

  // The shipping module isn't enabled, override the shipping preference
  // configured.
  if (!$shipping_exists) {
    $shipping_preference = 'no_shipping';
  }
  else {

    // If no shipping address was already collected, override the shipping
    // preference to "GET_FROM_FILE" so that the shipping address is collected
    // on the PayPal site.
    if ($shipping_preference == 'set_provided_address' && !$shipping_address) {
      $shipping_preference = 'get_from_file';
    }
  }

  // No need to pass a shipping_address if the shipping address collection
  // is configured to "no_shipping".
  if ($shipping_address && $shipping_preference !== 'no_shipping') {
    $request_body['purchase_units'][0]['shipping'] = $shipping_address;
  }
  $request_body['application_context']['shipping_preference'] = strtoupper($shipping_preference);
  return $request_body;
}

/**
 * Formats the given address into a format expected by PayPal.
 *
 * @param array $address
 *   The address to format.
 * @param $profile_type
 *   The profile type ("billing"|"shipping").
 *
 * @return array
 *   The formatted address.
 */
function commerce_paypal_checkout_format_address($address, $profile_type) {
  $return = array();
  if (!empty($address['postal_code'])) {
    $return = array(
      'address' => array_filter(array(
        'address_line_1' => $address['thoroughfare'],
        'address_line_2' => $address['premise'],
        'admin_area_2' => mb_substr($address['locality'], 0, 120),
        'admin_area_1' => $address['administrative_area'],
        'postal_code' => mb_substr($address['postal_code'], 0, 60),
        'country_code' => $address['country'],
      )),
    );
  }
  if ($profile_type == 'billing') {
    $return['name'] = array(
      'given_name' => $address['first_name'],
      'surname' => $address['last_name'],
    );
  }
  elseif ($profile_type == 'shipping') {
    $return['name'] = array(
      'full_name' => $address['name_line'],
    );
  }
  return $return;
}

/**
 * Implements hook_theme().
 */
function commerce_paypal_checkout_theme($existing, $type, $theme, $path) {
  return array(
    'commerce_paypal_checkout_smart_payment_buttons' => array(
      'variables' => array(
        'payment_method' => array(),
        'commit' => FALSE,
        'order' => NULL,
        'flow' => NULL,
      ),
    ),
  );
}

/**
 * Returns HTML for the Smart payment buttons.
 *
 * @param $variables
 *   An associative array containing:
 *   - payment_method: The payment method instance.
 *   - commit: A boolean indicating whether to commit the transaction.
 *   - order: The order.
 *   - flow: The flow ("shortcut"|"mark").
 *
 * @ingroup themeable
 */
function theme_commerce_paypal_checkout_smart_payment_buttons($variables) {
  $payment_method = $variables['payment_method'];
  $settings = $payment_method['settings'] + commerce_paypal_checkout_default_settings();
  if (empty($settings['client_id']) || empty($variables['order']) || empty($variables['flow'])) {
    return;
  }
  $order = $variables['order'];
  $order_total = field_get_items('commerce_order', $order, 'commerce_order_total', LANGUAGE_NONE);
  if (!isset($order_total[0]['currency_code'])) {
    return;
  }
  $flow = $variables['flow'];
  list(, $rule_name) = explode('|', $payment_method['instance_id']);
  $options = array(
    'external' => TRUE,
    'query' => array(
      'client-id' => $settings['client_id'],
      'commit' => $variables['commit'] ? 'true' : 'false',
      'intent' => $settings['intent'],
      'currency' => $order_total[0]['currency_code'],
    ),
  );
  if (!empty($settings['disable_funding'])) {
    $options['query']['disable-funding'] = implode(',', $settings['disable_funding']);
  }
  if (!empty($settings['disable_card'])) {
    $options['query']['disable-card'] = implode(',', $settings['disable_card']);
  }
  $path = drupal_get_path('module', 'commerce_paypal_checkout');
  $js_settings = array(
    'paypalCheckout' => array(
      'src' => url('https://www.paypal.com/sdk/js', $options),
      'createOrderUri' => url("commerce-paypal-checkout/create-order/{$order->order_id}/{$rule_name}"),
      'onApproveUri' => url("commerce-paypal-checkout/approve-order/{$order->order_id}/{$rule_name}/{$flow}"),
      'style' => $settings['style'],
    ),
  );
  drupal_add_css($path . '/css/commerce_paypal_checkout.css');
  drupal_add_js($js_settings, 'setting');
  drupal_add_js($path . '/js/commerce_paypal_checkout.js');
  $id = drupal_html_id('paypal-buttons-container');
  return '<div id="' . $id . '" class="paypal-buttons-container"></div>';
}

/**
 * Returns the first configured PayPal checkout payment method instance for the
 * given order, if any.
 *
 * @param $order
 *   The order that needs to be checked.
 *
 * @return array|bool
 *   The first payment method instance found for given order, FALSE otherwise.
 */
function commerce_paypal_checkout_get_payment_method_instance($order) {
  if (empty($order->payment_methods)) {
    $order->payment_methods = array();
    rules_invoke_all('commerce_payment_methods', $order);

    // Sort the payment methods array by the enabling Rules' weight values.
    uasort($order->payment_methods, 'drupal_sort_weight');
  }
  foreach (array_keys($order->payment_methods) as $instance_id) {

    // Explode the method key into its component parts.
    list($method_id) = explode('|', $instance_id);
    if ($method_id != 'paypal_checkout') {
      continue;
    }
    return commerce_payment_method_instance_load($instance_id);
  }
  return FALSE;
}

/**
 * Implements hook_form_alter().
 */
function commerce_paypal_checkout_form_alter(&$form, &$form_state, $form_id) {
  if (!is_string($form_id)) {
    return;
  }

  // If we're altering a shopping cart form.
  if (strpos($form_id, 'views_form_commerce_cart_form_') === 0) {

    // If the cart form View shows line items...
    if (!empty($form_state['build_info']['args'][0]->result)) {
      $order = $form_state['order'];
      $payment_method = commerce_paypal_checkout_get_payment_method_instance($order);

      // If no PayPal checkout payment method is configured, or if the buttons
      // are explicitly not shown on the cart page, stop here.
      if (!$payment_method || empty($payment_method['settings']['enable_on_cart'])) {
        return;
      }
      $form['smart_payment_buttons'] = array(
        '#theme' => 'commerce_paypal_checkout_smart_payment_buttons',
        '#payment_method' => $payment_method,
        '#commit' => FALSE,
        '#flow' => 'shortcut',
        '#order' => $order,
        '#weight' => 100,
      );
    }
  }
}

/**
 * Returns an instantiated PayPal Checkout API client for the given settings.
 *
 * @param array $config
 *   An associative array containing at least the following keys:
 *   - client_id: The client ID.
 *   - secret: The client secret.
 *   - server: The API server ("sandbox or "live").
 *
 * @return PayPalCheckoutClient|NULL.
 *   An instantiated PayPalCheckout client, NULL if no client_id/secret were
 *   specified.
 */
function commerce_paypal_checkout_api_client($config) {
  if (!isset($config['client_id']) || !isset($config['secret'])) {
    return NULL;
  }
  $instances =& drupal_static(__FUNCTION__, array());
  if (isset($instances[$config['client_id']])) {
    return $instances[$config['client_id']];
  }
  $instances[$config['client_id']] = new PayPalCheckoutClient($config);
  return $instances[$config['client_id']];
}

/**
 * Payment method callback: redirect form.
 */
function commerce_paypal_checkout_redirect_form($form, &$form_state, $order, $payment_method) {
  $form['smart_payment_buttons'] = array(
    '#theme' => 'commerce_paypal_checkout_smart_payment_buttons',
    '#payment_method' => $payment_method,
    '#commit' => TRUE,
    '#order' => $order,
    '#flow' => 'mark',
  );
  return $form;
}

/**
 * Payment method callback: redirect form return validation.
 */
function commerce_paypal_checkout_redirect_form_validate($order, $payment_method) {
  $payment_method['settings'] += commerce_paypal_checkout_default_settings();

  // Check if the PayPal order ID is known, as well as the "flow".
  if (empty($order->data['commerce_paypal_checkout']['remote_id']) || !isset($order->data['commerce_paypal_checkout']['flow'])) {
    return FALSE;
  }
  $flow = $order->data['commerce_paypal_checkout']['flow'];
  $api_client = commerce_paypal_checkout_api_client($payment_method['settings']);
  if (!$api_client) {
    return FALSE;
  }
  $remote_id = $order->data['commerce_paypal_checkout']['remote_id'];
  try {
    $paypal_order = $api_client
      ->getOrder($remote_id);
  } catch (\Exception $exception) {
    watchdog_exception('commerce_paypal_checkout', $exception);
    return FALSE;
  }
  $order_total = field_get_items('commerce_order', $order, 'commerce_order_total', LANGUAGE_NONE);
  $paypal_amount = $paypal_order['purchase_units'][0]['amount'];
  $paypal_total = commerce_currency_decimal_to_amount($paypal_amount['value'], $paypal_amount['currency_code']);

  // Check the remote status, and that the PayPal total matches the order total.
  if (!in_array($paypal_order['status'], [
    'APPROVED',
    'SAVED',
  ]) || $paypal_total != $order_total[0]['amount'] || $paypal_amount['currency_code'] != $order_total[0]['currency_code']) {
    return FALSE;
  }

  // Store the intent for later reuse, it can't be updated, so no risk in
  // being out of sync.
  $order->data['commerce_paypal_checkout']['intent'] = strtolower($paypal_order['intent']);
  $payer = $paypal_order['payer'];

  // If the user is anonymous, add their PayPal e-mail to the order.
  if (empty($order->mail)) {
    $order->mail = $payer['email_address'];
  }

  // Create a billing information profile for the order with the available info.
  if (!empty($payment_method['settings']['update_billing_profiles'])) {
    commerce_paypal_checkout_customer_profile($order, 'billing', $paypal_order);
  }

  // If the shipping module exists on the site, create a shipping information
  // profile for the order with the available info.
  if (module_exists('commerce_shipping') && !empty($payment_method['settings']['update_shipping_profiles'])) {
    commerce_paypal_checkout_customer_profile($order, 'shipping', $paypal_order);
  }

  // Recalculate the price of products on the order in case taxes have
  // changed or prices have otherwise been affected.
  if ($flow == 'shortcut') {
    commerce_cart_order_refresh($order);
  }

  // Save the changes to the order.
  commerce_order_save($order);
  if ($flow == 'mark') {
    return commerce_paypal_checkout_do_payment($order, $payment_method);
  }
}

/**
 * Capture/authorize a PayPal order and create a payment transaction.
 *
 * @param $order
 *   The order the payment is for.
 * @param $payment_method
 *   The PayPal Checkout payment method instance whose settings should
 *   be used to submit the request.
 *
 * @return
 *   Boolean indicating the success or failure of the payment request.
 */
function commerce_paypal_checkout_do_payment($order, $payment_method) {
  if (empty($order->data['commerce_paypal_checkout']['remote_id'])) {
    return FALSE;
  }
  $paypal_checkout_data = $order->data['commerce_paypal_checkout'];
  $intent = isset($paypal_checkout_data['intent']) ? $paypal_checkout_data['intent'] : $payment_method['settings']['intent'];
  $api_client = commerce_paypal_checkout_api_client($payment_method['settings']);
  try {
    if ($intent == 'capture') {
      $response = $api_client
        ->captureOrder($paypal_checkout_data['remote_id']);
      $remote_payment = $response['purchase_units'][0]['payments']['captures'][0];
    }
    else {
      $response = $api_client
        ->authorizeOrder($paypal_checkout_data['remote_id']);
      $remote_payment = $response['purchase_units'][0]['payments']['authorizations'][0];
    }
  } catch (\Exception $exception) {
    watchdog_exception('commerce_paypal_checkout', $exception);

    // Display an error message and remain on the same page.
    drupal_set_message(t('We could not complete your payment with PayPal. Please try again or contact us if the problem persists.'), 'error');
    watchdog('commerce_paypal_checkout', 'PayPal Checkout transaction failed for order @order_number.', array(
      '@order_number' => $order->order_number,
    ), WATCHDOG_ERROR);
    return FALSE;
  }
  $remote_status = strtolower($remote_payment['status']);

  // Prepare a transaction object to log the API response.
  $transaction = commerce_payment_transaction_new('paypal_checkout', $order->order_id);
  $transaction->instance_id = $payment_method['instance_id'];
  $transaction->amount = commerce_currency_decimal_to_amount($remote_payment['amount']['value'], $remote_payment['amount']['currency_code']);
  $transaction->currency_code = $remote_payment['amount']['currency_code'];
  $transaction->payload[REQUEST_TIME] = $response;
  $transaction->remote_id = $remote_payment['id'];
  $transaction->remote_status = $remote_payment['status'];

  // Store the transaction ID as the parent transaction ID in case subsequent
  // API operations alter this transaction's remote ID.
  if (!empty($transaction->remote_id)) {
    $transaction->data['commerce_paypal_checkout']['original_remote_id'] = $transaction->remote_id;
  }
  if (in_array($remote_status, [
    'denied',
    'expired',
    'declined',
  ])) {
    $transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE;
    commerce_payment_transaction_save($transaction);

    // Display an error message and remain on the same page.
    drupal_set_message(t('We could not complete your payment with PayPal. Please try again or contact us if the problem persists.'), 'error');
    watchdog('commerce_paypal_checkout', 'PayPal Checkout transaction failed for order @order_number.', array(
      '@order_number' => $order->order_number,
    ), WATCHDOG_ERROR);
    return FALSE;
  }

  // Map the remote status to a Drupal commerce payment status.
  $status_mapping = array(
    'created' => COMMERCE_PAYMENT_STATUS_PENDING,
    'pending' => COMMERCE_PAYMENT_STATUS_PENDING,
    'completed' => COMMERCE_PAYMENT_STATUS_SUCCESS,
  );

  // If we do not know how to handle this remote payment status, set the payment
  // status to failure and stop here.
  if (!isset($status_mapping[$remote_status])) {
    $transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE;
    commerce_payment_transaction_save($transaction);

    // Display an error message and remain on the same page.
    drupal_set_message(t('We could not complete your payment with PayPal. Please try again or contact us if the problem persists.'), 'error');
    watchdog('commerce_paypal_checkout', 'PayPal Checkout transaction failed for order @order_number.', array(
      '@order_number' => $order->order_number,
    ), WATCHDOG_ERROR);
    return FALSE;
  }
  $transaction->status = $status_mapping[$remote_status];
  commerce_payment_transaction_save($transaction);
  return TRUE;
}

/**
 * Creates or updates a customer profile for an order based on information
 * obtained from PayPal after a payment via PayPal Checkout.
 *
 * @param $order
 *   The order that was paid via PayPal Checkout.
 * @param $profile_type
 *   The type of the customer profile that should be created or updated.
 * @param array $paypal_order
 *   The PayPal order retrieved via the getOrder() API call.
 * @param $skip_save
 *   Boolean indicating whether or not this function should skip saving the
 *   order after setting it to reference the newly created customer profile;
 *   defaults to TRUE, requiring the caller to save the order.
 */
function commerce_paypal_checkout_customer_profile($order, $profile_type, $paypal_order, $skip_save = TRUE) {

  // First check if the order already references a customer profile of the
  // specified type.
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $field_name = variable_get('commerce_customer_profile_' . $profile_type . '_field', '');

  // If the associated order field has been set and the order currently
  // references a customer profile through it...
  if (!empty($field_name) && !empty($order_wrapper->{$field_name})) {

    // Update the existing customer profile.
    $profile = $order_wrapper->{$field_name}
      ->value();
  }
  elseif (!empty($order->data['profiles']['customer_profile_' . $profile_type])) {

    // Otherwise look for an association stored in the order's data array.
    $profile = commerce_customer_profile_load($order->data['profiles']['customer_profile_' . $profile_type]);
  }

  // Create a new profile if we could not find an existing one.
  if (empty($profile)) {
    $profile = commerce_customer_profile_new($profile_type, $order->uid);
  }

  // Add the order context to the profile to ensure it can be updated without
  // resulting in customer profile duplication.
  $profile->entity_context = array(
    'entity_type' => 'commerce_order',
    'entity_id' => $order->order_id,
  );

  // Prepare an addressfield array to set to the customer profile.
  $field = field_info_field('commerce_customer_address');
  $instance = field_info_instance('commerce_customer_profile', 'commerce_customer_address', $profile_type);
  $address = addressfield_default_values($field, $instance);
  $paypal_address = array();

  // Use the first name and last name if the profile is a billing profile.
  if ($profile_type == 'billing') {
    $address['first_name'] = $paypal_order['payer']['name']['given_name'];
    $address['last_name'] = $paypal_order['payer']['name']['surname'];
    $address['name_line'] = $address['first_name'] . ' ' . $address['last_name'];
    if (isset($paypal_order['payer']['address'])) {
      $paypal_address = $paypal_order['payer']['address'];
    }
  }
  elseif ($profile_type == 'shipping') {

    // If no shipping info was returned by PayPal.
    if (empty($paypal_order['purchase_units'][0]['shipping'])) {
      return;
    }
    $shipping_info = $paypal_order['purchase_units'][0]['shipping'];
    $names = explode(' ', $shipping_info['name']['full_name']);
    $first_name = array_shift($names);
    $last_name = implode(' ', $names);
    $address['first_name'] = $first_name;
    $address['last_name'] = $last_name;
    $address['name_line'] = $first_name . ' ' . $last_name;
    if (isset($shipping_info['address'])) {
      $paypal_address = $shipping_info['address'];
    }
  }
  if ($paypal_address) {

    // Map PayPal address keys to Address field keys.
    $mapping = array(
      'address_line_1' => 'thoroughfare',
      'address_line_2' => 'premise',
      'admin_area_1' => 'administrative_area',
      'admin_area_2' => 'locality',
      'postal_code' => 'postal_code',
      'country_code' => 'country',
    );
    foreach ($paypal_address as $key => $value) {
      if (!isset($mapping[$key])) {
        continue;
      }

      // PayPal address fields have a higher maximum length than ours.
      $value = $key == 'country_code' ? $value : mb_substr($value, 0, 255);
      $address[$mapping[$key]] = $value;
    }
  }

  // Add the addressfield value to the customer profile.
  $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
  $profile_wrapper->commerce_customer_address = $address;

  // Save the customer profile and update the order to reference it.
  $profile_wrapper
    ->save();
  $order_wrapper->{'commerce_customer_' . $profile_type} = $profile_wrapper;

  // Save the order if specified.
  if (!$skip_save) {
    $order_wrapper
      ->save();
  }
}

/**
 * Update the PayPal order.
 *
 * @param $order
 *   The order.
 * @param $payment_method
 *   The payment method instance.
 *
 * @return bool
 *   TRUE if the update was successful, FALSE otherwise.
 *
 * @see commerce_paypal_checkout_review_pane_checkout_form_submit()
 */
function commerce_paypal_checkout_update_order($order, $payment_method) {
  if (!isset($order->data['commerce_paypal_checkout']['remote_id'])) {
    return FALSE;
  }
  $remote_id = $order->data['commerce_paypal_checkout']['remote_id'];
  $request_body = commerce_paypal_checkout_prepare_order_request($order, $payment_method['settings']);
  $update_params = array(
    array(
      'op' => 'replace',
      'path' => "/purchase_units/@reference_id=='default'",
      'value' => $request_body['purchase_units'][0],
    ),
  );
  drupal_alter('commerce_paypal_checkout_update_order_request', $update_params, $order);
  $api_client = commerce_paypal_checkout_api_client($payment_method['settings']);
  try {
    $api_client
      ->updateOrder($remote_id, $update_params);

    // Assume the update worked if we ended up here, if the update failed,
    // an exception was thrown.
    return TRUE;
  } catch (\Exception $exception) {
    watchdog_exception('commerce_paypal_checkout', $exception);
    return FALSE;
  }
}

/**
 * Formats a price amount into a decimal value as expected by PayPal.
 *
 * @param $amount
 *   An integer price amount.
 * @param $currency_code
 *   The currency code of the price.
 *
 * @return
 *   The decimal price amount as expected by PayPal API servers.
 */
function commerce_paypal_checkout_price_amount($amount, $currency_code) {
  $rounded_amount = commerce_currency_round($amount, commerce_currency_load($currency_code));
  return number_format(commerce_currency_amount_to_decimal($rounded_amount, $currency_code), 2, '.', '');
}

Functions

Namesort descending Description
commerce_paypal_checkout_api_client Returns an instantiated PayPal Checkout API client for the given settings.
commerce_paypal_checkout_approve_order Page callback: Provide the onApprove() callback expected by the SDK.
commerce_paypal_checkout_capture_void_access Determines access to the prior authorization capture form or void form for Paypal Checkout transactions.
commerce_paypal_checkout_commerce_checkout_page_info Implements hook_commerce_checkout_page_info().
commerce_paypal_checkout_commerce_checkout_pane_info Implements hook_commerce_checkout_pane_info().
commerce_paypal_checkout_commerce_checkout_router Implements hook_commerce_checkout_router().
commerce_paypal_checkout_commerce_payment_method_info Implements hook_commerce_payment_method_info().
commerce_paypal_checkout_create_order Page callback: Provide the createOrder() callback expected by the SDK.
commerce_paypal_checkout_customer_profile Creates or updates a customer profile for an order based on information obtained from PayPal after a payment via PayPal Checkout.
commerce_paypal_checkout_customize_buttons_validate Element validate callback for the customize_buttons payment method setting.
commerce_paypal_checkout_default_settings Returns the default settings for the PayPal Checkout payment method.
commerce_paypal_checkout_disable_card_validate Element validate callback for the disable_card payment method setting.
commerce_paypal_checkout_disable_funding_validate Element validate callback for the disable_funding payment method setting.
commerce_paypal_checkout_do_payment Capture/authorize a PayPal order and create a payment transaction.
commerce_paypal_checkout_embedded_checkout_panes Returns the checkout pane IDs of checkout panes that should be embedded in the PayPal Checkout review and confirm page.
commerce_paypal_checkout_format_address Formats the given address into a format expected by PayPal.
commerce_paypal_checkout_form_alter Implements hook_form_alter().
commerce_paypal_checkout_get_payment_method_instance Returns the first configured PayPal checkout payment method instance for the given order, if any.
commerce_paypal_checkout_menu Implements hook_menu().
commerce_paypal_checkout_method_instance_load Load the given PayPal checkout payment method instance.
commerce_paypal_checkout_prepare_order_request Prepare the request parameters for the create/update order request.
commerce_paypal_checkout_price_amount Formats a price amount into a decimal value as expected by PayPal.
commerce_paypal_checkout_redirect_form Payment method callback: redirect form.
commerce_paypal_checkout_redirect_form_validate Payment method callback: redirect form return validation.
commerce_paypal_checkout_refund_access Determines access to the refund form for Paypal Checkout transactions.
commerce_paypal_checkout_settings_form Payment method callback: settings form.
commerce_paypal_checkout_theme Implements hook_theme().
commerce_paypal_checkout_update_order Update the PayPal order.
theme_commerce_paypal_checkout_smart_payment_buttons Returns HTML for the Smart payment buttons.