You are here

commerce_gc.module in Commerce GC 7

Provides Giftcard coupon bundle, Giftcard Transaction entity and basic user interface elements.

File

commerce_gc.module
View source
<?php

/**
 * @file
 * Provides Giftcard coupon bundle, Giftcard Transaction entity and basic user
 * interface elements.
 */

/*
 * Complete status: for successful, paid-in-full purchases or standalone
 * transactions against a giftcard
 */
define('COMMERCE_GC_TRANSACTION_COMPLETE_STATUS', 'complete');

/*
 * Pending status: for reserving an amount to be completed later
 */
define('COMMERCE_GC_TRANSACTION_PENDING_STATUS', 'pending');

/*
 * Void status: for transactions that are cancelled and should not be counted as
 * part of the balance
 */
define('COMMERCE_GC_TRANSACTION_VOID_STATUS', 'void');

/*
 * For payments that completed checkout but where some amount on the order
 * remains to be paid.
 */
define('COMMERCE_GC_TRANSACTION_AUTHORIZED_STATUS', 'authorized');

/*
 * Implements hook_entity_info().
 */
function commerce_gc_entity_info() {
  $entity_info['commerce_gc_transaction'] = array(
    'label' => t('Commerce giftcard transaction'),
    'plural label' => t('Commerce giftcard transactions'),
    'views controller class' => 'CommerceGCTransactionViewsController',
    'controller class' => 'CommerceGCTransactionEntityController',
    'base table' => 'commerce_gc_transaction',
    'fieldable' => FALSE,
    'locking mode' => 'pessimistic',
    'entity keys' => array(
      'id' => 'transaction_id',
      'label' => 'transaction_id',
    ),
    'access callback' => 'commerce_entity_access',
    'access arguments' => array(
      'access tag' => 'commerce_gc_transaction_access',
    ),
    'bundles' => array(
      'commerce_gc_transaction' => array(
        'label' => t('Commerce giftcard transaction'),
      ),
    ),
    'module' => 'commerce_gc',
    'permission labels' => array(
      'singular' => t('Giftcard transaction'),
      'plural' => t('Giftcard Transactions'),
    ),
  );
  return $entity_info;
}

/*
 * Implements hook_commerce_gc_transaction_status_info().
 */
function commerce_gc_commerce_gc_transaction_status_info() {
  return array(
    COMMERCE_GC_TRANSACTION_COMPLETE_STATUS => array(
      'label' => t('Complete'),
      'total' => TRUE,
    ),
    COMMERCE_GC_TRANSACTION_PENDING_STATUS => array(
      'label' => t('Pending'),
      'total' => TRUE,
    ),
    COMMERCE_GC_TRANSACTION_VOID_STATUS => array(
      'label' => t('Void'),
      'total' => FALSE,
    ),
    COMMERCE_GC_TRANSACTION_AUTHORIZED_STATUS => array(
      'label' => t('Authorized'),
      'total' => TRUE,
    ),
  );
}

/**
 * Get a list of all transaction status info or info about a particular status.
 */
function commerce_gc_transaction_statuses($name = FALSE) {
  $cache =& drupal_static(__FUNCTION__);
  if (!$cache) {
    $statuses = module_invoke_all('commerce_gc_transaction_status_info');
    foreach ($statuses as $machine_name => $status) {
      $cache[$machine_name] = $status + array(
        'machine_name' => $machine_name,
      );
    }
  }
  return isset($cache[$name]) ? $cache[$name] : $cache;
}

/**
 * Get a list of statuses, by name, that are eligible to be counted as part of
 * a giftcard's transaction balance.
 *
 * @return array
 *  List of status names
 */
function commerce_gc_balance_total_status_names() {
  return array_keys(commerce_gc_balance_total_statuses());
}

/**
 * Get all statuses in a particular state
 *
 * @param type $states
 * @return type
 */
function commerce_gc_balance_total_statuses() {
  $names = array();
  foreach (commerce_gc_transaction_statuses() as $name => $status) {
    if (!empty($status['total'])) {
      $names[] = $name;
    }
  }
  return $names;
}

/**
 * Get a list of transaction status options
 *
 * @return array
 *  Option list
 */
function commerce_gc_transaction_status_option_list() {
  $options = array();
  foreach (commerce_gc_transaction_statuses() as $name => $status) {
    $options[$name] = $status['label'];
  }
  return $options;
}

/*
 * Implements hook_commerce_cart_order_refresh().
 */
