diff --git uc_protx_vsp_direct.install uc_protx_vsp_direct.install new file mode 100755 index 0000000..07479a4 --- /dev/null +++ uc_protx_vsp_direct.install @@ -0,0 +1,198 @@ + 'Stores transaction requests information', + 'fields' => array( + 'tid' => array( + 'description' => 'The primary identifier for a transaction.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE), + 'oid' => array( + 'description' => 'The current {orders}.oid identifier.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0), + 'TxType' => array( + 'description' => 'Transaction type: "PAYMENT", "DEFERRED" or "AUTHENTICATE".', + 'type' => 'varchar', + 'length' => 15, + 'not null' => TRUE, + 'default' => ''), + 'VendorTxCode' => array( + 'description' => 'This should be your own reference code to the transaction. You should provide a completely unique VendorTxCode for each transaction.', + 'type' => 'varchar', + 'length' => 40, + 'not null' => TRUE, + 'default' => ''), + 'VPSTxId' => array( + 'description' => 'Response. Sage Pay ID to uniquely identify the transaction. Only present if Status is OK.', + 'type' => 'varchar', + 'length' => 38, + 'not null' => TRUE, + 'default' => ''), + 'SecurityKey' => array( + 'description' => '10-digit alphanumeric code used in digitally signing the transaction', + 'type' => 'char', + 'length' => 10, + 'not null' => TRUE, + 'default' => ''), + 'TxAuthNo' => array( + 'description' => 'Unique reference number to the authorisation', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0), + 'Status' => array( + 'description' => 'The transaction status', + 'type' => 'varchar', + 'length' => 15, + 'not null' => TRUE, + 'default' => ''), + 'StatusDetail' => array( + 'description' => 'Human-readable text providing extra detail for the Status message.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => ''), + 'AVSCV2' => array( + 'description' => 'Response from AVS and CV2 checks.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => ''), + 'AddressResult' => array( + 'description' => 'The specific result of the checks on the cardholder\'s address from the AVS/CV2 checks.', + 'type' => 'varchar', + 'length' => 20, + 'not null' => TRUE, + 'default' => ''), + 'PostCodeResult' => array( + 'description' => 'The specific result of the checks on the cardholder\'s Post Code from the AVS/CV2 checks. Not present if the Status is 3DAUTH, AUTHENTICATED or REGISTERED, or PPREDIRECT', + 'type' => 'varchar', + 'length' => 20, + 'not null' => TRUE, + 'default' => ''), + 'CV2Result' => array( + 'description' => 'The specific result of the checks on the cardholder\'s CV2 code from the AVS/CV2 checks. Not present if the Status is 3DAUTH, AUTHENTICATED or REGISTERED, or PPREDIRECT', + 'type' => 'varchar', + 'length' => 20, + 'not null' => TRUE, + 'default' => ''), + 'raw_response' => array( + 'description' => 'Raw response data', + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'medium'), + ), + 'unique keys' => array( + 'oid' => array('oid'), + 'VendorTxCode' => array('VendorTxCode'), + ), + 'primary key' => array('tid'), + ); + + $schema['uc_protx_vsp_direct_refunds'] = array( + 'description' => 'Stores refunds information', + 'fields' => array( + 'rid' => array( + 'description' => 'The primary identifier for a refund.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE), + 'tid' => array( + 'description' => 'The current {uc_protx_requests}.tid identifier.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0), + 'uid' => array( + 'description' => 'The id of the user who requested the refund', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0), + 'VendorTxCode' => array( + 'description' => 'This should be your own reference code for the refund. You should provide a completely unique VendorTxCode for each transaction.', + 'type' => 'varchar', + 'length' => 40, + 'not null' => TRUE, + 'default' => ''), + 'amount' => array( + 'description' => 'Amount to Refund. You can make multiple refunds against a single transaction but the total value of all refunds CANNOT exceed the amount of the original transaction.', + 'type' => 'float', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0.0), + 'Description' => array( + 'description' => 'Free text description of reason for Refund.', + 'type' => 'varchar', + 'length' => 100, + 'not null' => TRUE, + 'default' => ''), + 'VPSTxId' => array( + 'description' => 'Response. Sage Pay ID to uniquely identify the transaction. Only present if Status is OK.', + 'type' => 'varchar', + 'length' => 38, + 'not null' => TRUE, + 'default' => ''), + 'TxAuthNo' => array( + 'description' => 'The Sage Pay authorisation code (also called VPSAuthCode) for the transaction. Only present if Status is OK.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0), + 'Status' => array( + 'description' => 'The transaction status', + 'type' => 'varchar', + 'length' => 15, + 'not null' => TRUE, + 'default' => ''), + 'StatusDetail' => array( + 'description' => 'Human-readable text providing extra detail for the Status message.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => ''), + 'refunded_at' => array( + 'description' => t('Time when the refund was requested.'), + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + ), + 'raw_response' => array( + 'description' => 'Raw response data', + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'medium'), + ), + 'unique keys' => array( + 'rid' => array('rid'), + 'VendorTxCode' => array('VendorTxCode'), + ), + 'primary key' => array('rid'), + ); + + return $schema; +} + +/** + * Implementation of hook_install + */ +function uc_protx_vsp_direct_install() { + drupal_install_schema('uc_protx_vsp_direct'); +} + +/** + * Implementation of hook_uninstall + */ +function uc_protx_vsp_direct_uninstall() { + drupal_uninstall_schema('uc_protx_vsp_direct'); +} diff --git uc_protx_vsp_direct.module uc_protx_vsp_direct.module index 05a63f5..5ef3290 100755 --- uc_protx_vsp_direct.module +++ uc_protx_vsp_direct.module @@ -102,6 +102,15 @@ function uc_protx_vsp_direct_menu() { 'access arguments' => array('authorize credit cards'), 'type' => MENU_CALLBACK ); + $items['admin/store/orders/%uc_order/refunds'] = array( + 'title' => 'Refunds', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('uc_protx_vsp_direct_refund_form', 3), + 'access arguments' => array('refund orders'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 3, + 'file' => 'uc_protx_vsp_direct_refunds.inc', + ); return $items; } @@ -477,6 +486,16 @@ function uc_protx_vsp_direct_charge($order_id, $amount, $data) { } $post = substr($post, 0, -1); + // inject the oid of the order + $transaction['oid'] = $order_id; + // inject the status + $transaction['StatusDetail'] = t('Sending request...'); + + // log request + if(!$transaction_inserted = uc_protx_vsp_direct_insert_transaction($transaction)) { + watchdog('error', 'Could not insert transaction', array(), WATCHDOG_WARNING); + } + list($response, $http_response_code, $curl_error) = uc_protx_vsp_direct_curl($url, $post); if ($curl_error!='') { @@ -499,6 +518,11 @@ function uc_protx_vsp_direct_charge($order_id, $amount, $data) { ); uc_order_comment_save($order_id, $user->uid, $comment); + // update transaction with the response information + if($transaction_inserted) { + uc_protx_vsp_direct_update_transaction($response, $transaction['tid']); + } + // ---------------------------------------- // Now make sense of the response. @@ -697,6 +721,11 @@ function uc_protx_vsp_direct_url($method, $server) { 1 => 'https://test.sagepay.com/gateway/service/direct3dcallback.vsp', 2 => 'https://live.sagepay.com/gateway/service/direct3dcallback.vsp', ), + 'refund' => array( + 0 => 'https://test.sagepay.com/Simulator/VSPServerGateway.asp?Service=VendorRefundTx', + 1 => 'https://test.sagepay.com/gateway/service/refund.vsp', + 2 => 'https://live.sagepay.com/gateway/service/refund.vsp', + ), ); return $servers[$method][$server]; @@ -911,6 +940,25 @@ function uc_protx_vsp_direct_auth_form_submit($form, &$form_state) { return 'admin/store/orders/'. $order_id .'/uc_protx_vsp_direct_auth'; } +function uc_protx_vsp_direct_auth_load_transaction($oid) { + return db_fetch_array(db_query("SELECT * FROM {uc_protx_vsp_direct_transactions} WHERE oid = %d", $oid)); +} + +function uc_protx_vsp_direct_insert_transaction(&$transaction) { + return drupal_write_record('uc_protx_vsp_direct_transactions', $transaction); +} + +function uc_protx_vsp_direct_update_transaction($response, $tid) { + $response['raw_response'] = serialize($response); + $response['tid'] = $tid; + drupal_write_record('uc_protx_vsp_direct_transactions', $response, 'tid'); +} + +function uc_protx_vsp_direct_auth_successful_transaction($transaction) { + return isset($transaction['Status']) && $transaction['Status'] == 'OK'; +} + + /** * Outputs the cards used in the configuration fieldset * @return diff --git uc_protx_vsp_direct_refunds.inc uc_protx_vsp_direct_refunds.inc new file mode 100755 index 0000000..e06f010 --- /dev/null +++ uc_protx_vsp_direct_refunds.inc @@ -0,0 +1,184 @@ + 'hidden', + '#value' => $order->order_id, + ); + + $form['price'] = array( + '#value' => t('Order total: %price %currency', array('%price' => $order->order_total, '%currency' => variable_get('uc_currency_sign', '£'))), + '#prefix' => '
', + '#suffix' => '
', + ); + + $balance_amount = $order->order_total - uc_protx_vsp_direct_refund_compute_refunds($order->order_id); + + $form['balance'] = array( + '#value' => t('Current balance: %balance %currency', array('%balance' => $balance_amount, '%currency' => variable_get('uc_currency_sign', '£'))), + '#prefix' => '
', + '#suffix' => '
', + ); + + $form['amount'] = array( + '#type' => 'textfield', + '#title' => t('Amount to refund (%currency)', array('%currency' => variable_get('uc_currency_sign', '£'))), + '#default_value' => $balance_amount, + '#required' => TRUE, + '#size' => 20, + ); + + $form['description'] = array( + '#type' => 'textarea', + '#title' => t('Description of reason for refund (max 100 characters)'), + '#required' => TRUE, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Refund'), + ); + + $form['#validate'] = array('uc_protx_vsp_direct_refund_form_validate'); + $form['#submit'] = array('uc_protx_vsp_direct_refund_form_submit'); + + return $form; +} + + +function uc_protx_vsp_direct_refund_form_validate($form, &$form_state) { + + $vendor = variable_get('uc_protx_vsp_direct_vendor', ''); + if ($vendor == '' || strlen($vendor) > 15) { + form_set_error('', t('Invalid Vendor Login Name. Authorization Request could not be sent.')); + } + + $order = uc_order_load($form_state['values']['order_id']); + + $balance = $order->order_total - uc_protx_vsp_direct_refund_compute_refunds($form_state['values']['order_id']); + $amount = (float) $form_state['values']['amount']; + + if(!$order) { + form_set_error('', t('Could not load order: #%order_id', array('%order_id' => $form_state['values']['order_id']))); + } + elseif(strlen($form_state['values']['description'] > 100)) { + form_set_error('description', t('The description should be less than 100 characters.')); + } + elseif(!is_numeric($amount)) { + form_set_error('amount', t('The refund amount is invalid.')); + } + elseif($amount < 1 || $amount > 100000) { + form_set_error('amount', t('The refund amount should be a value bigger than 1.00 and smaller than 100,000.00.')); + } + elseif ($amount > $balance) { + form_set_error('amount', t('The refund amount should be less or equal than the balance of the order.')); + } +} + +function uc_protx_vsp_direct_refund_form_submit($form, &$form_state) { + global $user; + + $oid = $form_state['values']['order_id']; + + $order_transaction = uc_protx_vsp_direct_auth_load_transaction($oid); + + $amount = sprintf('%01.2f', $form_state['values']['amount']); + + $transaction = array( + 'VPSProtocol' => UC_PROTX_VSP_DIRECT_PROTOCOL_VERSION, + 'TxType' => 'REFUND', + 'Vendor' => variable_get('uc_protx_vsp_direct_vendor', ''), + 'VendorTxCode' => md5( time() . $user->uid . $oid . rand() ), // This must be unique to the vendor. + 'Amount' => $amount, + 'Currency' => variable_get('uc_currency_code', 'GBP'), // This is a UK-based payment gateway, hence GBP default. + 'Description' => $form_state['values']['description'], + 'RelatedVPSTxId' => $order_transaction['VPSTxId'], + 'RelatedVendorTxCode' => $order_transaction['VendorTxCode'], + 'RelatedSecurityKey' => $order_transaction['SecurityKey'], + 'RelatedTxAuthNo' => $order_transaction['TxAuthNo'], + ); + + // Set HTTPS URL + $url = uc_protx_vsp_direct_url('refund', variable_get('uc_protx_vsp_direct_server', 2)); + + // Put all this data into an HTTPS POST request + $post = ''; + foreach ($transaction as $name => $value) { + //$post .= urlencode(iconv('UTF-8', 'ISO-8859-1', $name)) . '=' . urlencode(iconv('UTF-8', 'ISO-8859-1', $value)) . '&'; + $post .= urlencode($name) .'='. urlencode($value) .'&'; + } + $post = substr($post, 0, -1); + + // record the uid of the user that requests the refund + $transaction['uid'] = $user->uid; + // record the uid of the user that requests the refund + $transaction['amount'] = $amount; + // inject the oid of the order + $transaction['tid'] = $order_transaction['tid']; + // inject the status + $transaction['StatusDetail'] = t('Sending request...'); + // record the time for the refound request + $transaction['refunded_at'] = time(); + + // log request + if(!$transaction_inserted = uc_protx_vsp_direct_insert_refund($transaction)) { + watchdog('error', 'Could not insert refund', array(), WATCHDOG_WARNING); + } + + list($response, $http_response_code, $curl_error) = uc_protx_vsp_direct_curl($url, $post); + + switch($response['Status']) { + case 'OK': + drupal_set_message(t('Successfuly refunded %amount %currency', array('%amount' => $amount, '%currency' => variable_get('uc_currency_sign', '£')))); + uc_order_log_changes($oid, array(t('Successfuly refunded %amount %currency', array('%amount' => $amount, '%currency' => variable_get('uc_currency_sign', '£'))))); + //watchdog('uc_protx_vsp_direct', 'Successfuly refunded %amount %currency', array('%amount' => $amount, '%currency' => variable_get('uc_currency_sign', '£')), WATCHDOG_NOTICE, $order_link); + break; + case 'NOTAUTHED': + case 'MALFORMED': + case 'INVALID': + drupal_set_message(t('The request was not successful. This is the StatusDetail received from gateway: %status_detail', array('%status_detail' => $response['StatusDetail'])), 'warning'); + // set the amount to 0 + $response['amount'] = 0; + break; + case 'ERROR': + drupal_set_message(t('I got an ERROR status repsonse. This is the StatusDetail received from gateway: %status_detail', array('%status_detail' => $response['StatusDetail'])), 'error'); + // We don't actually know if the refund took place or not + $order_link = l($oid, 'admin/store/orders/' . $oid . '/refund'); + watchdog('uc_protx_vsp_direct', "Received an ERROR status response while trying to refund order %order. StatusDetail: %status_detail", array('%order' => $order_link, '%status_detail' => $response['StatusDetail']), WATCHDOG_ERROR, $order_link); + break; + + default: + // This should never happen. + $order_link = l($oid, 'admin/store/orders/' . $oid . '/refund'); + watchdog('uc_protx_vsp_direct', 'SagePay responded with a status code that indicates acceptance of the request, but which is not implemented by this module: @code @StatusDetail', array('@code' => $response['Status'], '@StatusDetail' => $response['StatusDetail']), WATCHDOG_ERROR, $order_link); + break; + } + + // update transaction with the response information + if($transaction_inserted) { + uc_protx_vsp_direct_update_refund($response, $transaction['rid']); + } +} + +function uc_protx_vsp_direct_insert_refund(&$transaction) { + return drupal_write_record('uc_protx_vsp_direct_refunds', $transaction); +} + +function uc_protx_vsp_direct_update_refund($response, $rid) { + $response['raw_response'] = serialize($response); + $response['rid'] = $rid; + drupal_write_record('uc_protx_vsp_direct_refunds', $response, 'rid'); +} + +function uc_protx_vsp_direct_refund_compute_refunds($oid) { + $transaction = uc_protx_vsp_direct_auth_load_transaction($oid); + + $total_refunds_amount = 0; + + $query = db_query("SELECT amount FROM {uc_protx_vsp_direct_refunds} WHERE tid = %d", $transaction['tid']); + while($refund = db_fetch_array($query)) { + $total_refunds_amount += current($refund); + } + + return $total_refunds_amount; +}