<?php
/**
 * @file
 * Main module file for the Commerce ePDQ Payment module.
 */

/**
 * Implements hook_menu().
 */
function commerce_epdq_hpp_menu() {
  $items = array();

  // The ePDQ callback URL. Access is public, but checked via HTTP Basic
  // authentication inside the callback.
  $items['commerceepdq/response'] = array(
    'page callback' => 'commerce_epdq_hpp_process_response',
    'page arguments' => array(),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );

  $items['commerceepdq/complete'] = array(
    'page callback' => 'commerce_epdq_hpp_process_complete',
    'page arguments' => array(),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );

  return $items;
}

/**
 * Implements hook_commerce_payment_method_info().
 *
 * Inform Commerce about the ePDQ payment method.
 */
function commerce_epdq_hpp_commerce_payment_method_info() {
  $payment_methods = array();

  $payment_methods['epdq_hpp'] = array(
    'base' => 'commerce_epdq_hpp',
    'title' => t('Barclaycard ePDQ HPP'),
    'short_title' => t('ePDQ - HPP'),
    'display_title' => variable_get('commerce_epdq_hpp_title', t('Credit or Debit card')),
    'description' => t('Redirect to the Barclaycard ePDQ payment gateway.'),
    'terminal' => FALSE,
    'offsite' => TRUE,
    'offsite_autoredirect' => TRUE,
  );

  return $payment_methods;
}

/**
 * Load the default payment method settings.
 *
 * @see commerce_epdq_hpp_settings_form()
 */
function _commerce_epdq_hpp_default_settings() {
  return array(
    'epdq_hpp_pspid' => '',
    'epdq_hpp_environment' => 'test',
    'epdq_hpp_prod_url' => 'https://payments.epdq.co.uk/ncol/prod/orderstandard_utf8.asp',
    'epdq_hpp_test_url' => 'https://mdepayments.epdq.co.uk/ncol/test/orderstandard_utf8.asp',
    'epdq_hpp_sha_in' => '',
    'epdq_hpp_sha_out' => '',
  );
}

/**
 * Implements hook_permission().
 */