function commerce_gc_commerce_cart_order_refresh($order_wrapper) {
  $order = $order_wrapper
    ->value();
  $line_item_data = array();
  $delete_line_item_ids = array();
  $remove_coupon_ids = array();

  // Create a list of line items in the order keyed by coupon id so that if
  // necessary we can reference/delete them later if they no longer are valid.
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
    if ($line_item_wrapper->type
      ->value() == 'giftcard_use') {
      $coupon_id = $line_item_wrapper->commerce_giftcard->coupon_id
        ->value();
      $line_item_data[$coupon_id] = array(
        'line_item' => clone $line_item_wrapper,
        'delta' => $delta,
      );

      // Set giftcard line item prices to zero. We always have to recalculate the price, similar to how commerce
      // discount works.
      commerce_gc_remove_giftcard_components($line_item_wrapper->commerce_unit_price);
      commerce_gc_remove_giftcard_components($line_item_wrapper->commerce_total);
    }
  }

  // Strip out giftcard components from order total.
  commerce_gc_remove_giftcard_components($order_wrapper->commerce_order_total);

  // Add giftcard use line items if necessary.
  foreach ($order_wrapper->commerce_coupons as $delta => $coupon_wrapper) {
    if ($coupon_wrapper
      ->value() && $coupon_wrapper->type
      ->value() == 'giftcard_coupon') {
      $coupon = $coupon_wrapper
        ->value();
      $coupon_id = $coupon->coupon_id;
      commerce_order_calculate_total($order);

      // Evaluate conditions.
      if (commerce_coupon_evaluate_conditions($coupon_wrapper, $order_wrapper)) {

        // We know the balance is positive, but we need to know exactly how much
        // of the giftcard can be applied based on the order total minus the
        // amount of any line item tied to the use of this particular giftcard.
        $amount = commerce_gc_order_giftcard_amount($order_wrapper, $coupon_wrapper);
        $price = array(
          'amount' => -$amount,
          'currency_code' => commerce_default_currency(),
        );

        // Add a new line item if one does not already exist.
        if (!isset($line_item_data[$coupon_wrapper->coupon_id
          ->value()])) {
          $new_line_item = commerce_gc_add_line_item($order_wrapper, $coupon_wrapper, $price);
          $new_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $new_line_item);
          $new_delta = $order_wrapper->commerce_line_items
            ->count();

          // Track this line item because we might have to delete it later.
          $line_item_data[$coupon_wrapper->coupon_id
            ->value()] = array(
            'line_item' => $new_line_item_wrapper,
            'delta' => $new_delta,
          );
        }
        else {
          $line_item_wrapper = $line_item_data[$coupon_id]['line_item'];
          commerce_gc_line_item_set_price($price, $line_item_wrapper, $coupon_wrapper);
          $line_item_wrapper
            ->save();
        }

        // Make sure this line item doesn't get deleted later, as long as its amount is not zero.
        if ($price['amount'] < 0) {
          unset($line_item_data[$coupon_id]);
        }
        else {
          $remove_coupon_ids[] = $coupon_id;
        }
      }
      else {
        $remove_coupon_ids[] = $coupon_id;
      }
    }
  }

  // Remove coupons and line items that no longer apply.
  foreach ($line_item_data as $coupon_id => $data) {

    // It is not safe to use the offsetUnset pattern because this is not
    // compatible with other modules that have already made changes to the
    // line item list in this fashion - namely Commerce Discount.
    $lang = field_language('commerce_order', $order, 'commerce_line_items');
    $delta = $line_item_data[$coupon_id]['delta'];
    unset($order->commerce_line_items[$lang][$delta]);
    if ($line_item_data[$coupon_id]['line_item']
      ->value()) {
      $delete_line_item_ids[] = $line_item_data[$coupon_id]['line_item']->line_item_id
        ->value();
    }
  }

  // Finally get rid of all line items that need to be removed.
  commerce_line_item_delete_multiple(array_values($delete_line_item_ids));

  // Get rid of orphaned coupon references.
  foreach ($remove_coupon_ids as $coupon_id) {
    $coupon = commerce_coupon_load($coupon_id);

    // Remove coupons from the order.
    commerce_coupon_remove_coupon_from_order($order, $coupon, FALSE);
  }
}

/*
 * Implements hook_commerce_discount_remove_price_component_alter().
 */
function commerce_gc_commerce_discount_remove_price_component_alter($component, &$remove) {
  if ($component['name'] == 'giftcard') {
    $remove = TRUE;
  }
}

/**
 * Remove giftcard components from a given price and recalculate the total.
 *
 * @param $price_wrapper
 *   Wrapped commerce price.
 */
function commerce_gc_remove_giftcard_components($price_wrapper) {
  $data = $price_wrapper->data
    ->value();
  $component_removed = FALSE;

  // Remove price components belonging to giftcards.
  foreach ($data['components'] as $key => $component) {
    if ($component['name'] == 'giftcard') {
      unset($data['components'][$key]);
      $component_removed = TRUE;
    }
  }

  // Don't alter the price components if no components were removed.
  if (!$component_removed) {
    return;
  }

  // Re-save the price without the giftcards (if existed).
  $price_wrapper->data
    ->set($data);

  // Re-set the total price.
  $total = commerce_price_component_total($price_wrapper
    ->value());
  $price_wrapper->amount
    ->set($total['amount']);
}

/**
 * Determine the amount of a particular giftcard that is eligible to apply to
 * apply against an order. Uses a static cache.
 *
 * @param EntityDrupalWrapper $order_wrapper
 * @param EntityDrupalWrapper $coupon_wrapper
 * @param type $exclude_amount
 * @param type $reset
 * @return type
 */
function commerce_gc_order_giftcard_amount(EntityDrupalWrapper $order_wrapper, EntityDrupalWrapper $coupon_wrapper, $reset = FALSE) {
  $cache =& drupal_static(__FUNCTION__);
  $coupon_id = $coupon_wrapper->coupon_id
    ->value();
  if ($reset || !isset($cache[$coupon_id])) {
    $order_amount = $order_wrapper->commerce_order_total->amount
      ->value();

    // Calculate the amount of the giftcard that may be applied.
    $balance_amount = commerce_gc_giftcard_balance($coupon_id);
    $cache[$coupon_id] = $order_amount < $balance_amount ? $order_amount : $balance_amount;
  }
  return $cache[$coupon_id];
}

/**
 * Implements hook_query_TAG_alter().
 *
 * Derived from
 * commerce_payment_query_commerce_payment_transaction_access_alter().
 */
