array( 'title' => t('Administer receipt settings'), 'description' => t(''), ), 'refund receipts' => array( 'title' => t('Refund receipts'), 'description' => t(''), ), 'view receipts' => array( 'title' => t('View receipts'), 'description' => t(''), ), 'view own receipts' => array( 'title' => t('View own receipts'), 'description' => t(''), ), 'complete receipts' => array( 'title' => t('Complete receipts'), 'description' => t(''), ), ); } /** * Implements hook_menu(). */ function ec_receipt_menu() { $items = array(); $items['admin/store/receipts/%ec_receipt/refund'] = array( 'title' => 'Refund Receipt', 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_refund', 3), 'access arguments' => array('refund receipts'), 'type' => MENU_CALLBACK, 'file' => 'ec_receipt.admin.inc', ); $items['admin/store/receipts/%ec_receipt/complete'] = array( 'title' => 'Complete receipt', 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_complete_form', 3), 'access arguments' => array('complete receipts'), 'type' => MENU_CALLBACK, 'file' => 'ec_receipt.admin.inc', ); $rtypes = ec_receipt_get_types(); $items['admin/config/store/rtypes'] = array( 'title' => 'Receipts', 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_settings'), 'access arguments' => array('administer receipt settings'), 'description' => 'Maintain receipt types.', 'file' => 'ec_receipt.admin.inc', ); $items['admin/config/store/rtypes/settings'] = array( 'title' => 'Settings', 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_settings'), 'access arguments' => array('administer receipt settings'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'file' => 'ec_receipt.admin.inc', 'weight' => -10, ); $items['admin/config/store/rtypes/list'] = array( 'title' => 'Types', 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_admin_rtypes_form'), 'access arguments' => array('administer receipt settings'), 'description' => 'Maintain receipt types.', 'type' => MENU_LOCAL_TASK, 'file' => 'ec_receipt.admin.inc', ); $rtypes = ec_receipt_get_types('names'); foreach ($rtypes as $type => $name) { $items['admin/config/store/rtypes/' . $type] = array( 'title' => t($name), 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_admin_type_form', 4), 'access arguments' => array('administer receipt settings'), 'type' => MENU_CALLBACK, 'file' => 'ec_receipt.admin.inc', ); $items['admin/config/store/rtypes/' . $type . '/options'] = array( 'title' => t($name) ." ". t('Options'), 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_admin_type_form', 4), 'access arguments' => array('administer receipt settings'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, 'file' => 'ec_receipt.admin.inc', ); } $items['admin/config/store/rtypes/alloc'] = array( 'title' => 'Allocation', 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_admin_atypes_form'), 'access arguments' => array('administer receipt settings'), 'description' => 'Maintain and configure allocations types', 'file' => 'ec_receipt.admin.inc', 'type' => MENU_LOCAL_TASK, ); $items['store/receipt/%ec_receipt/view'] = array( 'title' => 'View Receipt', 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_view_form', 2, TRUE), 'access callback' => 'ec_customer_check_access', 'access arguments' => array('receipt', 2), 'type' => MENU_CALLBACK, 'file' => 'ec_receipt.admin.inc', ); $items['store/receipt/%ec_receipt/reverse/%'] = array( 'title' => 'Reverse Allocation', 'page callback' => 'drupal_get_form', 'page arguments' => array('ec_receipt_reverse_allocation_form', 2, 4), 'access arguments' => array('refund receipts'), 'type' => MENU_CALLBACK, 'file' => 'ec_receipt.admin.inc', ); return $items; } /** * Implements hook_entity_info(). */ function ec_receipt_entity_info() { return array( 'ec_receipt' => array( 'label' => t('Receipt'), 'base table' => 'ec_receipt', 'entity keys' => array( 'id' => 'erid', ), 'load hook' => 'entity_receipt_load', ), ); } /** * Implements hook_theme(). */ function ec_receipt_theme() { return array( 'ec_receipt_checkout_types' => array( 'variables' => array('rtype' => NULL), 'template' => 'ec_receipt_checkout_types', 'path' => drupal_get_path('module', 'ec_receipt') . '/templates', ), 'ec_receipt_admin_atypes_form' => array( 'render element' => 'form', 'file' => 'ec_receipt.theme.inc', ), 'ec_receipt_admin_rtypes_form' => array( 'render element' => 'form', 'file' => 'ec_receipt.theme.inc', ), 'ec_receipt_icon' => array( 'variables' => array('icon' => NULL), 'file' => 'ec_receipt.theme.inc', ), 'ec_receipt_review_form' => array( 'render element' => 'form', 'file' => 'ec_receipt.theme.inc', ), ); } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_preprocess_ec_receipt_checkout_types(&$variables) { if (isset($variables['rtype']) && $variables['rtype']) { $variables['template_files'][] = 'ec_receipt_checkout_types_' . $variables['rtype']->type; $variables += (array) $variables['rtype']; } else { $variables['name'] = ''; } if (isset($variables['icon']) && $variables['icon']) { $variables['icon'] = theme('ec_receipt_icon', array('icon' => $variables['icon'])); } } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_access_receipt() { if ($customer = ec_customer_get_by_uid(arg(1))) { return (user_access(EC_PERM_MANAGE) || arg(1) == $user->uid); } return FALSE; } /** * Implements hook_views_api(). */ function ec_receipt_views_api() { return array('api' => 2.0); } /** * Implements hook_checkout_info(). */ function ec_receipt_checkout_info() { return array( 'receipts' => array( 'name' => t('Receipting'), 'description' => t("If required, display the 'Select Payment Option' page to the customer. This checkout page is skipped if there is only one receipt type (payment gateway) enabled."), 'module' => 'ec_receipt', 'file' => 'ec_receipt.checkout.inc', ), ); } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_checkout_types($a) { return theme('ec_receipt_checkout_types', array('rtype' => $a)); } /** * Retrieve Receipt types */ function ec_receipt_get_types($op = 'types', $rtype = NULL, $reset = FALSE) { $_receipt_types = & drupal_static(__FUNCTION__ . '__receipt_types'); $_receipt_names = & drupal_static(__FUNCTION__ . '__receipt_names'); if ($reset || !isset($_receipt_types)) { $_receipt_types = array(); $_receipt_names = array(); foreach (module_implements('receipt_info') as $module) { $path = drupal_get_path('module', $module); $types = module_invoke($module, 'receipt_info'); foreach ($types as $type => $info) { $_receipt_types[$type] = (object) $info; $_receipt_types[$type]->type = $type; $_receipt_names[$type] = $_receipt_types[$type]->name; if (isset($info['file'])) { $_receipt_types[$type]->filepath = $path . '/' . $info['file']; } } } $result = db_query('SELECT * FROM {ec_receipt_types}') ->fetchAll(); foreach ($result as $info) { if (isset($_receipt_types[$info->type]) && (empty($_receipt_types[$info->type]->internal) || !$_receipt_types[$info->type]->internal)) { $_receipt_types[$info->type]->name = !empty($info->name) ? $info->name : $_receipt_types[$info->type]->name; $_receipt_names[$info->type] = $_receipt_types[$info->type]->name; $_receipt_types[$info->type]->description = !empty($info->description) ? $info->description : $_receipt_types[$info->type]->description; $_receipt_types[$info->type]->allow_payments = isset($_receipt_types[$info->type]->allow_payments) ? $info->allow_payments : NULL; $_receipt_types[$info->type]->allow_admin_payments = isset($_receipt_types[$info->type]->allow_admin_payments) ? $info->allow_admin_payments : NULL; $_receipt_types[$info->type]->allow_refunds = isset($_receipt_types[$info->type]->allow_refunds) ? $info->allow_refunds : NULL; $_receipt_types[$info->type]->allow_payto = isset($_receipt_types[$info->type]->allow_payto) ? $info->allow_payto : NULL; $_receipt_types[$info->type]->allow_recurring = isset($_receipt_types[$info->type]->allow_recurring) ? $info->allow_recurring : NULL; $_receipt_types[$info->type]->weight = $info->weight; $_receipt_types[$info->type]->in_db = TRUE; } else { ec_receipt_type_delete($info->type); } } if (!empty($_receipt_types)) { uasort($_receipt_types, 'ec_sort'); asort($_receipt_names); } else { return array(); } } if (isset($rtype) && !isset($_receipt_types[$rtype])) { return FALSE; } switch ($op) { case 'types': return $_receipt_types; case 'type': if (isset($_receipt_types[$rtype])) { return $_receipt_types[$rtype]; } break; case 'module': if (isset($_receipt_types[$rtype])) { return $_receipt_types[$rtype]->module; } break; case 'names': return $_receipt_names; case 'name': if (isset($_receipt_names[$rtype])) { return $_receipt_names[$rtype]; } break; } } /** * Get receipting allocation types. */ function ec_receipt_get_atypes($op = 'types', $atype = NULL, $reset = FALSE) { $_types = & drupal_static(__FUNCTION__ . '__types'); $_names = & drupal_static(__FUNCTION__ . '__names'); if ($reset || empty($_types)) { foreach (module_implements('allocation_info') as $module) { $path = drupal_get_path('module', $module); $atypes = module_invoke($module, 'allocation_info'); foreach ($atypes as $type => $info) { $_types[$type] = (object) $info; $_types[$type]->type = $type; $_names[$type] = $_types[$type]->name; if (isset($info['file'])) { $_types[$type]->filepath[] = $path . '/' . $info['file']; } } } asort($_names); uasort($_types, 'ec_sort'); drupal_alter('ec_receipt_atypes', $_types); } if (!empty($atype) && !isset($_types[$atype])) { return FALSE; } switch ($op) { case 'types': return $_types; break; case 'type': return $_types[$atype]; break; case 'module': return $_types[$atype]->module; break; case 'names': return $_names; break; case 'name': return $_names[$atype]; break; } return FALSE; } /** * Process payment for transaction that has not been completed. * * This process is used when you have transactions that are going to be created * very soon, but the need is to get the payment before creating the transaction * and then after allocating the payment to the transaction. * This is used in checkout situations where the transaction is not created * until the money has taken from the customer. * This also gives the opportunity to pass the error message back to the user * and then they can edit the cart and make any corrections to the purchase. * This process will be used with payment gateways like Authorize.net and * ccard where the interaction with the payment gateway is between the web * server and the payment gateway. */ function ec_receipt_payment_process($type, &$object) { $rtype = ec_receipt_alloc_invoke($type, 'get_payment_type', $object); if (ec_receipt_get_invoke_function($rtype, 'process_payment')) { if ($receipt = ec_receipt_load(ec_receipt_build_receipt($type, $object))) { return ec_receipt_invoke($rtype, 'process_payment', $receipt, $type, $object); } } } /** * Process payments for hosted gateways. * * Hosted payment gateways like PayPal process the payment directly with * payment gateway and then the result is past back the the web server to * complete the payment. */ function ec_receipt_payment_goto($type, $object, $alloc, $erid = NULL) { $rtype = ec_receipt_alloc_invoke($type, 'get_payment_type', $object); if (ec_receipt_get_invoke_function($rtype, 'payment_url')) { if ($erid && !empty($alloc)) { $receipt = ec_receipt_load($erid); $receipt->allocation = $alloc; ec_receipt_save($receipt); } if ($receipt = ec_receipt_load($erid ? $erid : ec_receipt_build_receipt($type, $object, $alloc))) { return ec_receipt_invoke($receipt->type, 'payment_url', $receipt, $type, $object); } } } /** * Allocate payment to a transaction. */ function ec_receipt_allocate($receipt, $allocations) { $v = array(); $rows = 0; $callback = array(); if ($receipt->balance > 0) { foreach ($allocations as $allocate) { if ($object = ec_receipt_alloc_invoke($allocate['type'], 'load', $allocate['id'])) { $currency = ec_receipt_alloc_invoke($allocate['type'], 'get_currency', $object); if ($currency == $receipt->currency) { $total = ec_receipt_alloc_invoke($allocate['type'], 'get_total', $object); $amount_allocated = ec_receipt_get_allocated_total(array('type' => $allocate['type'], 'etid' => $allocate['id'])); $balance = ($total - $amount_allocated) < 0 ? 0 : ($total - $amount_allocated); if ($balance > 0) { $amount = isset($allocate['amount']) && $receipt->balance > $allocate['amount'] ? $allocate['amount'] : $receipt->balance; $amount = $balance > $amount ? $amount : $balance; $rows++; $alloc[] = array( 'erid' => $receipt->erid, 'type' => $allocate['type'], 'etid' => $allocate['id'], 'created' => REQUEST_TIME, 'amount' => $amount, ); $callback[] = array($allocate['type'], 'allocation', $object, end($alloc), ($balance - $amount)); $receipt->allocated += $amount; $receipt->balance -= $amount; if ($receipt->balance <= 0) { break; } } } else { watchdog('ec_receipt', 'Unable to allocate receipt %erid (%currency) to %type-%id (%obj-currency) as currencies are not the same.', array('%type' => $allocate['type'], '%id' => $allocate['id'], '%erid' => $receipt->erid, '%currency' => $receipt->currency, '%obj-currency' => $currency)); } } else { watchdog('ec_receipt', 'Unable to load type %type, id %id to allocate to receipt %erid', array('%type' => $allocate['type'], '%id' => $allocate['id'], '%erid' => $receipt->erid)); } } if ($rows) { foreach ($alloc as $record) { drupal_write_record('ec_receipt_allocation', $record); } } ec_receipt_save($receipt); foreach ($callback as $args) { call_user_func_array('ec_receipt_alloc_invoke', $args); } } } /** * Create a receipt and submit to the database. * * TODO: Maybe we need to enhance this so it will so that it will check all * parameters and make sure it is a valid receipt. */ function ec_receipt_create_receipt($receipt) { return ec_receipt_save($receipt); } /** * Build receipt from allocation object. */ function ec_receipt_build_receipt($type, $object, $alloc = NULL, $amount = NULL) { $receipt = new StdClass; $customer = ec_customer_get_customer(ec_receipt_alloc_invoke($type, 'get_customer', $object)); if (empty($customer->ecid)) { $customer = ec_customer_insert($customer); } if (empty($customer->ecid)) { return FALSE; } $receipt->type = ec_receipt_alloc_invoke($type, 'get_payment_type', $object); $receipt->ecid = $customer->ecid; $receipt->currency = ec_receipt_alloc_invoke($type, 'get_currency', $object); $receipt->amount = $receipt->balance = (isset($amount) && is_numeric($amount) ? $amount : ec_receipt_alloc_invoke($type, 'get_total', $object)); $receipt->allocated = 0; $receipt->status = RECEIPT_STATUS_PENDING; if (!empty($alloc)) { $receipt->allocation = $alloc; } return ec_receipt_create_receipt($receipt); } /** * Save Receipt to database. */ function ec_receipt_save($receipt) { $new = empty($receipt->erid); $ret = FALSE; if ($new) { if (empty($receipt->created)) { $receipt->created = REQUEST_TIME; } $orig_receipt = NULL; } else { $orig_receipt = ec_receipt_load($receipt->erid); foreach ($orig_receipt as $key => $value) { if (!isset($receipt->$key)) { $receipt->$key = $value; } } } $receipt->changed = REQUEST_TIME; if (!$new) { $ret = drupal_write_record('ec_receipt', $receipt, 'erid'); } if ($new || !$ret) { drupal_write_record('ec_receipt', $receipt); } ec_receipt_invoke($receipt->type, 'save', $receipt); $receipt = ec_receipt_load($receipt->erid); module_invoke_all('ec_receipt_post_save', $receipt, $orig_receipt); return $receipt->erid; } /** * Load Receipt. * * @param $erid * Receipt id to be loaded. * @return * Returns the receipt object or NULL if no receipt is found. */ function ec_receipt_load($erid) { // ERROR: return statement not found in hook_load if ($receipt = db_query('SELECT * FROM {ec_receipt} WHERE erid = :erid', array(':erid' => $erid))->fetchObject()) { if (!empty($receipt->allocation)) { $receipt->allocation = unserialize($receipt->allocation); } if ($extra = ec_receipt_invoke($receipt->type, 'load', $receipt)) { foreach ($extra as $key => $value) { $receipt->$key = $value; } } return $receipt; } } /** * Wrapper for ec_receipt_load() to allow it to work with entities. */ function ec_receipt_entity_receipt_load($receipts) { foreach ($receipts as $erid => $receipt) { $receipts[$erid] = ec_receipt_load($receipt->erid); } } /** * Filter payment types based upon a criteria. * * @param $criteria * An array of keys/values which allow the filtering of payment gateways. * @param $type * Allocation type of the object which is passed. This is passed to the payment * gateway for more advanced filtering. * @param $object * The object which is being passed to the payment gateway for collecting * information on with what the payment is going to be paid against which may * or may not impact on the list of payment gateways which are available. * @return * Return a list of payment gateways which are available for this object. */ function ec_receipt_type_filter($criteria = array(), $type = NULL, $object = NULL) { _ec_receipt_filter($criteria, 'init', $type, $object); return array_filter(ec_receipt_get_types(), '_ec_receipt_filter'); } function _ec_receipt_filter($var, $op = 'process', $itype = NULL, $iobject = NULL) { $filter = & drupal_static(__FUNCTION__ . '_filter', NULL); $type = & drupal_static(__FUNCTION__ . '_type', NULL); $object = & drupal_static(__FUNCTION__ . '_object', NULL); if ($op == 'init') { $filter = $var; $type = $itype; $object = $iobject; return; } foreach ($filter as $key => $opt) { if ($opt === TRUE || $opt === FALSE) { $opt = $opt === TRUE ? 1 : 0; } $value = isset($var->$key) ? $var->$key : array(); if ($value === TRUE || $value === FALSE) { $value = $value === TRUE ? 1 : 0; } if (is_array($value)) { if (!in_array(drupal_strtoupper($opt), $value)) { return FALSE; } } elseif (is_numeric($value)) { if ($opt != $value) { return FALSE; } } else { if ($opt != $value) { return FALSE; } } } $return = ec_receipt_invoke($var->type, 'filter', $type, $object); if (!is_null($return) && !$return) { return $return; } foreach (module_implements('ec_receipt_type_filter') as $module) { $return = module_invoke($module, 'ec_receipt_type_filter', $var->type, $type, $object); if (!is_null($return) && !$return) { return $return; } } return TRUE; } /** * Return the function that should be called for a receipt invoke. */ function ec_receipt_get_invoke_function($ctype, $hook) { if ($info = ec_receipt_get_types('type', $ctype)) { if (isset($info->filepath)) { include_once DRUPAL_ROOT . '/' . $info->filepath; } foreach ($info->module as $module) { $function = $module . '_receipt_' . $hook; if (function_exists($function)) { return $function; } } } } /** * Invoke receipt type operations. */ function ec_receipt_invoke() { $args = func_get_args(); $ctype = array_shift($args); $hook = array_shift($args); if ($function = ec_receipt_get_invoke_function($ctype, $hook)) { return call_user_func_array($function, $args); } } /** * Invoke receipt allocation type operations. */ function ec_receipt_alloc_invoke() { $args = func_get_args(); $atype = array_shift($args); $hook = array_shift($args); $info = ec_receipt_get_atypes('type', $atype); if (isset($info->filepath)) { array_map('_ec_load_map_inc', $info->filepath); } foreach ($info->module as $module) { $function = $module . '_alloc_' . $hook; if (function_exists($function)) { return call_user_func_array($function, $args); } } } function _ec_load_map_inc($a) { include_once DRUPAL_ROOT . '/' . $a; return $a; } /** * Save receipt types. */ function ec_receipt_type_save($info) { $info = (object) array_merge((array) ec_receipt_get_types('type', $info->type), (array) $info); $existing = db_select('ec_receipt_types', 'ert') ->fields('ert') ->condition('type', $info->type) ->execute() ->fetch(); if ($existing) { drupal_write_record('ec_receipt_types', $info, 'type'); } else { drupal_write_record('ec_receipt_types', $info); } } /** * Delete receipt types. */ function ec_receipt_type_delete($type) { db_delete('ec_receipt_types') ->condition('type', $type) ->execute(); } /** * A list of the world currencies. */ function ec_receipt_currency_list($filter = NULL) { $currency = & drupal_static(__FUNCTION__ . '_currency', NULL); if (empty($currency)) { module_load_include('inc', 'ec_receipt', 'currency'); $currency = ec_receipt_get_all_currencies(); } if (empty($filter)) { $filter = array(); if ($types = ec_receipt_get_types()) { foreach ($types as $info) { if (!empty($info->currencies_supported) && is_array($info->currencies_supported)) { $filter += $info->currencies_supported; } } } } return array_intersect_key($currency, array_flip($filter)); } /** * Implements hook_ec_receipt_post_save(). */ function ec_receipt_ec_receipt_post_save($receipt, $original) { if (empty($original) || $receipt->status != $original->status) { module_invoke_all('ec_receipt_status', $receipt, $original); } if (!empty($original)) { db_update('search_dataset') ->fields(array( 'reindex' => REQUEST_TIME, )) ->condition('sid', $receipt->erid) ->condition('type', 'ec_receipt') ->execute(); } } /** * Implements hook_ec_receipt_status(). * * Once the payment has been completed allocate the payment based on the * pre-loaded rules. However if the payments status is changed from completed * to another status like refunded, then reverse the allocations. */ function ec_receipt_ec_receipt_status($receipt, $original) { $completed = array(RECEIPT_STATUS_COMPLETED); if (in_array($receipt->status, $completed) && !empty($receipt->allocation)) { ec_receipt_allocate($receipt, $receipt->allocation); } elseif (!empty($original) && in_array($original->status, $completed)) { // TODO: Reverse the Allocations. } } /** * Get all allocated data for a receipt */ function ec_receipt_get_allocations($erid) { $allocations = array(); $result = db_query('SELECT * FROM {ec_receipt_allocation} WHERE erid = :erid AND reversed = 0', array(':erid' => $erid)); foreach ($result as $allocation) { $allocations[$allocation->eaid] = $allocation; } return $allocations; } /** * Return the total allocated from the filter */ function ec_receipt_get_allocated_total($filter) { $query = db_select('ec_receipt_allocation', 'era'); foreach ($filter as $field => $value) { $query->condition('era.' . $field, $value, '='); } $query->addExpression('SUM(amount)', 'total'); $result = $query->execute(); return $result->fetchField(0); } /** * Reverse Allocation. * TODO: Convert to use drupal_write_record. */ function ec_receipt_reverse_allocation($receipt, $alloc) { if ($object = ec_receipt_alloc_invoke($alloc->type, 'load', $alloc->etid)) { if ((ec_receipt_alloc_invoke($alloc->type, 'can_reverse', $object)) === FALSE) { return FALSE; } $total = ec_receipt_alloc_invoke($alloc->type, 'get_total', $object); $reversal = clone $alloc; unset($reversal->eaid); $reversal->created = time(); $reversal->amount*= -1; $reversal->reversed = $alloc->eaid; $receipt->allocated+= $reversal->amount; $receipt->balance-= $reversal->amount; $params = array( 'amount' => $reversal->amount, ); ec_receipt_alloc_invoke($alloc->type, 'allocation', $object, $params, $total - $reversal->amount); drupal_write_record('ec_receipt_allocation', $reversal); $alloc->reversed = $reversal->eaid; drupal_write_record('ec_receipt_allocation', $alloc, 'eaid'); ec_receipt_save($receipt); return TRUE; } } /** * Implementation hook_link(). */ function ec_receipt_link($type, $object, $teaser = FALSE) { $links = array(); if ($type == 'ec_store_transaction' && $object->allocation) { $links['receipt_search'] = array( 'title' => t('View receipts'), 'href' => 'admin/store/receipts', 'query' => array( 'allocation' => array('' => 'transaction'), 'etid' => $object->txnid, ), 'attributes' => array( 'title' => t('View receipts for transaction @txnid', array('@txnid' => $object->txnid)), ), ); } if ($type == 'ec_receipt') { $links['receipt_details'] = array( 'title' => t('View'), 'href' => 'store/receipt/' . $object->erid . '/view', ); if (in_array($object->status, array(RECEIPT_STATUS_PENDING, RECEIPT_STATUS_RECEIVED)) && user_access('complete receipts')) { $links['receipt_complete'] = array( 'title' => t('Complete'), 'href' => 'admin/store/receipts/' . $object->erid . '/complete', 'query' => drupal_get_destination(), ); } if ($object->status == RECEIPT_STATUS_COMPLETED && $object->allocated == 0 && user_access(EC_PERM_MANAGE)) { $links['refund_receipt'] = array( 'title' => t('Refund Receipt'), 'href' => 'admin/store/receipts/' . $object->erid . '/refund', 'query' => drupal_get_destination(), ); } } if ($type == 'ec_receipt' && isset($object->eaid)) { $links['transaction_search'] = array( 'title' => t('View transactions'), 'href' => 'admin/store/transaction', 'query' => 'erid=' . $object->erid, 'attributes' => array( 'title' => t('View transactions paid by receipt @erid', array('@erid' => $object->erid)), ), ); } if ($type == 'ec_receipt_allocation' && user_access(EC_PERM_MANAGE) && !$object->reversed && $object->type != 'refund') { $links['reverse_allocation'] = array( 'title' => t('Reverse allocation'), 'href' => 'store/receipt/' . $object->erid . '/reverse/' . $object->eaid, 'attributes' => array( 'title' => t('Reverse allocation'), ), ); } if ($type == 'ec_customer_list') { $links['receipts'] = array( 'title' => t('Receipts'), 'href' => 'admin/store/receipts', 'query' => 'ecid=' . $object->ecid, ); } return $links; } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_get_statuses() { return array( RECEIPT_STATUS_PENDING => t('Pending'), RECEIPT_STATUS_RECEIVED => t('Received'), RECEIPT_STATUS_COMPLETED => t('Completed'), RECEIPT_STATUS_FAILED => t('Failed'), RECEIPT_STATUS_DENIED => t('Denied'), RECEIPT_STATUS_REFUND_PENDING => t('Refund Pending'), RECEIPT_STATUS_REFUNDED => t('Refunded'), RECEIPT_STATUS_CANCELLED => t('Cancelled'), ); } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_get_status($status = NULL) { $statuses = ec_receipt_get_statuses(); return isset($status) && isset($statuses[$status]) ? $statuses[$status] : $status; } /** * Converts an associative array to xml. Useful for creating XML for payment * gateways. An example array: * * array( * 'Foo' => '1', * 'Links' => array( * array( * 'Link' => array( * 'Url' => 'http://synerger.com', * 'Title' => 'Synerger Pty Ltd', * ), * ), * array( * 'Link' => array( * 'Url' => 'http://drupal.org', * 'Title' => 'Drupal: Community Plumbing', * ), * ), * ), * 'Information' => array( * 'Name' => 'Sammy Spets', * 'Gender' => 'Male', * ), * ); * * Will produce the following XML: * * 1 * * * http://synerger.com * Synerger Pty Ltd * * * http://drupal.org * Drupal: Community Plumbing * * * * Sammy Spets * Male * * * @param $request * Array, Described in documentation above. * @param $depth * Number, Indentation depth. * @param $indent * Boolean, TRUE if indentation is desired in the output. * @return * String, XML conversion of the request given. */ function ec_receipt_convert_request_to_xml(&$request, $depth = 0, $indent = FALSE) { $output = ($depth == 0 ? "\n" : ''); $indent = $indent ? str_repeat(' ', $depth) : ''; foreach ($request as $key => $value) { // Can't use numbers as XML tags. Also allows us to have duplicate nested // elements. $has_nested = is_numeric($key); if (!$has_nested) { $output .= "$indent<$key>"; } if (is_array($value)) { if (!$has_nested) { $output .= "\n"; } $output .= ec_receipt_convert_request_to_xml($value, $depth + (is_numeric($key) ? 0 : 1)); } else { $output .= htmlentities($value, ENT_QUOTES); } if (!$has_nested) { if (is_array($value)) { $output .= $indent; } $output .= "\n"; } } return $output; } /** * Sends off a XML payment request to the given URL. * * @param $url * A string containing a fully qualified URI. * @param $headers * An array containing an HTTP header => value pair. Defaults * to array('Content-Type' => 'application/x-www-form-urlencoded'). * @param $method * A string defining the HTTP request to use. * @param $request * Associative array of data for the request. See * ec_receipt_convert_request_to_xml() for more information. * @param $retry * An integer representing how many times to retry the request in case of a * redirect. * @return * Response as returned by simplexml_load_string(). */ function ec_receipt_xml_request($url, $headers = array(), $method = 'POST', $request = NULL, $retry = 3) { if (empty($headers)) { $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); } $ret = drupal_http_request($url, array('headers' => $headers, 'method' => $method, 'data' => ec_receipt_convert_request_to_xml($request), 'max_redirects' => $retry)); return simplexml_load_string($ret->data); } /** * Implements hook_element_info(). */ function ec_receipt_element_info() { $type['credit_card'] = array( '#input' => TRUE, '#process' => array('expand_credit_card'), '#cvnshow' => FALSE, '#cvnrequired' => FALSE, '#element_validate' => array('valid_credit_card'), '#attributes' => array('class' => array('ignore-update')), ); $type['credit_card_expiry'] = array( '#input' => TRUE, '#process' => array('expand_credit_card_expiry'), '#theme_wrappers' => array('form_element'), '#field_prefix' => '
', '#field_suffix' => '
', ); $type['electronic_check'] = array( '#input' => TRUE, '#process' => array('expand_electronic_check'), '#element_validate' => array('valid_electronic_check'), ); return $type; } /* * An array of functions that are called when an element is processed. * Using this callback, modules can "register" further actions. * */ function expand_credit_card_expiry($element) { $element['expmonth'] = array( '#type' => 'textfield', '#size' => 3, '#maxlength' => 2, '#value' => isset($element['#value']['expmonth']) ? $element['#value']['expmonth'] : '', '#description' => t('MM'), '#attributes' => array('class' => array('ignore-update')), ); $element['expyear'] = array( '#type' => 'textfield', '#size' => 3, '#maxlength' => 2, '#value' => isset($element['#value']['expyear']) ? $element['#value']['expyear'] : '', '#description' => t('YY'), '#attributes' => array('class' => array('ignore-update')), ); $element['#tree'] = TRUE; return $element; } /* * An array of functions that are called when an element is processed. * Using this callback, modules can "register" further actions. * */ function expand_credit_card($element, &$form_state, &$form) { $id = implode('-', $element['#parents']); if (empty($element['#value']) && !empty($form_state['cc'][$id])) { $element['#value'] = $form_state['cc'][$id]; } elseif (!empty($element['#value']) && empty($form_state['cc'][$id])) { $form_state['cc'][$id] = $element['#value']; } $element['name'] = array( '#type' => 'textfield', '#title' => t('Cardholder name'), '#size' => 60, '#maxlength' => 100, '#default_value' => isset($element['#default_value']['name']) ? $element['#default_value']['name'] : (isset($element['#value']['name']) ? $element['#value']['name'] : ''), '#required' => TRUE, '#attributes' => array('class' => array('ignore-update')), ); if (isset($element['#cardtype']) && $element['#cardtype'] && ($options = ec_receipt_card_types())) { $element['cardtype'] = array( '#type' => 'radios', '#title' => t('CardType'), '#default_value' => isset($element['#value']['cardtype']) ? $element['#value']['cardtype'] : '', '#options' => $options, '#required' => TRUE, '#attributes' => array('class' => array('ignore-update')), '#attached' => array( 'css' => array( drupal_get_path('module', 'ec_receipt') . '/css/receipt.css', ), ), ); } $element['cardnumber'] = array( '#type' => 'textfield', '#title' => t('Credit card number'), '#value' => isset($element['#value']['cardnumber']) ? $element['#value']['cardnumber'] : '', '#size' => 20, '#maxlength' => 40, '#required' => TRUE, '#attributes' => array('class' => array('ignore-update')), ); $element['expiry'] = array( '#type' => 'credit_card_expiry', '#title' => t('Card expiry date'), '#value' => isset($element['#value']['expiry']) ? $element['#value']['expiry'] : '', '#required' => TRUE, '#attributes' => array('class' => array('ignore-update')), ); if ($element['#cvnshow'] || $element['#cvnrequired']) { $element['cvn'] = array( '#type' => 'textfield', '#title' => t('Card verification number'), '#value' => isset($element['#value']['cvn']) ? $element['#value']['cvn'] : '', '#size' => 4, '#maxlength' => 4, '#description' => t('The card verification number (cvn) is the last three or four digit number printed on the signature strip of a credit card'), '#required' => (isset($element['#cvnrequired']) ? $element['#cvnrequired'] : FALSE), '#attributes' => array('class' => array('ignore-update')), ); } $element['#required'] = FALSE; return $element; } /* * An array of functions that are called when an element is processed. * Using this callback, modules can "register" further actions. * */ function valid_credit_card(&$element, &$form_state, $form) { // Validate the card number. $checksum = 0; $element['cardnumber']['#value'] = str_replace(' ', '', $element['cardnumber']['#value']); $cardnumber = $element['cardnumber']['#value']; if ($cardnumber == '' || !preg_match('/^\d+$/', $cardnumber)) { form_error($element['cardnumber'], t('A valid Credit Card Number is required')); } else { $j = 1; for ($i = drupal_strlen($cardnumber) - 1; $i >= 0; $i--) { $calc = $cardnumber[$i] * $j; if ($calc > 9) { $checksum++; $calc -= 10; } $checksum += $calc; $j = ($j == 1 ? 2 : 1); } if ($checksum % 10 != 0) { form_error($element['cardnumber'], t('Credit Card Number is not valid. Please check number')); } } // Validate the expiry date. $month = $element['expiry']['expmonth']['#value']; $year = $element['expiry']['expyear']['#value']; $valid_2_digit = "/[0-9]{2}/"; if (empty($month) || empty($year)) { form_error($element['expiry']['expmonth'], 'Both expiry month and expiry year are required'); form_error($element['expiry']['expyear'], ' '); } elseif ((preg_match($valid_2_digit, $month) == 0) || (preg_match($valid_2_digit, $year) == 0)) { form_error($element['expiry'], t('Card expiry month and year must have two digits each')); } elseif (($expiry = strtotime("20$year-$month-1")) == -1) { form_error($element['expiry'], t('Card expiry date is not a valid date')); } elseif ($expiry < strtotime(date('Y-m-1'))) { form_error($element['expiry'], t('Card expiry date is in the past')); } // Validate the CVN. if ($element['cvn']['#value'] && !preg_match('/^\d{3,4}$/', $element['cvn']['#value'])) { form_error($element['cvn'], t('Card Verification Number is a required field, and must be numeric')); } return !is_array(form_get_errors()); } function expand_electronic_check($element, &$form_state, &$form) { $id = implode('-', $element['#parents']); if (empty($element['#value']) && !empty($form_state['electronic_check'][$id])) { $element['#value'] = $form_state['electronic_check'][$id]; } elseif (!empty($element['#value']) && empty($form_state['electronic_check'][$id])) { $form_state['electronic_check'][$id] = $element['#value']; } $options = array( 'checking' => t('Checking'), 'savings' => t('Savings'), 'businessChecking' => t('Business Checking'), ); $element['account_type'] = array( '#type' => 'select', '#title' => t('Account Type'), '#options' => $options, '#attributes' => array('class' => array('ignore-update')), ); $element['account_name'] = array( '#type' => 'textfield', '#title' => t('Name On Bank Account'), '#value' => $element['#value']['account_name'], '#size' => 20, '#maxlength' => 22, '#attributes' => array('class' => array('ignore-update')), '#required' => TRUE, ); $element['routing_number'] = array( '#type' => 'textfield', '#title' => t('Routing Number'), '#value' => $element['#value']['routing_number'], '#size' => 20, '#maxlength' => 9, '#attributes' => array('class' => array('ignore-update')), '#required' => TRUE, ); $element['account_number'] = array( '#type' => 'textfield', '#title' => t('Account Number'), '#value' => $element['#value']['account_number'], '#size' => 20, '#maxlength' => 17, '#attributes' => array('class' => array('ignore-update')), '#required' => TRUE, ); $element['bank_name'] = array( '#type' => 'textfield', '#title' => t('Name of Bank'), '#value' => $element['#value']['bank_name'], '#size' => 20, '#maxlength' => 30, '#attributes' => array('class' => array('ignore-update')), '#required' => TRUE, ); return $element; } /* * An array of functions that are called when an element is processed. * Using this callback, modules can "register" further actions. * */ function valid_electronic_check(&$element, &$form_state, $form) { // ensure we have a name on Bank Account - at least 4 characters if ( drupal_strlen($element['account_name']['#value']) < 4 ) { form_error($element['account_name'], t('Name on Bank Account is required')); } // validate the routing_number if ( drupal_strlen($element['routing_number']['#value']) != 9) { form_error($element['routing_number'], t('Routing Number is 9 digits- no dashes or spaces')); } // validate the account_number if ( drupal_strlen($element['account_number']['#value']) < 5 || drupal_strlen($element['account_number']['#value']) > 17) { form_error($element['account_number'], t('Account Number is between 5 and 17 digits- no dashes or spaces')); } // ensure we have a name of bank if ( drupal_strlen($element['bank_name']['#value']) < 4) { form_error($element['bank_name'], t('Name Bank Name is required')); } return !is_array(form_get_errors()); } /** * Implements hook_ec_txn_workflow(). */ function ec_receipt_ec_txn_workflow($txn, $orig_txn) { if (ec_store_transaction_workflow('type_code', $txn->workflow) == EC_WORKFLOW_TYPE_CANCEL) { $result = db_query("SELECT * FROM {ec_receipt_allocation} WHERE type = :type AND etid = :etid AND reversed = :reversed", array(':type' => 'transaction', ':etid' => $txn->txnid, ':reversed' => 0)) ->execute(); $amount = 0; foreach ($result as $alloc) { $amount += $alloc->amount; $receipt = ec_receipt_load($alloc->erid); ec_receipt_reverse_allocation($receipt, $alloc); } if ($amount) { drupal_set_message(t('%amount of allocation has been reversed.', array('%amount' => format_currency($amount)))); } } } /** * Implements hook_allocation_info(). */ function ec_receipt_allocation_info() { return array( 'refund' => array( 'name' => t('Refund Receipts'), 'description' => t('Allocation of refunded receipts.'), 'module' => array('ec_receipt_refund'), ), ); } /** * Implements hook_alloc_load(). */ function ec_receipt_refund_alloc_load($erid) { return $erid; } /** * Implements hook_alloc_get_currency(). */ function ec_receipt_refund_alloc_get_currency($erid) { if ($receipt = ec_receipt_load($erid)) { return $receipt->currency; } } /** * Implements hook_alloc_get_total(). */ function ec_receipt_refund_alloc_get_total($erid) { if ($receipt = ec_receipt_load($erid)) { return $receipt->amount; } } /** * Implements hook_alloc_allocation(). */ function ec_receipt_refund_alloc_allocation($erid, $balance) { if ($receipt = ec_receipt_load($erid)) { $receipt->status = RECEIPT_STATUS_REFUND_PENDING; ec_receipt_save($receipt); watchdog('ec_receipt', 'Receipt !erid refunded to customer.', array('!erid' => $erid)); } } /** * Implements hook_views_bulk_operations_object_info(). */ function ec_receipt_views_bulk_operations_object_info() { return array( 'receipt' => array( 'type' => 'receipt', 'base_table' => 'ec_receipt', 'load' => 'ec_receipt_load', 'title' => 'erid', ), ); } /** * Implements hook_action_info(). */ function ec_receipt_action_info() { return array( 'ec_receipt_action_delete' => array( 'type' => 'ec_receipt', 'label' => t('Delete receipt'), 'configurable' => FALSE, 'triggers' => array( ), ), 'ec_receipt_action_allocate' => array( 'type' => 'ec_receipt', 'label' => t('Allocate receipt'), 'configurable' => TRUE, 'triggers' => array( ), ), 'ec_receipt_action_refund' => array( 'type' => 'ec_receipt', 'label' => t('Refund receipt'), 'configurable' => FALSE, 'triggers' => array( ), ), ); } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_action_delete($receipt, $context) { return; } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_action_allocate($receipt, $context) { ec_receipt_allocate($receipt, $context['allocations']); } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_action_allocate_form($edit) { $view = views_get_view('receipt_allocation'); $view->set_display(); $view->pre_execute(); $view->execute(); $view->post_execute(); $sets = $view->style_plugin->render_grouping($view->result, $view->style_options['grouping']); $form['allocations'] = array( '#type' => 'views_node_selector', '#view' => $view, '#sets' => $sets, '#value' => array(), '#prefix' => '
', '#suffix' => '
', ); return $form; } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_action_allocate_submit($form, &$form_state) { $allocations = array(); foreach (array_filter($form_state['values']['allocations']) as $id) { $allocations[] = array( 'type' => 'transaction', 'id' => $id, ); } return array('allocations' => $allocations); } /** * @todo Please document this function. * @see http://drupal.org/node/1354 */ function ec_receipt_action_refund($receipt, $context) { if (user_access('refund receipts')) { $receipt =& $context['receipt']; $receipt->status = RECEIPT_STATUS_REFUNDED; watchdog('ec_receipt', 'Receipt %erid has been marked as refunded.', array('erid' => $receipt->erid)); ec_receipt_save($receipt); return; } elseif ($cancel_url == '%cancel-order') { $cancel_url = ''; } return $cancel_url; } /** * Implements hook_update_index(). */ function ec_receipt_update_index() { module_load_include('inc', 'ec_receipt', 'ec_receipt.admin'); $limit = (int) variable_get('search_cron_limit', 100); $result = db_query("SELECT r.erid FROM {ec_receipt} r LEFT JOIN {search_dataset} d ON d.type = 'ec_receipt' AND d.sid = r.erid WHERE d.sid IS NULL OR d.reindex <> :d.reindex ORDER BY d.reindex ASC, r.erid ASC", array(':d.reindex' => 0)) ->limit($limit) ->execute(); foreach ($result as $receipt) { $txn = ec_receipt_load($receipt->erid); $text = drupal_get_form('ec_receipt_view_form', $receipt); search_index($receipt->erid, 'ec_receipt', $text); } } /** * Return a list of credit card types. * * @param $all * Set to TRUE if you want to get all types not only the valid ones. * @return * Associative arrays with credit card types. Keys are machine reable names * and values are human readable names. */ function ec_receipt_card_types($all = FALSE) { $cardtypes = array( 'mastercard' => t('Mastercard'), 'visa' => t('VISA'), 'amex' => t('American Express'), 'discover' => t('Discover'), ); if ($all) { return $cardtypes; } $valid_cards = array_flip(variable_get('ec_receipt_valid_cards', array_keys($cardtypes))); return array_intersect_key($cardtypes, $valid_cards); } /** * create the return url for when an order has been completed. * * @param $receipt * Receipt object which has been processed by the system. * * @return * either a string or an array which can be passed to drupal_goto(). */ function ec_receipt_return_page($receipt) { global $user; $return_url = variable_get('ec_receipt_return_url', 'node'); drupal_alter('ec_receipt_return_page', $return_url, $receipt); if ($return_url == '%order-history') { $return_url = "user/$user->uid/store"; } return $return_url; } /** * create the cancel url for when an order has been canceled by the payment system. * * @param $receipt * Receipt object which has been processed by the system. * * @return * either a string or an array which can be passed to drupal_goto(). */ function ec_receipt_cancel_page($receipt, $invoice_no = NULL) { $cancel_url = variable_get('ec_receipt_cancel_url', 'node'); drupal_alter('ec_receipt_cancel_page', $return_url, $receipt); if ($cancel_url == '%cancel-order' && isset($invoice_no)) { $cancel_url = 'store/transaction/' . $invoice_no . '/cancel'; } elseif ($cancel_url == '%cancel-order') { $cancel_url = ''; } return $cancel_url; }