diff --git a/commerce_paypal.module b/commerce_paypal.module
index 8767833..30efea0 100644
--- a/commerce_paypal.module
+++ b/commerce_paypal.module
@@ -471,6 +471,7 @@ function commerce_paypal_currencies($method_id) {
case 'paypal_ppa':
return drupal_map_assoc(array('AUD', 'CAD', 'EUR', 'GBP', 'JPY', 'USD'));
case 'paypal_wps':
+ case 'paypal_apc':
return drupal_map_assoc(array('AUD', 'BRL', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'ILS', 'JPY', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'SEK', 'SGD', 'THB', 'TRY', 'TWD', 'USD'));
}
}
diff --git a/modules/adaptive/README.md b/modules/adaptive/README.md
new file mode 100644
index 0000000..7ee1d2d
--- /dev/null
+++ b/modules/adaptive/README.md
@@ -0,0 +1,27 @@
+PayPal Adaptive Payments
+========================
+
+This module implements the [PayPal Adaptive Payments API][api] which allows for
+marketplace-style split transactions of up to 9 receivers. Transactions can be
+split or parallel and have different options for distributing fees and handling
+payments.
+
+On its own, this module will effectively act like a standard PayPal module and
+will not specify or create multiple transactions per order. In order to add
+extra receivers to an order is to use the alter hook defined in the API. You
+can find more information about that in the `commerce_paypal_adaptive.api.php`
+file in this directory.
+
+
+Setting Up
+----------
+
+You will need to [create Signature API credentials][credentials] in order to
+use this module. Enter these API credentials into the payment configuration
+form and save it. You will also need to at least one PayPal address that can
+receive payments.
+
+
+
+ [api]: https://developer.paypal.com/docs/classic/products/adaptive-payments/
+ [credentials]: https://developer.paypal.com/docs/classic/api/apiCredentials/#creating-an-api-signature
diff --git a/modules/adaptive/commerce_paypal_adaptive.api.php b/modules/adaptive/commerce_paypal_adaptive.api.php
new file mode 100644
index 0000000..0a2af76
--- /dev/null
+++ b/modules/adaptive/commerce_paypal_adaptive.api.php
@@ -0,0 +1,62 @@
+ 'merchant@example.com',
+ 'amount' => '5',
+ );
+}
diff --git a/modules/adaptive/commerce_paypal_adaptive.info b/modules/adaptive/commerce_paypal_adaptive.info
new file mode 100644
index 0000000..a61b28d
--- /dev/null
+++ b/modules/adaptive/commerce_paypal_adaptive.info
@@ -0,0 +1,9 @@
+name = PayPal Adaptive Payments
+description = Implements PayPal Adaptive Payments for use with Commerce Marketplace.
+package = Commerce (PayPal)
+dependencies[] = commerce
+dependencies[] = commerce_ui
+dependencies[] = commerce_payment
+dependencies[] = commerce_order
+dependencies[] = commerce_paypal
+core = 7.x
diff --git a/modules/adaptive/commerce_paypal_adaptive.install b/modules/adaptive/commerce_paypal_adaptive.install
new file mode 100644
index 0000000..23917bf
--- /dev/null
+++ b/modules/adaptive/commerce_paypal_adaptive.install
@@ -0,0 +1,6 @@
+ 'commerce_paypal_adaptive_chained',
+ 'title' => t('PayPal Adaptive (Chained)'),
+ 'short_title' => t('PayPal'),
+ 'description' => t('PayPal Adaptive Payments (Chained)'),
+ 'terminal' => FALSE,
+ 'offsite' => TRUE,
+ 'offsite_autoredirect' => TRUE,
+
+ // Because the order form generation code does not have access to a payment
+ // method info array, we set the bn directly there instead of making use of
+ // this buttonsource variable. It's here for consistency with other payment
+ // methods in this package.
+ 'buttonsource' => 'CommerceGuys_Cart_APC',
+ );
+
+ return $payment_methods;
+}
+
+/**
+ * Returns the default settings for the PayPal WPS payment method.
+ */
+function commerce_paypal_adaptive_default_settings() {
+ $default_currency = commerce_default_currency();
+
+ return array(
+ 'api_username' => '',
+ 'api_password' => '',
+ 'api_signature' => '',
+ 'currency_code' => in_array($default_currency, array_keys(commerce_paypal_currencies('paypal_wps'))) ? $default_currency : 'USD',
+ 'allow_supported_currencies' => FALSE,
+ 'language' => 'US',
+ 'server' => 'sandbox',
+ 'action_type' => 'PAY',
+ 'fees_payer' => 'SECONDARYONLY',
+ 'ipn_logging' => 'notification',
+ 'primary_receiver' => '',
+ 'ipn_create_billing_profile' => FALSE,
+ 'show_payment_instructions' => FALSE,
+ );
+}
+
+/**
+ * Payment method callback: settings form.
+ */
+function commerce_paypal_adaptive_chained_settings_form($settings = array()) {
+ $form = array();
+
+ // Merge default settings into the stored settings array.
+ $settings = (array) $settings + commerce_paypal_adaptive_default_settings();
+
+ $form['api_username'] = array(
+ '#type' => 'textfield',
+ '#title' => t('API username'),
+ '#default_value' => $settings['api_username'],
+ );
+ $form['api_password'] = array(
+ '#type' => 'textfield',
+ '#title' => t('API password'),
+ '#default_value' => $settings['api_password'],
+ );
+ $form['api_signature'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Signature'),
+ '#default_value' => $settings['api_signature'],
+ );
+ $form['currency_code'] = array(
+ '#type' => 'select',
+ '#title' => t('Default currency'),
+ '#description' => t('Transactions in other currencies will be converted to this currency, so multi-currency sites must be configured to use appropriate conversion rates.'),
+ '#options' => commerce_paypal_currencies('paypal_apc'),
+ '#default_value' => $settings['currency_code'],
+ );
+ $form['allow_supported_currencies'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Allow transactions to use any currency in the options list above.'),
+ '#description' => t('Transactions in unsupported currencies will still be converted into the default currency.'),
+ '#default_value' => $settings['allow_supported_currencies'],
+ );
+ $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['action_type'] = array(
+ '#type' => 'radios',
+ '#title' => t('Payment action'),
+ '#options' => array(
+ 'PAY' => t('Pay - capture and pay all receivers of a multi-receiver payment'),
+ 'PAY_PRIMARY' => t('Pay Primary - pays the primary receiver of a chained payment'),
+ ),
+ '#default_value' => $settings['action_type'],
+ );
+ $form['fees_payer'] = array(
+ '#type' => 'radios',
+ '#title' => t('Who pays the fees?'),
+ '#options' => array(
+ 'SENDER' => t('Sender pays the fees.'),
+ 'PRIMARYRECEIVER' => t('Primary receiver pays all fees.'),
+ 'EACHRECEIVER' => t('Each receiver pays their own fee.'),
+ 'SECONDARYONLY' => t('Secondary receivers pay all fees.'),
+ ),
+ '#default_value' => $settings['fees_payer'],
+ );
+ $form['ipn_logging'] = array(
+ '#type' => 'radios',
+ '#title' => t('IPN logging'),
+ '#options' => array(
+ 'notification' => t('Log notifications during IPN validation and processing.'),
+ 'full_ipn' => t('Log notifications with the full IPN during validation and processing (used for debugging).'),
+ ),
+ '#default_value' => $settings['ipn_logging'],
+ );
+ $form['primary_receiver'] = array(
+ '#type' => 'textfield',
+ '#title' => t('PayPal primary receiver e-mail addresses'),
+ '#description' => t('Enter the primary e-mail address for your PayPal account if different from the one entered above or a comma separated list of all valid e-mail addresses on the account.') . '
' . t('IPNs that originate from payments made to a PayPal account whose e-mail address is not in this list will not be processed.'),
+ '#default_value' => $settings['primary_receiver'],
+ );
+ $form['ipn_create_billing_profile'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Create a billing profile based on name and country data in the IPN for any order that does not have one yet.'),
+ '#description' => t('This is most useful for sites that do not collect billing information locally but still want to have customer names on orders.'),
+ '#default_value' => $settings['ipn_create_billing_profile'],
+ );
+ $form['show_payment_instructions'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show a message on the checkout form when PayPal Adaptive Payments is selected telling the customer to "Continue with checkout to complete payment via PayPal."'),
+ '#default_value' => $settings['show_payment_instructions'],
+ );
+
+ return $form;
+}
+
+/**
+ * Payment method callback.
+ *
+ * Adds a message to the submission form if enabled in the payment method
+ * settings.
+ */
+function commerce_paypal_adaptive_chained_submit_form($payment_method, $pane_values, $checkout_pane, $order) {
+ $form = array();
+
+ if (!empty($payment_method['settings']['show_payment_instructions'])) {
+ $form['paypal_apc_information'] = array(
+ '#markup' => '' . t('(Continue with checkout to complete payment via PayPal.)') . '',
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_paypal_adaptive_form_commerce_checkout_form_alter(&$form, &$form_state) {
+ // If this checkout form contains the payment method radios...
+ if (!empty($form['commerce_payment']['payment_method']['#options'])) {
+ // Loop over its options array looking for a PayPal WPS option.
+ foreach ($form['commerce_payment']['payment_method']['#options'] as $key => &$value) {
+ list($method_id, $rule_name) = explode('|', $key);
+
+ // If we find PayPal WPS...
+ if ($method_id == 'paypal_apc') {
+ // Prepare the replacement radio button text with icons.
+ $icons = commerce_paypal_icons();
+ $value = t('!logo PayPal - pay securely without sharing your financial information', array('!logo' => $icons['paypal']));
+ $value .= '
' . check_plain(print_r($_POST, TRUE)) . ''), WATCHDOG_NOTICE); + } + + // This may be an unnecessary step, but if for some reason the user does end + // up returning at the success URL with a Failed payment, go back. + if (!empty($_POST['payment_status']) && $_POST['payment_status'] == 'Failed') { + return FALSE; + } +} + +/** + * Payment method callback: redirect form return submission. + */ +function commerce_paypal_adaptive_chained_redirect_form_submit($order, $payment_method) { + // Nothing here yet. +} + +/** + * Implements hook_commerce_paypal_ipn_process(). + */ +function commerce_paypal_adaptive_commerce_paypal_ipn_process($order, $payment_method, $ipn) { + // @todo Implement. +} + +/** + * Builds a Website Payments Standard form from an order object. + * + * @param object $order + * The fully loaded order being paid for. + * @param array $settings + * An array of settings used to build out the form, including: + * - server: which server to use, either sandbox or live + * - business: the PayPal e-mail address the payment submits to + * - cancel_return: the URL PayPal should send the user to on cancellation + * - return: the URL PayPal should send the user to on successful payment + * - currency_code: the PayPal currency code to use for this payment if the + * total for the order is in a non-PayPal supported currency + * - language: the PayPal language code to use on the payment form + * - action_type: the PayPal payment action to use: sale, authorization, + * or order + * - payment_method: optionally a payment method instance ID to include in the + * IPN notify_url. + * + * @return array + * A renderable form array. + */ +function commerce_paypal_adaptive_chained_order_form($form, &$form_state, $order, $settings) { + $wrapper = entity_metadata_wrapper('commerce_order', $order); + + // Determine the currency code to use to actually process the transaction, + // which will either be the default currency code or the currency code of the + // order if it's supported by PayPal if that option is enabled. + $currency_code = $settings['currency_code']; + $order_currency_code = $wrapper->commerce_order_total->currency_code->value(); + + if (!empty($settings['allow_supported_currencies']) && in_array($order_currency_code, array_keys(commerce_paypal_currencies('paypal_wps')))) { + $currency_code = $order_currency_code; + } + + $amount = $wrapper->commerce_order_total->amount->value(); + + // Ensure a default value for the payment_method setting. + $settings += array('payment_method' => ''); + + // Build the data array that will be translated into hidden form values. + $data = array( + // Specify the checkout experience to present to the user. + 'actionType' => $settings['action_type'], + // The path PayPal should send the IPN to. + 'ipnNotificationUrl' => commerce_paypal_ipn_url($settings['payment_method']), + // Return to the review page when payment is canceled. + 'cancelUrl' => $settings['cancel_return'], + // Return to the payment redirect page for processing successful payments. + 'returnUrl' => $settings['return'], + // Set the currency and language codes. + 'currencyCode' => $currency_code, + + // Primary Receiver information. + 'receiverList' => array( + 'receiver' => array( + array( + 'email' => $settings['primary_receiver'], + 'amount' => commerce_paypal_price_amount(commerce_currency_convert($amount, $order_currency_code, $currency_code), $currency_code), + 'primary' => 'true', + ), + ), + ), + + // Use the timestamp to generate a unique invoice number. + 'invoice' => commerce_paypal_ipn_invoice($order), + // Required. + 'requestEnvelope' => array( + 'errorLanguage' => 'en_US', + ), + // Shown on the checkout page. + 'memo' => t('Order #@order', array('@order' => $order->order_id)), + // Who is paying the fees? + 'feesPayer' => $settings['fees_payer'], + ); + + // Allow modules to alter parameters of the API request. + drupal_alter('commerce_paypal_apc_order_form_data', $data, $order, $settings); + + $response = commerce_paypal_adaptive_payrequest($data, $settings); + + if ($response !== FALSE) { + if ($response['responseEnvelope']['ack'] == 'Success') { + $form['#action'] = commerce_paypal_adaptive_redirect_url($response['payKey'], $settings); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Proceed to PayPal'), + ); + } + else { + watchdog('commerce_paypal_adaptive', 'Error #!error_id creating transaction: !message
' . print_r($response['error'][0], TRUE) . '', + ), WATCHDOG_ERROR); + drupal_set_message(t('Unable to initialize transaction with PayPal.'), 'error'); + drupal_goto($settings['cancel_return']); + } + } + else { + drupal_set_message(t('Unable to initialize transaction with PayPal.'), 'error'); + drupal_goto($settings['cancel_return']); + } + + return $form; +} + +/** + * Returns the URL to the specified PayPal Adaptive server. + * + * @param string $server + * Either sandbox or live indicating which server to get the URL for. + * + * @return string + * The URL to use to submit requests to the PayPal Adaptive server. + */ +function commerce_paypal_adaptive_server_url($server) { + switch ($server) { + case 'sandbox': + return 'https://www.sandbox.paypal.com/cgi-bin/webscr'; + + case 'live': + return 'https://www.paypal.com/cgi-bin/webscr'; + } +} + +/** + * Perform a PayPal Adaptive Payments PayRequest Request. + * + * @param array $params + * The parameters for the request. See the PayPal API page (linked below) for + * more information. This structure will be converted to JSON and sent to the + * PayPal API. + * @param array $settings + * Payment method settings array. + * + * @return mixed + * A response object or FALSE if the request could not be completed. + * + * @see https://developer.paypal.com/docs/classic/api/adaptive-payments/Pay_API_Operation/ + */ +function commerce_paypal_adaptive_payrequest($params, $settings) { + if ($settings['ipn_logging'] == 'full_ipn') { + watchdog('commerce_paypal_adaptive', 'PayPal Adaptive PayRequest Request: !ipn_data', array('!ipn_data' => '
' . check_plain(print_r($params, TRUE)) . '')); + } + + $response = commerce_paypal_adaptive_post($params, 'Pay', $settings); + + if ($settings['ipn_logging'] == 'full_ipn') { + watchdog('commerce_paypal_adaptive', 'PayPal Adaptive PayRequest Response: !ipn_data', array('!ipn_data' => '
' . check_plain(print_r($response, TRUE)) . '')); + } + + return $response; +} + +/** + * Perform an API POST operation. + * + * @param array $post_data + * The data to post to the API. + * @param string $type + * The endpoint to post to. Usually 'Pay' or 'PaymentDetails'. + * @param array $settings + * Payment method settings array. + * + * @return mixed + * The decoded API response. + */ +function commerce_paypal_adaptive_post($post_data, $type, $settings) { + if ($type != 'Pay' && $type != 'PaymentDetails') { + drupal_set_message(t('Invalid PayPal Adaptive Payment request: @type.', array('@type' => $type)), 'error'); + watchdog('commerce_paypal_adaptive', 'Invalid PayPal Adaptive Payment request: @type.', array('@type' => $type), WATCHDOG_ERROR); + } + + if ($settings['server'] == 'sandbox') { + $url = 'https://svcs.sandbox.paypal.com/AdaptivePayments/'; + $application_id = 'APP-80W284485P519543T'; + } + else { + $url = 'https://svcs.paypal.com/AdaptivePayments/'; + $application_id = $settings['application_id']; + } + + $curl = curl_init($url . $type); + + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); + curl_setopt($curl, CURLOPT_POSTFIELDS, drupal_json_encode($post_data)); + curl_setopt($curl, CURLOPT_HTTPHEADER, array( + 'X-PAYPAL-SECURITY-USERID: ' . $settings['api_username'], + 'X-PAYPAL-SECURITY-PASSWORD: ' . $settings['api_password'], + 'X-PAYPAL-SECURITY-SIGNATURE: ' . $settings['api_signature'], + 'X-PAYPAL-REQUEST-DATA-FORMAT: JSON', + 'X-PAYPAL-RESPONSE-DATA-FORMAT: JSON', + 'X-PAYPAL-APPLICATION-ID: ' . $application_id, + )); + + $result = curl_exec($curl); + + curl_close($curl); + + return drupal_json_decode($result); +} + +/** + * Generate the redirect URL for an Adaptive Payments payKey. + * + * @param string $pay_key + * The payKey returned by a successful PayRequest. + * @param array $settings + * Payment method settings array. + * + * @return string + * The URL that the customer will be redirected to in order to complete their + * transaction. + */ +function commerce_paypal_adaptive_redirect_url($pay_key, $settings) { + $server_url = commerce_paypal_adaptive_server_url($settings['server']); + $params = array( + 'cmd' => '_ap-payment', + 'paykey' => $pay_key, + ); + return $server_url . '?' . http_build_query($params); +}