function commerce_gc_query_commerce_gc_transaction_access_alter(QueryAlterableInterface $query) {

  // Read the meta-data from the query.
  if (!($account = $query
    ->getMetaData('account'))) {
    global $user;
    $account = $user;
  }

  // If the user is allowed to administrate giftcards, stop here.
  if (user_access('administer giftcard transactions')) {
    return;
  }

  // If the user is not administrative-level, he/she must own the coupon related
  // to a given transaction record.
  if (user_access('view own giftcard transactions')) {
    $tables =& $query
      ->getTables();

    // Look for an existing commerce_coupon table as well as the transaction
    // table.
    foreach ($tables as $table) {
      if ($table['table'] == 'commerce_coupon') {
        $coupon_alias = $table['alias'];
      }
      else {
        if ($table['table'] == 'commerce_gc_transaction') {
          $transaction_alias = $table['alias'];
        }
      }
    }

    // If not found, join to the coupon table and check access on the coupon.
    // Otherwise, we know that Commerce Coupon has already added its access
    // checks.
    if (!isset($coupon_alias) && isset($transaction_alias)) {
      $coupon_alias = $query
        ->innerJoin('commerce_coupon', 'cc', '%alias.coupon_id = ' . $transaction_alias . '.coupon_id');

      // Look up access on the coupon.
      commerce_coupon_apply_access_query_substitute($query, $coupon_alias, $account);
    }
  }
  else {

    // The user may not view the results of this query.
    $query
      ->condition('1=0');
  }
}

/*
 * Implements hook_menu().
 */
function commerce_gc_menu() {
  $items['giftcards/%commerce_coupon/transactions/%commerce_gc_transaction/delete'] = array(
    'title' => 'Delete giftcard transaction',
    'access arguments' => array(
      'delete giftcard transactions',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_gc_delete_transaction_form',
      1,
      3,
    ),
  );
  return $items;
}

/*
 * Delete transaction confirm form
 */
function commerce_gc_delete_transaction_form($form, &$form_state, $coupon, $transaction) {
  $form_state['commerce_gc_transaction'] = $transaction;
  $form_state['commerce_coupon'] = $coupon;
  $form['#submit'][] = 'commerce_gc_delete_transaction_form_submit';
  $form = confirm_form($form, t('Are you sure you want to delete this transaction?'), 'giftcards/' . $coupon->coupon_id . '/transactions/' . $transaction->transaction_id);
  return $form;
}

/*
 * Delete transaction form submit
 */
function commerce_gc_delete_transaction_form_submit(&$form, &$form_state) {
  commerce_gc_transaction_delete($form_state['commerce_gc_transaction']->transaction_id);
  drupal_set_message(t('Giftcard transaction successfully deleted.'));
  $form_state['redirect'] = 'giftcards/' . $form_state['commerce_coupon']->coupon_id . '/transactions/';
}

/*
 * Implements hook_permission().
 */
function commerce_gc_permission() {
  return array(
    'administer giftcard transactions' => array(
      'title' => t('Administer giftcard transactions'),
    ),
    'administer giftcards' => array(
      'title' => t('Administer giftcards'),
    ),
    'view own giftcard transactions' => array(
      'title' => t('View own giftcard transactions'),
    ),
    'create new giftcard transactions' => array(
      'title' => t('Create new giftcard transactions'),
    ),
    'delete giftcard transactions' => array(
      'title' => t('Delete giftcard transactions'),
    ),
  );
}

/*
 * Implements hook_commerce_coupon_type_info().
 */
function commerce_gc_commerce_coupon_type_info() {
  $types['giftcard_coupon'] = array(
    'label' => t('Giftcard'),
    'type' => 'giftcard_coupon',
  );
  return $types;
}

/*
 * Implements hook_commerce_price_component_type_info().
 */
function commerce_gc_commerce_price_component_type_info() {
  $types['giftcard'] = array(
    'title' => t('Giftcard'),
  );
  return $types;
}

/*
 * Implements hook_commerce_line_item_type_info().
 */
function commerce_gc_commerce_line_item_type_info() {
  $types['giftcard_use'] = array(
    'type' => 'giftcard_use',
    'name' => t('Giftcard use'),
    'description' => t('Line item for giftcard usage.'),
    'add_form_submit_value' => t('Add giftcard'),
    'base' => 'commerce_gc_line_item',
  );
  return $types;
}

/*
 * Line item type callback: giftcard use configuration
 */
function commerce_gc_line_item_configuration() {
  field_info_cache_clear();
  $fields = field_info_fields();
  $instances = field_info_instances();

  /*
   * Line item: giftcard reference
   */
  if (empty($fields['commerce_giftcard'])) {

    // Create giftcard reference field
    $field = array(
      'settings' => array(
        'target_type' => 'commerce_coupon',
        'handler' => 'base',
        'handler_settings' => array(
          'target_bundles' => array(
            'product_display' => 'giftcard_coupon',
          ),
        ),
      ),
      'field_name' => 'commerce_giftcard',
      'type' => 'entityreference',
      'locked' => TRUE,
      'cardinality' => '1',
    );
    field_create_field($field);
  }
  if (empty($instances['commerce_line_item']['giftcard_use']['commerce_giftcard'])) {
    $instance = array(
      'label' => t('Giftcard'),
      'widget' => array(
        'type' => 'entityreference_autocomplete',
        'weight' => '9',
        'settings' => array(
          'match_operator' => 'CONTAINS',
          'size' => 60,
          'path' => '',
        ),
      ),
      'field_name' => 'commerce_giftcard',
      'entity_type' => 'commerce_line_item',
      'bundle' => 'giftcard_use',
      'default_value' => NULL,
    );
    field_create_instance($instance);
  }
}

/*
 * Line item callback: title
 */
function commerce_gc_line_item_title() {
  return t('Giftcard use');
}

/*
 * Line item type callback: giftcard use add form
 */
