diff --git a/wwwroot/sites/all/modules/commerce_paypal/commerce_paypal.module b/wwwroot/sites/all/modules/commerce_paypal/commerce_paypal.module index 5917f90..0907c5b 100644 --- a/wwwroot/sites/all/modules/commerce_paypal/commerce_paypal.module +++ b/wwwroot/sites/all/modules/commerce_paypal/commerce_paypal.module @@ -320,7 +320,7 @@ function commerce_paypal_api_request($payment_method, $nvp = array(), $order = N drupal_alter('commerce_paypal_api_request', $nvp, $order, $payment_method); // Log the request if specified. - if ($payment_method['settings']['log']['request'] == 'request') { + if ($payment_method['settings']['log']['request'] === 'request') { // Mask the credit card number and CVV. $log_nvp = $nvp; $log_nvp['PWD'] = str_repeat('X', strlen($log_nvp['PWD'])); @@ -385,7 +385,7 @@ function commerce_paypal_api_request($payment_method, $nvp = array(), $order = N } // Log the response if specified. - if ($payment_method['settings']['log']['response'] == 'response') { + if ($payment_method['settings']['log']['response'] === 'response') { watchdog('commerce_paypal', 'PayPal server response: !param', array('!param' => '
' . check_plain(print_r($response, TRUE)) . '
', WATCHDOG_DEBUG)); } @@ -445,7 +445,7 @@ function commerce_paypal_rest_api_request($payment_method, $rest = array(), $par drupal_alter('commerce_paypal_rest_api_request', $params, $order, $payment_method); // Log the request if specified. - if ($payment_method['settings']['log']['request'] == 'request') { + if ($payment_method['settings']['log']['request'] === 'request') { // Sanitize the request to mask CC number, CVV, and account info. // This has been turned into a separate function for potential re-use and @@ -461,7 +461,7 @@ function commerce_paypal_rest_api_request($payment_method, $rest = array(), $par $json_error = json_last_error(); // Make sure someone didn't pass us a bogus array to json_encode(). if (!$rest_json && $json_error !== JSON_ERROR_NONE) { - $log_params = commerce_paypal_sanitize_api_params($params); + $log_params = commerce_paypal_sanitize_rest_api_params($params); watchdog('commerce_paypal', 'PayPal REST API request failed to json_encode(): !param', array('!param' => '
' . check_plain(print_r(array('Rest API' => $rest,'Params' => $log_params), TRUE)) . '
'), WATCHDOG_ERROR); return FALSE; } @@ -522,7 +522,7 @@ function commerce_paypal_rest_api_request($payment_method, $rest = array(), $par $response->data = json_decode($response_body); // Log the response if specified. - if ($payment_method['settings']['log']['response'] == 'response') { + if ($payment_method['settings']['log']['response'] === 'response') { watchdog('commerce_paypal', 'PayPal server response: !param', array('!param' => '
' . check_plain(print_r($response, TRUE)) . '
', WATCHDOG_DEBUG)); } @@ -561,7 +561,8 @@ function commerce_paypal_rest_api_token_fetch($payment_method, $params) { curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_NOPROGRESS, 1); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($ch, CURLOPT_FAILONERROR, TRUE); if (variable_get('commerce_paypal_cacert', FALSE)) { curl_setopt($ch, CURLOPT_CAINFO, variable_get('commerce_paypal_cacert', '')); @@ -585,11 +586,11 @@ function commerce_paypal_rest_api_token_fetch($payment_method, $params) { function commerce_paypal_sanitize_param(&$param, $key) { switch ($key) { case 'number': - $value = str_repeat('X', strlen($value) - 4) . substr($value, -4); + $param = str_repeat('X', strlen($param) - 4) . substr($param, -4); break; case 'cvv2': case 'cvv': - $value = str_repeat('X', strlen($value)); + $param = str_repeat('X', strlen($param)); break; } } diff --git a/wwwroot/sites/all/modules/commerce_paypal/modules/vault/commerce_paypal_vault.module b/wwwroot/sites/all/modules/commerce_paypal/modules/vault/commerce_paypal_vault.module index f8a3b5a..1f03d19 100644 --- a/wwwroot/sites/all/modules/commerce_paypal/modules/vault/commerce_paypal_vault.module +++ b/wwwroot/sites/all/modules/commerce_paypal/modules/vault/commerce_paypal_vault.module @@ -6,6 +6,185 @@ */ /** + * Implements hook_menu(). + */ +function commerce_paypal_vault_menu() { + $items = array(); + + // Add a menu item for capturing authorizations. + $items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/paypal-vault-capture'] = array( + 'title' => 'Capture', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('commerce_paypal_vault_capture_form', 3, 5), + 'access callback' => 'commerce_paypal_vault_capture_access', + 'access arguments' => array(3, 5), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'context' => MENU_CONTEXT_INLINE, + 'weight' => 2, + ); + + return $items; +} + +/** + * Determines access to the prior authorization capture form for PayPal WPP + * Vault credit card transactions. + * + * @param $order + * The order the transaction is on. + * @param $transaction + * The payment transaction object to be captured. + * @return bool TRUE or FALSE indicating capture access. + * TRUE or FALSE indicating capture access. + */ +function commerce_paypal_vault_capture_access($order, $transaction) { + // Return FALSE if the transaction isn't for PayPal or isn't awaiting capture. + if ($transaction->payment_method != 'paypal_wpp' || $transaction->remote_status != 'authorize') { + 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); +} + +/** + * Form callback: allows the user to capture a prior authorization via Paypal + * Vault. + */ +function commerce_paypal_vault_capture_form($form, &$form_state, $order, $transaction) { + form_load_include($form_state, 'inc', 'commerce_paypal_wpp', 'includes/commerce_paypal_wpp.admin'); + $form = commerce_paypal_wpp_capture_form($form, $form_state, $order, $transaction); + + $form['#validate'] = array('commerce_paypal_wpp_capture_form_validate'); + $form['#submit'] = array('commerce_paypal_vault_capture_form_submit'); + + return $form; +} + +/** + * Submit handler: process a prior authorization capture via WPP. + */ +function commerce_paypal_vault_capture_form_submit($form, &$form_state) { + $transaction = $form_state['transaction']; + $amount = $form_state['values']['amount']; + + $order = $form_state['order']; + $payment_method = $form_state['payment_method']; + $authorization_valid = TRUE; + + // If the original authorization was more than 3 days ago, PayPal's honor + // period is over and a reauthorization is required before capturing. + if (REQUEST_TIME - $transaction->created > 86400 * 3) { + // Submit the re-authorization request. + $rest = array( + 'method' => 'POST', + 'version' => 'v1', + 'endpoint' => 'payments/authorization/' . $transaction->remote_id . '/reauthorize', + ); + $params = array( + 'amount' => array( + 'total' => $amount, + 'currency' => $transaction->currency_code, + ), + ); + $response = commerce_paypal_rest_api_request($payment_method, $rest, $params, $order); + + // If the response contains an authorization ID... + if (isset($response->data->id)) { + // Update the original transaction with the new ID to use when capturing. + $transaction->remote_id = $response->data->id; + } + else { + // Otherwise do not allow the capture to proceed. + $transaction->message .= '
' . t('Reauthorization failed: @time', array('@time' => format_date(REQUEST_TIME, 'short'))); + $authorization_valid = FALSE; + + // Display a failure message on the redirect. + drupal_set_message(t('PayPal requires a reauthorization before capture after 3 days have passed since the initial authorization.'), 'error'); + drupal_set_message(t('Reauthorization failed with the following error, so the transaction could not capture and will remain in a pending status.'), 'error'); + drupal_set_message(check_plain($response->data->message), 'error'); + } + } + + // If the authorization is valid or successfully reauthorized... + if ($authorization_valid) { + // Determine the remaining balance if the capture is successful. + $balance = commerce_payment_order_balance($order); + $balance = commerce_currency_convert($balance['amount'], $balance['currency_code'], $transaction->currency_code); + $transaction_balance = $balance - commerce_currency_decimal_to_amount($amount, $transaction->currency_code); + + // Submit the capture request request to PayPal. + $rest = array( + 'method' => 'POST', + 'version' => 'v1', + 'endpoint' => 'payments/authorization/' . $transaction->remote_id . '/capture', + ); + $params = array( + 'amount' => array( + 'total' => $amount, + 'currency' => $transaction->currency_code, + ), + ); + if ($transaction_balance == 0) { + $params['is_final_capture'] = TRUE; + } + $response = commerce_paypal_rest_api_request($payment_method, $rest, $params, $order); + + $transaction->payload[REQUEST_TIME . '-capture'] = $response; + + if ($response && $response->code == 200) { + + drupal_set_message(t('Prior authorization captured successfully.')); + + // If this capture did not complete the authorization and subsequent + // captures can be processed against it, create a follow-up transaction + // to represent the remaining authorization amount. + if (!$response->data->is_final_capture) { + $new_transaction = clone($transaction); + unset($new_transaction->transaction_id, $new_transaction->revision_id); + $new_transaction->amount = $transaction_balance; + commerce_payment_transaction_save($new_transaction); + + drupal_set_message(t("A follow-up transaction was made to represent the remaining authorization amount that can be captured within PayPal's allowed time limit.")); + } + + // Update the original transaction amount to the actual capture amount, + // its remote ID to the capture's transaction ID, and its statuses to + // indicate successful payment. + $transaction->amount = commerce_currency_decimal_to_amount($amount, $transaction->currency_code); + $transaction->remote_id = $response->data->id; + $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS; + $transaction->remote_status = $response->data->state; + + $transaction->message = t('Name: @name. Transaction status: !status.', array( + '@name' => 'Card on File', + '!status' => $transaction->status, + )); + + // Note the capture in the transaction message. + $transaction->message .= '
' . t('Captured: @date', array('@date' => format_date(REQUEST_TIME, 'short'))); + + } + else { + // Display an error message but leave the transaction pending. + drupal_set_message(t('Prior authorization capture failed, so the transaction will remain in a pending status.'), 'error'); + drupal_set_message(check_plain($response['L_LONGMESSAGE0']), 'error'); + } + + // Save the updated original transaction. + commerce_payment_transaction_save($transaction); + + // Redirect back to the current order payment page. + $form_state['redirect'] = 'admin/commerce/orders/' . $form_state['order']->order_id . '/payment'; + } +} + +/** * Implements hook_commerce_payment_method_info_alter(). * * Used to add the PayPal Vault callbacks to PayPal Website Payments Pro. @@ -18,9 +197,6 @@ function commerce_paypal_vault_commerce_payment_method_info_alter(&$methods) { if (module_exists('commerce_paypal_wpp') && isset($methods['paypal_wpp'])) { $methods['paypal_wpp']['cardonfile'] = array( 'create callback' => 'commerce_paypal_vault_cardonfile_create', - // @todo: This callback is implemented but it doesn't work because there - // is no documentation on the PATCH method via the PayPal REST API. - // Argh. 'update callback' => 'commerce_paypal_vault_cardonfile_update', 'delete callback' => 'commerce_paypal_vault_cardonfile_delete', 'charge callback' => 'commerce_paypal_vault_cardonfile_charge', @@ -35,8 +211,6 @@ function commerce_paypal_vault_cardonfile_create($form, $form_state, $payment_me $card_number = $form_state['values']['credit_card']['number']; $card_expire_month = $form_state['values']['credit_card']['exp_month']; $card_expire_year = $form_state['values']['credit_card']['exp_year']; - $card_expire = $card_expire_year . '-' . $card_expire_month; - $card_code = $form_state['values']['credit_card']['code']; $card_type = $form_state['values']['credit_card']['type']; $card_owner = $form_state['values']['credit_card']['owner']; $name_fragments = explode(' ', $card_owner); @@ -54,7 +228,7 @@ function commerce_paypal_vault_cardonfile_create($form, $form_state, $payment_me 'last_name' => $last_name, ); $rest = array( - 'endpoint' => 'vault/credit-card', + 'endpoint' => 'vault/credit-cards', 'method' => 'POST', 'version' => 'v1', ); @@ -62,17 +236,17 @@ function commerce_paypal_vault_cardonfile_create($form, $form_state, $payment_me $response = commerce_paypal_rest_api_request($payment_method, $rest, $params); // @todo: This needs much better error handling based on the PayPal response. - if (!$response || $response->code != '201') { - // Our request failed. Whoops. - return FALSE; - drupal_set_message(t('This card was not able to be added. Please contact the site administrator for additional information.'), 'error'); - } - else { + if ($response && $response->code == '201') { // Op success! $card->remote_id = $response->data->id; $card->card_name = $card_owner; return $card; } + else { + // Our request failed. Whoops. + watchdog('commerce_paypal', 'PayPal Vault card on file creation failed: !param', array('!param' => '
' . check_plain(print_r($response, TRUE)) . '
'), WATCHDOG_ERROR); + return FALSE; + } } @@ -84,13 +258,14 @@ function commerce_paypal_vault_cardonfile_delete($form, &$form_state, $payment_m // None of the rigmarole from the various CC gateways. $rest = array( 'method' => 'DELETE', - 'endpoint' => 'vault/credit-card/' . $card->remote_id, + 'endpoint' => 'vault/credit-cards/' . $card->remote_id, 'version' => 'v1', ); $response = commerce_paypal_rest_api_request($payment_method, $rest); + // PayPal Vault returns a 204 on a successful deletion. - if ($response->code == 204) { + if ($response && $response->code == 204) { return TRUE; } else { @@ -101,45 +276,57 @@ function commerce_paypal_vault_cardonfile_delete($form, &$form_state, $payment_m } /** - * Delete callback for PayPal Vault stored cards. + * Update callback for PayPal Vault stored cards. * https://developer.paypal.com/docs/api/#update-a-stored-credit-card * * Doesn't work yet -- waiting on feedback from PayPal Developer Support. */ function commerce_paypal_vault_cardonfile_update($form, &$form_state, $payment_method, $card) { - $card_number = $form_state['values']['credit_card']['number']; $card_expire_month = $form_state['values']['credit_card']['exp_month']; $card_expire_year = $form_state['values']['credit_card']['exp_year']; - //$card_expire = $card_expire_year . '-' . $card_expire_month; - //$card_code = $form_state['values']['credit_card']['code']; - $card_type = $form_state['values']['credit_card']['type']; - // $billing_address = $form_state['values']['commerce_customer_address'][LANGUAGE_NONE][0]; + $card_owner = $form_state['values']['credit_card']['owner']; + $name_fragments = explode(' ', $card_owner); + $last_name = array_pop($name_fragments); + $first_name = implode(' ', $name_fragments); $params = array( - 'op' => 'replace', - 'path' => '/', //to be set - 'value' => array( - 'payer_id' => 'user_' . $card->uid, - 'type' => $card_type, - // 'number' => $card_number, - 'expire_month' => $card_expire_month, - 'expire_year' => $card_expire_year, - // 'first_name' => $billing_address['first_name'], - // 'last_name' => $billing_address['last_name'], - ) + array( + 'op' => 'replace', + 'path' => '/first_name', + 'value' => $first_name, + ), + array( + 'op' => 'replace', + 'path' => '/last_name', + 'value' => $last_name, + ), + array( + 'op' => 'replace', + 'path' => '/expire_month', + 'value' => $card_expire_month, + ), + array( + 'op' => 'replace', + 'path' => '/expire_year', + 'value' => $card_expire_year, + ), ); $rest = array( 'method' => 'PATCH', - 'endpoint' => 'vault/credit-card/' . $card->remote_id, + 'endpoint' => 'vault/credit-cards/' . $card->remote_id, 'version' => 'v1', ); - + $response = commerce_paypal_rest_api_request($payment_method, $rest, $params); - - // @todo: What do we do here? Who knows! There is no documentation and all I get are 401s! - // Even the official SDKs don't implement this even though there is a HATEOAS link provided! - // Argh. - - return FALSE; + + // PayPal Vault returns a 200 on a successful update. + if ($response && $response->code == 200) { + return TRUE; + } + else { + // Something screwed up here. Watchdog the response. + watchdog('commerce_paypal', 'PayPal Vault card on file update failed: !param', array('!param' => '
' . check_plain(print_r($response, TRUE)) . '
'), WATCHDOG_ERROR); + return FALSE; + } } /** @@ -165,7 +352,6 @@ function commerce_paypal_vault_cardonfile_charge($payment_method, $card, $order, if (!isset($charge)) { $wrapper = entity_metadata_wrapper('commerce_order', $order); $charge = commerce_line_items_total($wrapper->commerce_line_items); - } $amount = commerce_currency_amount_to_decimal($charge['amount'], $charge['currency_code']); @@ -175,7 +361,7 @@ function commerce_paypal_vault_cardonfile_charge($payment_method, $card, $order, 'endpoint' => 'payments/payment', ); $params = array( - 'intent' => ($payment_method['settings']['txn_type'] == 'auth_capture' ? 'sale' : 'authorization'), + 'intent' => ($payment_method['settings']['txn_type'] == 'auth_capture' ? 'sale' : 'authorize'), 'payer' => array( 'payment_method' => 'credit_card', 'funding_instruments' => array( @@ -200,40 +386,59 @@ function commerce_paypal_vault_cardonfile_charge($payment_method, $card, $order, $response = commerce_paypal_rest_api_request($payment_method, $rest, $params, $order); - $txn = commerce_payment_transaction_new($payment_method['method_id'], $order->order_id); - $txn->instance_id = $payment_method['instance_id']; - $txn->remote_id = $response->data->id; - $txn->amount = $amount; - $txn->currency_code = $charge['currency_code']; - $txn->payload[REQUEST_TIME] = $response; - $txn->remote_status = $response->data->state; - if ($response && in_array($txn->remote_status, array('approved', 'created'))) { + // @todo: needs better error checking? + $transaction = commerce_payment_transaction_new($payment_method['method_id'], $order->order_id); + $transaction->instance_id = $payment_method['instance_id']; + $transaction->remote_id = isset($response->data->id) ? $response->data->id : ''; + $transaction->amount = $charge['amount']; + $transaction->currency_code = $charge['currency_code']; + $transaction->payload[REQUEST_TIME] = $response; + $transaction->remote_status = isset($response->data->state) ? $response->data->state : 'failed'; + if (isset($response->data->intent) && $response->data->intent == 'authorize' && isset($response->data->state) && $response->data->state = 'approved') { + // Set our remote status to authorize so we can capture it later. + $transaction->remote_status = 'authorize'; + if (isset($response->data->transactions[0]->related_resources[0]->authorization->id)) { + // Save our authorization id for future use with capture. + // @todo: find a way to get this data easily? + $transaction->remote_id = $response->data->transactions[0]->related_resources[0]->authorization->id; + } else { + // Set the transaction to failed as we need the authorization id to + // capture this order. + $transaction->remote_status = 'failed'; + } + } + if (in_array($transaction->remote_status, array('approved', 'created', 'authorize'))) { switch ($payment_method['settings']['txn_type']) { case COMMERCE_CREDIT_AUTH_ONLY: - $txn->status = COMMERCE_PAYMENT_STATUS_PENDING; + $transaction->status = COMMERCE_PAYMENT_STATUS_PENDING; break; case COMMERCE_CREDIT_AUTH_CAPTURE: - $txn->status = COMMERCE_PAYMENT_STATUS_SUCCESS; + $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS; break; case COMMERCE_CREDIT_CAPTURE_ONLY: - $txn->status = COMMERCE_PAYMENT_STATUS_SUCCESS; - break; + $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS; + break; } } else { - $txn->status = COMMERCE_PAYMENT_STATUS_FAILURE; + $transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE; } + $transaction->message = t('Name: @name. Transaction status: !status.', array( + '@name' => 'Card on File', + '!status' => $transaction->status, + )); - commerce_payment_transaction_save($txn); + commerce_payment_transaction_save($transaction); // @todo: Check for other error types and do deactivation of expired/messed-up cards here. - if (!in_array($txn->remote_status, array('approved', 'created'))) { - if ($response && isset($response->data->details)) { + if ($transaction->status == COMMERCE_PAYMENT_STATUS_FAILURE) { + if (isset($response->data->details)) { + $messages = array(); foreach ($response->data->details as $detail) { $messages[] = $detail->field . ' -- ' . $detail->issue; } drupal_set_message(t('We received the following error processing your card. Please enter your information again or try a different card.'), 'error'); - drupal_set_message(implode('
', array_map('check_plain', $messages)), 'error'); + watchdog('commerce_paypal', 'PayPal Vault card on file charge failed: !param', array('!param' => implode('
', array_map('check_plain', $messages))), WATCHDOG_ERROR); } else { drupal_set_message(t('We received an error while processing your card. Please try a different card or contact a site administrator for assistance.'), 'error'); @@ -243,5 +448,3 @@ function commerce_paypal_vault_cardonfile_charge($payment_method, $card, $order, return TRUE; } - -