function commerce_epdq_hpp_permission() {
  return array(
    'administer commerce epdq hpp settings' => array(
      'title' => t('Administer site-wide Commerce ePDQ settings'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Implements hook_commerce_order_state_info().
 */
function commerce_epdq_hpp_commerce_order_state_info() {
  $order_states = array();
  $order_states['epdq_hpp_failed'] = array(
    'name' => 'epdq_hpp_failed',
    'title' => t('EPDQ HPP: Payment failed'),
    'description' => t('Payment was attempted via Commerce ePDQ, but the payment failed.'),
    'default_status' => 'epdq_hpp_failed',
    'weight' => 20,
  );
  $order_states['epdq_hpp_paid'] = array(
    'name' => 'epdq_hpp_paid',
    'title' => t('EPDQ HPP: Paid'),
    'description' => t('The order is paid in full.'),
    'default_status' => 'epdq_hpp_paid',
    'weight' => 30,
  );
  return $order_states;
}

/**
 * Implements hook_commerce_order_status_info().
 */
function commerce_epdq_hpp_commerce_order_status_info() {
  $order_statuses = array();

  // The customer was sent to ePDQ for payment.
  $order_statuses['epdq_hpp_processing'] = array(
    'name' => 'epdq_hpp_processing',
    'title' => t('EPDQ HPP: Processing'),
    'state' => 'pending',
  );

  // The customer was sent to ePDQ for payment but nothing has since happened
  // for a while.
  $order_statuses['epdq_hpp_abandoned'] = array(
    'name' => 'epdq_hpp_abandoned',
    'title' => t('EPDQ HPP: Abandoned'),
    'state' => 'canceled',
  );

  // A payment was attempted through ePDQ but failed.
  $order_statuses['epdq_hpp_failed'] = array(
    'name' => 'epdq_hpp_failed',
    'title' => t('EPDQ HPP: Payment failed'),
    'state' => 'epdq_hpp_failed',
  );

  // A payment was made through ePDQ but the order balance is greater than 0.
  $order_statuses['epdq_hpp_incomplete'] = array(
    'name' => 'epdq_hpp_incomplete',
    'title' => t('EPDQ HPP: Incomplete'),
    'state' => 'pending',
  );

  // A successful payment was attempted through ePDQ.
  $order_statuses['epdq_hpp_paid'] = array(
    'name' => 'epdq_hpp_paid',
    'title' => t('EPDQ HPP: Paid'),
    'state' => 'epdq_hpp_paid',
  );

  return $order_statuses;
}

/**
 * Implements hook_rules_event_info().
 */
function commerce_epdq_hpp_rules_event_info() {
  $events = array();
  $events['commerce_epdq_hpp_payment_failed'] = array(
    'label' => t('ePDQ payment failed'),
    'group' => t('Commerce ePDQ - HPP'),
    'variables' => array(
      'commerce_order' => array(
        'type' => 'commerce_order',
        'label' => t('Failed order', array(), array('context' => 'a drupal commerce order')),
      ),
    ),
    'access callback' => 'commerce_order_rules_access',
  );
  return $events;
}

/**
 * Implements CALLBACK_commerce_payment_method_settings_form().
 *
 * Returns a short Form API snippet, for the ePDQ payment method's
 * administrative settings.
 */
function commerce_epdq_hpp_settings_form($settings = NULL) {
  // Merge any given settings with this module's default settings.
  $settings = (array) $settings + _commerce_epdq_hpp_default_settings();

  // Build the settings form additions.
  $form = array();
  $form['epdq_hpp_pspid'] = array(
    '#type' => 'textfield',
    '#title' => t('PSPID'),
    '#description' => t('Your ePDQ merchant ID.'),
    '#default_value' => $settings['epdq_hpp_pspid'],
    '#size' => 10,
    '#required' => TRUE,
  );
  $form['epdq_hpp_environment'] = array(
    '#type' => 'select',
    '#title' => t('Environment'),
    '#description' => t('Which environment should the payment data be sent to'),
    '#default_value' => $settings['epdq_hpp_environment'],
    '#required' => TRUE,
    '#options' => array(
      'test' => t('Test'),
      'prod' => t('Production'),
    ),
  );
  $form['epdq_hpp_sha_in'] = array(
    '#type' => 'textfield',
    '#title' => t('SHA In'),
    '#description' => t('The unique passphrase used to check the integrity of data going to ePDQ'),
    '#default_value' => $settings['epdq_hpp_sha_in'],
    '#required' => TRUE,
  );
  $form['epdq_hpp_sha_out'] = array(
    '#type' => 'textfield',
    '#title' => t('SHA Out'),
    '#description' => t('The unique passphrase used to check the integrity of data coming from ePDQ'),
    '#default_value' => $settings['epdq_hpp_sha_out'],
    '#required' => TRUE,
  );
  $form['epdq_hpp_prod_url'] = array(
    '#type' => 'textfield',
    '#title' => t('ePDQ production URL'),
    '#description' => t('The absolute URL to the ePDQ production payment page.'),
    '#default_value' => $settings['epdq_hpp_prod_url'],
    '#required' => TRUE,
    '#element_validate' => array('commerce_epdq_hpp_settings_form_validate_url'),
  );
  $form['epdq_hpp_test_url'] = array(
    '#type' => 'textfield',
    '#title' => t('ePDQ test URL'),
    '#description' => t('The absolute URL to the ePDQ test payment page.'),
    '#default_value' => $settings['epdq_hpp_test_url'],
    '#required' => TRUE,
    '#element_validate' => array('commerce_epdq_hpp_settings_form_validate_url'),
  );
  return $form;
}

/**
 * Validate URLs entered in the payment method rule settings form.
 *
 * @see commerce_epdq_hpp_settings_form()
 */
function commerce_epdq_hpp_settings_form_validate_url($element, $form, &$form_state) {
  if (!filter_var($element['#value'], FILTER_VALIDATE_URL)) {
    form_error($element, t('Please enter a valid absolute URL.'));
  }
}

/**
 * Sanitize data before posting it to ePDQ.
 */
function _commerce_epdq_hpp_sanitize($str) {
  return trim(preg_replace('/\'/', '', $str));
}

/**
 * Implements CALLBACK_commerce_payment_method_redirect_form().
 */
function commerce_epdq_hpp_redirect_form($form, &$form_state, $order, $payment_method) {

  $settings = $payment_method['settings'];

  // Get an Entity API object wrapper for the $order.
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);

  // Get the billing address.
  $billing_address = $order_wrapper->commerce_customer_billing->commerce_customer_address->value();

  // Define the data to be sent via hidden form fields to the payment page.
  $hidden_data = array(
    'AMOUNT' => $order_wrapper->commerce_order_total->amount->value(),
    'CN' => $billing_address['name_line'],
    'CURRENCY' => 'GBP',
    'EMAIL' => $order->mail,
    'LANGUAGE' => 'en_US',
    'ORDERID' => $order->order_id,
    'OWNERADDRESS' => _commerce_epdq_hpp_sanitize($billing_address['thoroughfare']),
    'OWNERCTY' => $billing_address['country'],
    'OWNERTOWN' => _commerce_epdq_hpp_sanitize($billing_address['locality']),
    'OWNERZIP' => _commerce_epdq_hpp_sanitize($billing_address['postal_code']),
    'PSPID' => $settings['epdq_hpp_pspid'],
  );

  // Generate the SHA-hash
  $sha = '';
  foreach ($hidden_data as $key => $value) {
    $sha .= $key .'='. $value . $settings['epdq_hpp_sha_in'];
  }

  // Create a SHA512 hash to send in data
  $hidden_data['SHASIGN'] = strtoupper(hash('sha512', $sha));

  // Add all this data to the form.
  foreach ($hidden_data as $key => $value) {
    $form[$key] = array('#type' => 'hidden', '#value' => $value);
  }

  // Set the URL to send the data to
  $form['#action'] = $settings['epdq_hpp_prod_url'];
  if ($settings['epdq_hpp_environment'] == 'test') {
    $form['#action'] = $settings['epdq_hpp_test_url'];
  }

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Continue to payment'),
    '#weight' => 100,
  );

  return $form;

}

/**
 * Menu callback for 'commerceepdq/complete'.
 * This is the page users are directed to.
 *
 * @todo put in some error handling for failed transactions
 */
function commerce_epdq_hpp_process_complete() {
  if ($_SERVER['REQUEST_METHOD'] !== 'GET' || empty($_GET['orderID'])) {
    return MENU_NOT_FOUND;
  }

  // @todo check the SHASIGN to make sure data hasn't been tampered with and from epdq

  // Should probably do some checks here to make sure the order is actually complete
  drupal_goto('checkout/'. $_GET['orderID'] .'/complete');
}

/**
 * Menu callback for 'commerceepdq/response'.
 * This is purely to save the data from ePDQ, user doesn't see anything
 */
function commerce_epdq_hpp_process_response() {

  if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_POST['orderID'])) {
    return MENU_NOT_FOUND;
  }

  // @todo check the SHASIGN to make sure data hasn't been tampered with and from epdq

  // Load the order from the $_POST data
  $order = commerce_order_load_by_number($_POST['orderID']);

  // Set up various bits of data we may want to use in the future, save in database
  $fields = array(
    'order_number' => $order ? $order->order_number : $_POST['orderID'],
    'order_id' => $order ? $order->order_id : NULL,
    'transaction_status' => $_POST['STATUS'],
    'order_total' => $_POST['amount'],
    'payment_method' => $_POST['PM'],
    'brand' => $_POST['BRAND'],
    'payment_id' => $_POST['PAYID'],
    'timestamp' => $_POST['TRXDATE'],
  );

  // If we can't load the order, throw an error
  if (!$order) {
    watchdog('commerce_epdq_hpp', '$_POST data for nonexistent order number @on (status: @status).', array(
      '@on' => $fields['order_number'],
      '@status' => $fields['transaction_status'],
    ), WATCHDOG_WARNING);
    drupal_exit();
    return;
  }

  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $currency_code = $order_wrapper->commerce_order_total->currency_code->value();

  // Create a new payment transaction for the order.
  $transaction = commerce_payment_transaction_new('epdq_hpp', $order->order_id);
  $transaction->remote_id = $_POST['orderID'];
  $transaction->amount = commerce_currency_decimal_to_amount($_POST['amount'], $currency_code);
  $transaction->currency_code = $currency_code;
  $transaction->payload[REQUEST_TIME] = $fields;
  $transaction->remote_status = $_POST['STATUS'];

  // @todo - put in some handling for case '5' which is authorised
  switch ($_POST['STATUS']) {

    case '9': // A successful order - money taken
      $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
      $transaction->message = t('Payment complete.');
      break;

    case '2': // Payment failed
      $transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE;
      $transaction->message = t('The payment failed: card declined.');
      break;

    default:
      $transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE;
      $transaction->message = t('The payment failed with the message: @status.', array('@status' => $_POST['STATUS']));
      break;

  }

  // Save the transaction information.
  commerce_payment_transaction_save($transaction);

  // Update the order status.
  $log = t('Updated after transaction ID @txn_id.', array('@txn_id' => $transaction->transaction_id));

  switch ($transaction->status) {
    case COMMERCE_PAYMENT_STATUS_SUCCESS:
      $balance = commerce_payment_order_balance($order);
      $paid_in_full = ($balance && $balance['amount'] <= 0);
      if ($paid_in_full) {
        commerce_order_status_update($order, 'epdq_hpp_paid', FALSE, TRUE, $log);
        commerce_checkout_complete($order);
      }
      else {
        commerce_order_status_update($order, 'epdq_hpp_incomplete', FALSE, TRUE, $log);
      }
      break;

    case COMMERCE_PAYMENT_STATUS_FAILURE:
      commerce_order_status_update($order, 'epdq_hpp_failed', FALSE, TRUE, $log);
      rules_invoke_event('commerce_epdq_hpp_payment_failed', $order);
      break;

  }

  // No need to load the rest of the page: only the ePDQ server sees this.
  drupal_exit();
}