function commerce_gc_line_item_add_form($element, &$form_state) {
  $form['code'] = array(
    '#type' => 'textfield',
    '#title' => t('Giftcard code'),
    '#description' => t('Enter a giftcard code to redeem.'),
    '#required' => TRUE,
  );
  $form['amount'] = array(
    '#type' => 'textfield',
    '#title' => t('Amount') . ' (' . commerce_default_currency() . ')',
    '#description' => t('Enter an amount to charge this giftcard.'),
  );

  // We do not allow the line item terminal to trigger transaction inserts if
  // the order is in the shopping cart state because orders are not supposed to
  // record transactions until checkout is complete.
  if (commerce_cart_order_is_cart($form_state['commerce_order'])) {
    $form['no_transaction'] = array(
      '#type' => 'markup',
      '#prefix' => '<div>',
      '#markup' => t('Since the order is in the shopping cart state, adding this giftcard line item will not generate a transaction.
        !link', array(
        '!link' => l('Manage giftcard transactions directly', 'admin/commerce/coupons/giftcards'),
      )),
      '#suffix' => '</div>',
    );
  }
  return $form;
}

/*
 * Line item type callback: giftcard use add form
 */
function commerce_gc_line_item_add_form_submit($line_item, $element, &$form_state, $form) {

  // No need to go further if there are errors.
  if (form_get_errors()) {
    return;
  }

  // Make sure amount is either empty or a number greater than zero.
  $decimal_amount = $element['actions']['amount']['#value'];
  if ($decimal_amount && (!is_numeric($decimal_amount) || $decimal_amount < 0)) {
    return t('You have entered an invalid amount.');
  }
  $amount = commerce_currency_decimal_to_amount($decimal_amount, commerce_default_currency());

  // Make sure the giftcard exists.
  $code = $element['actions']['code']['#value'];
  $coupon = commerce_coupon_load_by_code($code);
  if (!$coupon || $coupon->type != 'giftcard_coupon') {
    return t('This giftcard does not exist.');
  }

  // Make sure there is not already a giftcard use line item referencing this
  // code.
  $order = $form_state['commerce_order'];
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  foreach ($order_wrapper->commerce_line_items as $order_line_item_wrapper) {
    if ($order_line_item_wrapper->type
      ->value() == 'giftcard_use' && $order_line_item_wrapper->commerce_giftcard->coupon_id
      ->value() == $coupon->coupon_id) {
      return t('You may not add the same giftcard code more than once per order.');
    }
  }

  // Make sure it has a positive balance.
  $balance = commerce_gc_giftcard_balance($coupon->coupon_id);
  if ($balance <= 0) {
    return t('This balance on this giftcard is not greater than zero.');
  }

  // Make sure that the amount entered does not exceed the balance
  if ($amount && $amount > $balance) {
    $balance_display = commerce_currency_format($balance, commerce_default_currency());
    return t('You have entered an amount greater than the balance on this card. Card balance is @balance', array(
      '@balance' => $balance_display,
    ));
  }
  commerce_order_calculate_total($order);
  $order_total = $order_wrapper->commerce_order_total->amount
    ->value();
  $ceiling_amount = $order_total < $balance ? $order_total : $balance;
  $line_item_amount = $amount ? $amount : $ceiling_amount;

  // This can happen if the order total is zero.
  if (!$line_item_amount) {
    return t('You cannot add a zero amount giftcard use line item.');
  }
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);

  // Set the giftcard reference
  $line_item_wrapper->commerce_giftcard = $coupon;

  // Set the price
  $price = array(
    'amount' => -$line_item_amount,
    'currency_code' => commerce_default_currency(),
    'data' => array(),
  );
  $line_item_wrapper->commerce_unit_price->amount = -$line_item_amount;
  $line_item_wrapper->commerce_unit_price->currency_code = commerce_default_currency();
  $base_price = array(
    'amount' => 0,
    'currency_code' => commerce_default_currency(),
    'data' => array(),
  );
  $line_item_wrapper->commerce_unit_price->data = commerce_price_component_add($base_price, $component_title, $price, TRUE);
}

/**
 * Write a new commerce giftcard transaction record. Uses a database transaction
 * to ensure balance integrity.
 *
 * @see CommerceGCTransactionEntityController::save().
 *
 * @param type $coupon_id
 * @param type $amount
 * @return type
 */
function commerce_gc_transaction_write($coupon_id, $amount, $status = 'complete') {
  $values = array(
    'coupon_id' => $coupon_id,
    'amount' => $amount,
    'status' => $status,
  );
  $transaction = commerce_gc_transaction_new($values);

  // Insert record
  commerce_gc_transaction_save($transaction);
  return !empty($transaction->transaction_id) ? $transaction->transaction_id : NULL;
}

/**
 * Create a stub giftcard transaction entity.
 *
 * @return type
 */
function commerce_gc_transaction_new($values) {
  return entity_get_controller('commerce_gc_transaction')
    ->create($values);
}

/**
 * Save a giftcard transaction entity.
 *
 * @return type
 */
function commerce_gc_transaction_save($transaction) {
  return entity_get_controller('commerce_gc_transaction')
    ->save($transaction);
}

/**
 * Load a giftcard transaction entity
 *
 * @param type $transaction_id
 * @param type $reset
 * @return type
 */
function commerce_gc_transaction_load($transaction_id, $reset = FALSE) {
  $transactions = commerce_gc_transaction_load_multiple(array(
    $transaction_id,
  ), array(), $reset);
  return reset($transactions);
}

/**
 * Load multiple giftcard transaction entities based on certain conditions.
 *
 * @param $commerce_coupon_ids
 *   An array of coupon IDs.
 * @param $conditions
 *   An array of conditions to match against the {commerce_coupon} table.
 * @param $reset
 *   A boolean indicating that the internal cache should be reset.
 * @return
 *   An array of coupon objects, indexed by coupon id.
 *
 * @see entity_load()
 */
function commerce_gc_transaction_load_multiple($transaction_ids = array(), $conditions = array(), $reset = FALSE) {
  if (empty($transaction_ids) && empty($conditions)) {
    return array();
  }
  return entity_load('commerce_gc_transaction', $transaction_ids, $conditions, $reset);
}

/**
 * Deletes a giftcard transaction by ID.
 *
 * @param $product_id
 *   The ID of the product to delete.
 *
 * @return
 *   TRUE on success, FALSE otherwise.
 */
function commerce_gc_transaction_delete($transaction_id) {
  return commerce_gc_transaction_delete_multiple(array(
    $transaction_id,
  ));
}

/**
 * Deletes giftcard transaction by ID.
 *
 * @param $transaction_ids
 *   An array of product IDs to delete.
 *
 * @return
 *   TRUE on success, FALSE otherwise.
 */
function commerce_gc_transaction_delete_multiple($transaction_ids) {
  return entity_get_controller('commerce_gc_transaction')
    ->delete($transaction_ids);
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function commerce_gc_form_commerce_coupon_type_settings_form_alter(&$form, &$form_state) {
  $form['commerce_gc_disable_empty_cards'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use cron to disable giftcards with no value remaining.'),
    '#default_value' => variable_get('commerce_gc_disable_empty_cards', FALSE),
  );
}

/*
 * Implements hook_form_FORM_ID_alter().
 */
function commerce_gc_form_commerce_order_ui_order_form_alter(&$form, &$form_state) {

  // Store the original total of each line item so that we can compute a delta
  // when we write the transaction. Only do this once to capture just the line
  // items that were previously saved in the db and thus have transactions
  // written already.
  $order = $form_state['commerce_order'];
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  if (!isset($form_state['original_gc_line_items'])) {
    $form_state['original_gc_line_items'] = array();
    foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
      if ($line_item_wrapper
        ->value() && $line_item_wrapper->type
        ->value() == 'giftcard_use') {
        $coupon_id = $line_item_wrapper->commerce_giftcard->coupon_id
          ->value();
        if ($line_item_wrapper->type
          ->value() == 'giftcard_use') {
          $form_state['original_gc_line_items'][$coupon_id]['amount'] = $line_item_wrapper->commerce_unit_price->amount
            ->value();
        }
      }
    }
  }

  // Add custom submit/validate handlers for recording transactions.
  $form['actions']['submit']['#validate'][] = 'commerce_gc_validate_order_form_giftcard_transactions';
  $form['actions']['submit']['#submit'][] = 'commerce_gc_submit_order_form_giftcard_transactions';
}

/*
 * Implements hook_form_alter().
 */
function commerce_gc_form_alter(&$form, &$form_state, $form_id) {
  if (strpos($form_id, 'commerce_checkout_form') === 0 && !empty($form['commerce_coupon'])) {

    // Change labels for coupon input pane.
    $form['commerce_coupon']['#title'] = t('Coupons and giftcards');
    $form['commerce_coupon']['coupon_code']['#description'] = t('Enter a coupon or giftcard code here.');
    $form['commerce_coupon']['coupon_code']['#title'] = t('Code');
    $form['commerce_coupon']['coupon_add']['#value'] = t('Add');
  }
}

/*
 * Implements hook_commerce_coupon_final_checkout_transaction_rollback().
 */
function commerce_gc_commerce_coupon_final_checkout_transaction_rollback($transaction_id) {
  commerce_gc_transaction_change_status(array(
    $transaction_id,
  ), array(
    COMMERCE_GC_TRANSACTION_PENDING_STATUS,
  ), COMMERCE_GC_TRANSACTION_VOID_STATUS);
}

/*
 * Implements hook_commerce_coupon_final_checkout_validate().
 */
function commerce_gc_commerce_coupon_final_checkout_validate($form, $form_state, EntityDrupalWrapper $order_wrapper) {
  $transaction_ids = array();

  // If the form was submitted via the continue button and there are no errors.
  foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
    if ($line_item_wrapper
      ->value() && $line_item_wrapper->type
      ->value() == 'giftcard_use') {
      $coupon = $line_item_wrapper->commerce_giftcard
        ->value();

      // Do not save transactions for giftcards with zero amount.
      if ($coupon) {

        // Attempt to write a pending transaction. This will be set to
        // complete when checkout is complete. This will fail if the balance is
        // too low.
        $transaction_id = commerce_gc_transaction_write($coupon->coupon_id, $line_item_wrapper->commerce_unit_price->amount
          ->value(), COMMERCE_GC_TRANSACTION_PENDING_STATUS);
        if (!$transaction_id) {
          form_set_error('', t('Invalid coupon amount, please try again.'));
        }
        else {

          // Remember which transactions we created so that we can roll them back
          // if the form returns with errors. This is separate from the order
          // data record below because we do not want to cancel pending
          // transactions unless the form is actually being rebuilt from this
          // submission.
          $transaction_ids[] = $transaction_id;
        }
      }
    }
  }
  if (!empty($transaction_ids)) {

    // Also store the transaction ids in the order data so the checkout
    // complete hook knows what to update.
    $order = $order_wrapper
      ->value();
    $order->data['giftcard_transaction_ids'] = $transaction_ids;
    $order_wrapper
      ->save();
  }
  return $transaction_ids;
}

/**
 * Given a set of transaction ids, set any that match a certain status to a
 * target status.
 *
 * @param type $order
 * @param type $status
 */
function commerce_gc_transaction_change_status($transaction_ids, $statuses, $target_status) {
  $transactions = commerce_gc_transaction_load_multiple($transaction_ids);

  // Save each individually so that the entity save controllers are triggered
  // in case we implement more advanced logging later. There should always be
  // a very small number of items here so performance is not an issue.
  foreach ($transactions as $transaction) {
    if (in_array($transaction->status, $statuses)) {
      $transaction->status = $target_status;
      commerce_gc_transaction_save($transaction);
    }
  }
}

/*
 * Form validate callback: validate giftcard use line item unit prices before
 * recording transactions
 */
function commerce_gc_validate_order_form_giftcard_transactions(&$form, &$form_state) {

  // Check if this using an inline entity form
  if (isset($form_state['inline_entity_form'])) {

    // Since line items are locked, we can rely on first entry
    $ief = reset($form_state['inline_entity_form']);
    $line_items = $ief['entities'];
  }
  else {
    $order = $form_state['commerce_order'];
    $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
    $line_items = $order_wrapper->commerce_line_items;
  }

  // Make sure that any giftcard line items do not exceed the current balance on
  // their respective coupon entities.
  foreach ($line_items as $line_item_wrapper) {

    // Double check we have a wrapper
    if (!$line_item_wrapper instanceof EntityMetadataWrapper) {
      $line_item_wrapper = entity_metadata_wrapper("commerce_line_item", $line_item_wrapper['entity']);
    }
    if ($line_item_wrapper
      ->value() && $line_item_wrapper->type
      ->value() == 'giftcard_use') {
      $unit_amount = $line_item_wrapper->commerce_unit_price->amount
        ->value();
      $coupon = $line_item_wrapper->commerce_giftcard
        ->value();

      // Giftcard use unit prices must be negative.
      if ($unit_amount === 0) {
        form_set_error('', t('The line item amount for giftcard @code cannot be zero. Try removing it instead.', array(
          '@code' => $coupon->code,
        )));
      }
      if ($unit_amount > 0) {
        form_set_error('', t('The line item amount for giftcard @code must be negative.', array(
          '@code' => $coupon->code,
        )));
      }
    }
  }
}

/*
 * Form submit callback: record giftcard transactions
 */
function commerce_gc_submit_order_form_giftcard_transactions(&$form, &$form_state) {
  $order = $form_state['commerce_order'];
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $save = FALSE;
  foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
    if ($line_item_wrapper->type
      ->value() == 'giftcard_use') {

      // Remember which giftcards are still present.
      $coupon_id = $line_item_wrapper->commerce_giftcard->coupon_id
        ->value();
      $existing_coupon_ids[$coupon_id] = TRUE;

      // Compute the amount that should be written for this transaction by using
      // the original value as an offset.
      $unit_price_amount = $line_item_wrapper->commerce_unit_price->amount
        ->value();
      if (isset($form_state['original_gc_line_items'][$coupon_id]['amount'])) {
        $offset_amount = $form_state['original_gc_line_items'][$coupon_id]['amount'];
        $transaction_amount = -($offset_amount - $unit_price_amount);
      }
      else {
        $transaction_amount = $unit_price_amount;
      }

      // As long as the order is not a shopping cart, record a transaction.
      // Shopping cart orders go through a separate refresh process and do not
      // create transactions for their giftcard use line items until checkout is
      // complete.
      if ($transaction_amount && !commerce_cart_order_is_cart($order)) {
        commerce_gc_transaction_write($coupon_id, $transaction_amount);
      }

      // Add the coupon to the order if it is not there already.
      if (!in_array($coupon_id, $order_wrapper->commerce_coupons
        ->raw())) {
        $order_wrapper->commerce_coupons[] = $coupon_id;
        $save = TRUE;
      }
    }
  }
  $remove_coupon_ids = array();

  // Handle giftcard line items that have been removed.
  foreach ($form_state['original_gc_line_items'] as $coupon_id => $original_line_item_data) {
    if (!isset($existing_coupon_ids[$coupon_id])) {

      // Create a transaction to reverse the missing line item's total, unless
      // the line item was never associated with a transaction.
      if (!commerce_cart_order_is_cart($order)) {
        commerce_gc_transaction_write($coupon_id, -$original_line_item_data['amount']);
      }
      $remove_coupon_ids[] = $coupon_id;
    }
  }

  // Remove giftcard coupon from order if giftcard's related line item was
  // removed.
  foreach ($order_wrapper->commerce_coupons
    ->raw() as $delta => $coupon_id) {
    if (in_array($coupon_id, $remove_coupon_ids)) {
      $order_wrapper->commerce_coupons
        ->offsetUnset($delta);
      $save = TRUE;
    }
  }
  if ($save) {
    commerce_order_save($order);
  }
}

/**
 * Load giftcard that belong to a certain user.
 *
 * @param type $uid
 * @param type $active
 * @return type
 */
function commerce_gc_load_user_giftcards($uid, $active = TRUE) {
  $coupons = array();
  $query = new EntityFieldQuery();
  $results = $query
    ->entityCondition('entity_type', 'commerce_coupon')
    ->entityCondition('bundle', 'giftcard_coupon')
    ->fieldCondition('commerce_coupon_recipient', 'target_id', $uid)
    ->propertyCondition('status', $active)
    ->execute();
  if (!empty($results['commerce_coupon'])) {
    $coupons = commerce_coupon_load_multiple(array_keys($results['commerce_coupon']));
  }
  return $coupons;
}

/*
 * Implements hook_commerce_coupon_value_display_alter().
 */
function commerce_gc_commerce_coupon_value_display_alter(&$text, $coupon, $order) {
  if ($coupon->type == 'giftcard_coupon') {
    $coupon_wrapper = entity_metadata_wrapper('commerce_coupon', $coupon);
    $name = $coupon_wrapper->commerce_gc_name
      ->value();
    $text = $name ? $name : t('Giftcard');
  }
}

/**
 * Creates a giftcard use line item on the provided order.
 *
 * @param EntityDrupalWrapper $order_wrapper
 *   The wrapped order entity.
 * @param string $discount_name
 *   The name of the discount being applied.
 * @param array $amount
 *   The discount amount price array (amount, currency_code).
 */
function commerce_gc_add_line_item(EntityDrupalWrapper $order_wrapper, EntityDrupalWrapper $coupon_wrapper, $price) {

  // Create a new line item.
  $line_item = entity_create('commerce_line_item', array(
    'type' => 'giftcard_use',
    'order_id' => $order_wrapper->order_id
      ->value(),
    'quantity' => 1,
  ));
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);

  // Set a reference to the coupon
  $line_item_wrapper->commerce_giftcard = $coupon_wrapper
    ->value();

  // Set the giftcard line item price.
  commerce_gc_line_item_set_price($price, $line_item_wrapper, $coupon_wrapper);

  // Save the line item and add it to the order.
  $line_item_wrapper
    ->save();

  // The wrapper "set" pattern breaks down because of the way Discount module
  // rebases line items during order refresh, so we manipulate the entity
  // directly. See commerce_gc_commerce_cart_order_refresh() for a similar
  // pattern.
  $order = $order_wrapper
    ->value();
  $lang = field_language('commerce_order', $order, 'commerce_line_items');
  $order->commerce_line_items[$lang][] = array(
    'line_item_id' => $line_item->line_item_id,
  );
  return $line_item;
}

/**
 * Set the price of a giftcard line item.
 *
 * @param array $price
 * @param EntityDrupalWrapper $line_item_wrapper
 * @param EntityDrupalWrapper $coupon_wrapper
 */
function commerce_gc_line_item_set_price($price, EntityDrupalWrapper $line_item_wrapper, EntityDrupalWrapper $coupon_wrapper) {

  // Initialize the line item unit price.
  $line_item_wrapper->commerce_unit_price->amount = $price['amount'];
  $line_item_wrapper->commerce_unit_price->currency_code = $price['currency_code'];

  // Reset the data array of the line item total field to only include a
  // base price component, set the currency code from the order.
  $base_price = array(
    'amount' => 0,
    'currency_code' => $price['currency_code'],
    'data' => array(),
  );
  $component_title = $coupon_wrapper->commerce_gc_name
    ->value() ? $coupon_wrapper->commerce_gc_name
    ->value() : 'giftcard';

  // Add some data elements to the price
  $price['data'] = array(
    'giftcard_component_title' => $component_title,
  );

  // Set components and save.
  $line_item_wrapper->commerce_unit_price->data = commerce_price_component_add($base_price, 'giftcard', $price, TRUE);
}

/*
 * Implements hook_commerce_price_formatted_components_alter().
 */
function commerce_gc_commerce_price_formatted_components_alter(&$components, $price, $entity) {

  // Similar to the implementation in Commerce Discount.
  if (isset($price['data']['components'])) {

    // Loop into price components and alter the component title if the giftcard
    // component label is found.
    foreach ($price['data']['components'] as $component) {
      if (!isset($component['price']['data']['giftcard_component_title'])) {
        continue;
      }
      $components[$component['name']]['title'] = $component['price']['data']['giftcard_component_title'];
    }
  }
}

/**
 * Compute the balance for a particular giftcard coupon.
 *
 * @param type $coupon_id
 * @return type
 */
function commerce_gc_giftcard_balance($coupon_id, $for_update = FALSE) {
  $coupon = commerce_coupon_load($coupon_id);
  if ($coupon) {
    $query = db_select('commerce_gc_transaction', 'c');
    $query
      ->addExpression('SUM(c.amount)', 'balance');
    $query
      ->condition('c.coupon_id', $coupon_id)
      ->condition('status', commerce_gc_balance_total_statuses());
    if ($for_update) {
      $query
        ->forUpdate();
    }
    $balance = $query
      ->execute()
      ->fetchCol();
    return $balance ? reset($balance) : 0;
  }
}

/*
 * Implements hook_commerce_coupon_insert().
 */
function commerce_gc_commerce_coupon_insert($coupon) {
  if ($coupon->type == 'giftcard_coupon') {
    $coupon_wrapper = entity_metadata_wrapper('commerce_coupon', $coupon);

    // Write a new transaction record for the coupon
    commerce_gc_transaction_write($coupon->coupon_id, $coupon_wrapper->commerce_gc_value->amount
      ->value());
  }
}

/*
 * Implements hook_views_api().
 */
function commerce_gc_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'commerce_gc') . '/includes/views',
  );
}

/*
 * Entity metadata property getter: coupons
 */
function commerce_gc_coupon_properties($coupon, $options, $name) {
  switch ($name) {
    case 'giftcard_balance':
      $amount = commerce_gc_giftcard_balance($coupon->coupon_id);
      return array(
        'amount' => $amount,
        'currency_code' => commerce_default_currency(),
        'data' => array(),
      );
  }
}

/*
 * Simple add transaction form
 */
function commerce_gc_add_transaction_action_form_simple($form, &$form_state, $coupon_id) {
  $form['add_transaction'] = array(
    '#type' => 'fieldset',
    '#title' => t('Add transaction'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['add_transaction']['amount'] = array(
    '#type' => 'textfield',
    '#title' => t('Amount (@currency)', array(
      '@currency' => commerce_default_currency(),
    )),
    '#description' => t('Provide a negative number for a debit and positive for credit.'),
    '#element_validate' => array(
      'element_validate_number',
    ),
    '#size' => 5,
  );
  $form['add_transaction']['status'] = array(
    '#type' => 'select',
    '#title' => t('Status'),
    '#description' => t('Select the transaction status'),
    '#default_value' => 'complete',
    '#options' => commerce_gc_transaction_status_option_list(),
  );
  $form['add_transaction']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  $form_state['coupon_id'] = (int) $coupon_id;
  return $form;
}

/*
 * Simple add transaction form submit
 */
function commerce_gc_add_transaction_action_form_simple_submit(&$form, &$form_state) {
  if (!empty($form_state['values']['amount'])) {
    $amount = commerce_currency_decimal_to_amount($form_state['values']['amount'], commerce_default_currency());
    commerce_gc_transaction_write($form_state['coupon_id'], $amount, $form_state['values']['status']);
    drupal_set_message(t('Transaction saved.'));
  }
}

/*
 * Implements hook_commerce_coupon_condition_component_alter().
 */
function commerce_gc_commerce_coupon_condition_component_alter($rule, $coupon_type) {
  if ($coupon_type == 'giftcard_coupon') {

    // Add a balance check condition to the coupon rules.
    $rule
      ->condition('commerce_gc_giftcard_minimum_balance', array(
      'commerce_coupon:select' => 'commerce_coupon',
    ));
  }
}

/**
 * Implements hook_cron().
 */
function commerce_gc_cron() {
  if (variable_get('commerce_gc_disable_empty_cards', FALSE)) {

    // Despite using a sum and subselect this query is extremely fast.
    $query = db_update('commerce_coupon');
    $query
      ->fields(array(
      'status' => 0,
    ));
    $query
      ->where('(SELECT SUM(amount) AS balance FROM {commerce_gc_transaction} t WHERE t.coupon_id = commerce_coupon.coupon_id AND status IN (:status)) = 0', array(
      ':status' => commerce_gc_balance_total_statuses(),
    ));
    $query
      ->condition('type', 'giftcard_coupon');
    $query
      ->condition('status', 1);
    $num_updated = $query
      ->execute();
    if ($num_updated) {
      watchdog('commerce_gc', 'Disabled @num giftcards with no remaining balance.', array(
        '@num' => $num_updated,
      ));
    }
  }
}

Functions

Namesort descending Description
commerce_gc_add_line_item Creates a giftcard use line item on the provided order.
commerce_gc_add_transaction_action_form_simple
commerce_gc_add_transaction_action_form_simple_submit
commerce_gc_balance_total_statuses Get all statuses in a particular state
commerce_gc_balance_total_status_names Get a list of statuses, by name, that are eligible to be counted as part of a giftcard's transaction balance.
commerce_gc_commerce_cart_order_refresh
commerce_gc_commerce_coupon_condition_component_alter
commerce_gc_commerce_coupon_final_checkout_transaction_rollback
commerce_gc_commerce_coupon_final_checkout_validate
commerce_gc_commerce_coupon_insert
commerce_gc_commerce_coupon_type_info
commerce_gc_commerce_coupon_value_display_alter
commerce_gc_commerce_discount_remove_price_component_alter
commerce_gc_commerce_gc_transaction_status_info
commerce_gc_commerce_line_item_type_info
commerce_gc_commerce_price_component_type_info
commerce_gc_commerce_price_formatted_components_alter
commerce_gc_coupon_properties
commerce_gc_cron Implements hook_cron().
commerce_gc_delete_transaction_form
commerce_gc_delete_transaction_form_submit
commerce_gc_entity_info
commerce_gc_form_alter
commerce_gc_form_commerce_coupon_type_settings_form_alter Implements hook_form_FORM_ID_alter().
commerce_gc_form_commerce_order_ui_order_form_alter
commerce_gc_giftcard_balance Compute the balance for a particular giftcard coupon.
commerce_gc_line_item_add_form
commerce_gc_line_item_add_form_submit
commerce_gc_line_item_configuration
commerce_gc_line_item_set_price Set the price of a giftcard line item.
commerce_gc_line_item_title
commerce_gc_load_user_giftcards Load giftcard that belong to a certain user.
commerce_gc_menu
commerce_gc_order_giftcard_amount Determine the amount of a particular giftcard that is eligible to apply to apply against an order. Uses a static cache.
commerce_gc_permission
commerce_gc_query_commerce_gc_transaction_access_alter Implements hook_query_TAG_alter().
commerce_gc_remove_giftcard_components Remove giftcard components from a given price and recalculate the total.
commerce_gc_submit_order_form_giftcard_transactions
commerce_gc_transaction_change_status Given a set of transaction ids, set any that match a certain status to a target status.
commerce_gc_transaction_delete Deletes a giftcard transaction by ID.
commerce_gc_transaction_delete_multiple Deletes giftcard transaction by ID.
commerce_gc_transaction_load Load a giftcard transaction entity
commerce_gc_transaction_load_multiple Load multiple giftcard transaction entities based on certain conditions.
commerce_gc_transaction_new Create a stub giftcard transaction entity.
commerce_gc_transaction_save Save a giftcard transaction entity.
commerce_gc_transaction_statuses Get a list of all transaction status info or info about a particular status.
commerce_gc_transaction_status_option_list Get a list of transaction status options
commerce_gc_transaction_write Write a new commerce giftcard transaction record. Uses a database transaction to ensure balance integrity.
commerce_gc_validate_order_form_giftcard_transactions
commerce_gc_views_api

Constants