<?php

/**
 * @file
 * Shipping quote method module that receives quotes from the United States
 * Postal Service via XML web service.
 */

/******************************************************************************
 * Drupal Hooks                                                               *
 ******************************************************************************/

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

  $items['admin/store/settings/quotes/methods/usps'] = array(
    'title' => 'USPS',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('uc_usps_admin_settings'),
    'access arguments' => array('configure quotes'),
    'type' => MENU_LOCAL_TASK,
    'file' => 'uc_usps.admin.inc',
  );

  return $items;
}

/**
 * Implements hook_theme().
 */
function uc_usps_theme() {
  return array(
    'uc_usps_option_label' => array(
      'arguments' => array(
        'service' => NULL,
        'packages' => NULL,
      ),
    ),
  );
}

/**
 * Implements hook_form_alter().
 *
 * Add package type to products.
 *
 * @see uc_product_form()
 */
function uc_usps_form_alter(&$form, $form_state, $form_id) {
  if (uc_product_is_product_form($form)) {
    $node = $form['#node'];
    $enabled = variable_get('uc_quote_enabled', array());
    $weight = variable_get('uc_quote_method_weight', array('usps' => 0, 'usps_intl' => 1));
    $form['shipping']['usps'] = array(
      '#type' => 'fieldset',
      '#title' => t('USPS product description'),
      '#collapsible' => TRUE,
      '#collapsed' => ($enabled['usps'] == FALSE || uc_product_get_shipping_type($node) != 'small_package'),
      '#weight' => $weight['usps'],
      '#tree' => TRUE,
    );
    $form['shipping']['usps']['container'] = array(
      '#type' => 'select',
      '#title' => t('Package type'),
      '#options' => _uc_usps_pkg_types(),
      '#default_value' => isset($node->usps['container']) ? $node->usps['container'] : 'VARIABLE',
    );
  }
}

/**
 * Implements hook_nodeapi().
 */
function uc_usps_nodeapi(&$node, $op) {
  if (uc_product_is_product($node->type)) {
    switch ($op) {
      case 'insert':
      case 'update':
        if (isset($node->usps)) {
          $usps_values = $node->usps;
          if (!$node->revision) {
            db_query("DELETE FROM {uc_usps_products} WHERE vid = %d", $node->vid);
          }
          db_query("INSERT INTO {uc_usps_products} (vid, nid, container) VALUES (%d, %d, '%s')",
            $node->vid, $node->nid, $usps_values['container']);
        }
      break;
      case 'load':
        if (uc_product_get_shipping_type($node) == 'small_package') {
          return array('usps' => db_fetch_array(db_query("SELECT * FROM {uc_usps_products} WHERE vid = %d", $node->vid)));
        }
      break;
      case 'delete':
        db_query("DELETE FROM {uc_usps_products} WHERE nid = %d", $node->nid);
      break;
      case 'delete revision':
        db_query("DELETE FROM {uc_usps_products} WHERE vid = %d", $node->vid);
      break;
    }
  }
}

/******************************************************************************
 * Conditional Actions Hooks                                                  *
 ******************************************************************************/

/**
 * Implements hook_ca_predicate().
 *
 * Connect USPS quote action and event.
 */
function uc_usps_ca_predicate() {
  $enabled = variable_get('uc_quote_enabled', array());
  $quote_action = array(
    '#name' => 'uc_quote_action_get_quote',
    '#title' => t('Fetch a shipping quote'),
    '#argument_map' => array(
      'order' => 'order',
      'method' => 'method',
    ),
  );
  // Domestic areas include U.S., American Samoa, Guam, Puerto Rico, and the Virgin Islands
  $countries = array(16, 316, 630, 840, 850);
  $predicates = array(
    'uc_usps_get_quote' => array(
      '#title' => t('Shipping quote from USPS'),
      '#trigger' => 'get_quote_from_usps',
      '#class' => 'uc_usps',
      '#status' => $enabled['usps'],
      '#conditions' => array(
        '#operator' => 'AND',
        '#conditions' => array(
          array(
            '#name' => 'uc_order_condition_delivery_country',
            '#title' => t('Is in domestic US areas (US, AS, GU, PR, VI)'),
            '#argument_map' => array(
              'order' => 'order',
            ),
            '#settings' => array(
              'countries' => $countries,
            ),
          ),
        ),
      ),
      '#actions' => array(
        $quote_action,
      ),
    ),
    'uc_usps_get_env_quote' => array(
      '#title' => t('Shipping quote from USPS'),
      '#trigger' => 'get_quote_from_usps_env',
      '#class' => 'uc_usps',
      '#status' => $enabled['usps_env'],
      '#conditions' => array(
        '#operator' => 'AND',
        '#conditions' => array(
          array(
            '#name' => 'uc_order_condition_delivery_country',
            '#title' => t('Is in domestic US areas (US, AS, GU, PR, VI)'),
            '#argument_map' => array(
              'order' => 'order',
            ),
            '#settings' => array(
              'countries' => $countries,
            ),
          ),
        ),
      ),
      '#actions' => array(
        $quote_action,
      ),
    ),
    'uc_usps_get_intl_quote' => array(
      '#title' => t('Shipping quote from USPS Intl.'),
      '#trigger' => 'get_quote_from_usps_intl',
      '#class' => 'uc_usps',
      '#status' => $enabled['usps_intl'],
      '#conditions' => array(
        '#operator' => 'AND',
        '#conditions' => array(
          array(
            '#name' => 'uc_order_condition_delivery_country',
            '#title' => t('Is not in domestic US areas (US, AS, GU, PR, VI)'),
            '#argument_map' => array(
              'order' => 'order',
            ),
            '#settings' => array(
              'negate' => TRUE,
              'countries' => $countries,
            ),
          ),
        ),
      ),
      '#actions' => array(
        $quote_action,
      ),
    ),
    'uc_usps_get_intl_env_quote' => array(
      '#title' => t('Shipping quote from USPS Intl.'),
      '#trigger' => 'get_quote_from_usps_intl_env',
      '#class' => 'uc_usps',
      '#status' => $enabled['usps_intl_env'],
      '#conditions' => array(
        '#operator' => 'AND',
        '#conditions' => array(
          array(
            '#name' => 'uc_order_condition_delivery_country',
            '#title' => t('Is not in domestic US areas (US, AS, GU, PR, VI)'),
            '#argument_map' => array(
              'order' => 'order',
            ),
            '#settings' => array(
              'negate' => TRUE,
              'countries' => $countries,
            ),
          ),
        ),
      ),
      '#actions' => array(
        $quote_action,
      ),
    ),
  );

  return $predicates;
}

/******************************************************************************
 * Ubercart Hooks                                                             *
 ******************************************************************************/

/**
 * Implements hook_shipping_type().
 */
function uc_usps_shipping_type() {
  $weight = variable_get('uc_quote_type_weight', array('envelope' => -1, 'small_package' => 0));

  $types = array(
    'envelope' => array(
      'id' => 'envelope',
      'title' => t('Envelope'),
      'weight' => $weight['envelope'],
    ),
    'small_package' => array(
      'id' => 'small_package',
      'title' => t('Small package'),
      'weight' => $weight['small_package'],
    ),
  );

  return $types;
}

/**
 * Implements hook_shipping_method().
 */
function uc_usps_shipping_method() {
  $enabled = variable_get('uc_quote_enabled', array());
  $weight = variable_get('uc_quote_method_weight', array(
    'usps_env' => 0,
    'usps' => 0,
    'usps_intl_env' => 1,
    'usps_intl' => 1,
   ));

  $methods = array(
    'usps_env' => array(
      'id' => 'usps_env',
      'module' => 'uc_usps',
      'title' => t('U.S. Postal Service (Envelope)'),
      'quote' => array(
        'type' => 'envelope',
        'callback' => 'uc_usps_quote',
        'accessorials' => _uc_usps_env_services(),
      ),
      'enabled' => $enabled['usps_env'],
      'weight' => $weight['usps_env'],
    ),
    'usps' => array(
      'id' => 'usps',
      'module' => 'uc_usps',
      'title' => t('U.S. Postal Service (Parcel)'),
      'quote' => array(
        'type' => 'small_package',
        'callback' => 'uc_usps_quote',
        'accessorials' => _uc_usps_services(),
      ),
      'enabled' => $enabled['usps'],
      'weight' => $weight['usps'],
    ),
    'usps_intl_env' => array(
      'id' => 'usps_intl_env',
      'module' => 'uc_usps',
      'title' => t('U.S. Postal Service (Intl., Envelope)'),
      'quote' => array(
        'type' => 'envelope',
        'callback' => 'uc_usps_quote',
        'accessorials' => _uc_usps_intl_env_services(),
      ),
      'enabled' => $enabled['usps_intl_env'],
      'weight' => $weight['usps_intl_env'],
    ),
    'usps_intl' => array(
      'id' => 'usps_intl',
      'module' => 'uc_usps',
      'title' => t('U.S. Postal Service (Intl., Parcel)'),
      'quote' => array(
        'type' => 'small_package',
        'callback' => 'uc_usps_quote',
        'accessorials' => _uc_usps_intl_services(),
      ),
      'enabled' => $enabled['usps_intl'],
      'weight' => $weight['usps_intl'],
    ),
  );

  return $methods;
}

/******************************************************************************
 * Module Functions                                                           *
 ******************************************************************************/

/**
 * Callback for retrieving USPS shipping quote.
 *
 * @param $products
 *   Array of cart contents.
 * @param $details
 *   Order details other than product information.
 * @param $method
 *   The shipping method to create the quote.
 *
 * @return
 *   JSON object containing rate, error, and debugging information.
 */
function uc_usps_quote($products, $details, $method) {
  // The uc_quote AJAX query can fire before the customer has completely
  // filled out the destination address, so check to see whether the address
  // has all needed fields. If not, abort.
  $destination = (object) $details;

  // Country code is always needed.
  if (empty($destination->country)) {
    // Skip this shipping method.
    return array();
  }

  // Shipments to the US also need zone and postal_code.
  if (($destination->country == 840) &&
      (empty($destination->zone) || empty($destination->postal_code))) {
    // Skip this shipping method.
    return array();
  }

  // USPS Production server.
  $connection_url = 'http://production.shippingapis.com/ShippingAPI.dll';

  // Initialize $services to prevent PHP notices here and in uc_quote.
  $services['data'] = array('debug' => NULL, 'error' => array());
  $addresses = array((array)variable_get('uc_quote_store_default_address', new stdClass()));
  $packages = _uc_usps_package_products($products, $addresses);
  if (!count($packages)) {
    return array();
  }

  foreach ($packages as $key => $ship_packages) {
    $orig = (object)$addresses[$key];
    $orig->email = uc_store_email();

    if (strpos($method['id'], 'intl') && ($destination->country != 840)) {
      // Build XML for international rate request
      $request = uc_usps_intl_rate_request($ship_packages, $orig, $destination);
    }
    elseif ($destination->country == 840) {
      // Build XML for domestic rate request
      $request = uc_usps_rate_request($ship_packages, $orig, $destination);
    }

    if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) {
      $services['data']['debug'] .= htmlentities(urldecode($request)) ."<br />\n";
    }

    $result = drupal_http_request($connection_url, array(), 'POST', $request);

    if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) {
      $services['data']['debug'] .= htmlentities($result->data) ."<br />\n";
    }

    $rate_type = variable_get('uc_usps_online_rates', FALSE);
    $response = new SimpleXMLElement($result->data);

    // Map double-encoded HTML markup in service names to Unicode characters
    $service_markup = array(
      '&lt;sup&gt;&amp;reg;&lt;/sup&gt;'   => '®',
      '&lt;sup&gt;&amp;trade;&lt;/sup&gt;' => '™',
      '**'                                 => '',
    );
    // Use this map to fix USPS service names
    if (strpos($method['id'], 'intl')) {
      // Find and replace markup in International service names
      foreach ($response->xpath('//Service') as $service) {
        $service->SvcDescription = str_replace(array_keys($service_markup), $service_markup, $service->SvcDescription);
      }
    }
    else {
      // Find and replace markup in Domestic service names
      foreach ($response->xpath('//Postage') as $postage) {
        $postage->MailService = str_replace(array_keys($service_markup), $service_markup, $postage->MailService);
      }
    }

    if (isset($response->Package)) {
      foreach ($response->Package as $package) {
        if (isset($package->Error)) {
          $services['data']['error'] .= (string)$package->Error[0]->Description .'<br />';
        }
        else {
          if (strpos($method['id'], 'intl')) {
            foreach ($package->Service as $service) {
              $id = (string)$service['ID'];
              $services[$id]['label'] = t('U.S.P.S. @service', array('@service' => (string)$service->SvcDescription));
              // Markup rate before customer sees it
              if (!isset($services[$id]['rate'])) {
                $services[$id]['rate'] = 0;
              }
              $services[$id]['rate'] += uc_usps_markup((string)$service->Postage);
            }
          }
          else {
            foreach ($package->Postage as $postage) {
              $classid = (string)$postage['CLASSID'];
              if ($classid === '0') {
                if ((string)$postage->MailService == "First-Class Mail® Parcel") {
                  $classid = 'zeroParcel';
                }
                elseif ((string)$postage->MailService == "First-Class Mail® Letter") {
                  $classid = 'zeroFlat';
                }
                else {
                  $classid = 'zero';
                }
              }
              if (!isset($services[$classid]['rate'])) {
                $services[$classid]['rate'] = 0;
              }
              $services[$classid]['label'] = t('U.S.P.S. @service', array('@service' => (string)$postage->MailService));
              // Markup rate before customer sees it
              // Rates are stored differently if ONLINE $rate_type is requested.
              // First Class doesn't have online rates, so if CommercialRate
              // is missing use Rate instead
              if ($rate_type && !empty($postage->CommercialRate)) {
                $services[$classid]['rate'] += uc_usps_markup((string)$postage->CommercialRate);
              }
              else {
                $services[$classid]['rate'] += uc_usps_markup((string)$postage->Rate);
              }
            }
          }
        }
      }
    }
  }

  $method_services = 'uc_'. $method['id'] .'_services';
  $usps_services = array_filter(variable_get($method_services, array_keys(call_user_func('_'. $method_services))));
  foreach ($services as $service => $quote) {
    if ($service != 'data' && !in_array($service, $usps_services)) {
      unset($services[$service]);
    }
  }
  $context = array(
    'revision' => 'themed',
    'type' => 'amount',
  );
  foreach ($services as $key => $quote) {
    if (isset($quote['rate'])) {
      $context['subject']['quote'] = $quote;
      $context['revision'] = 'altered';
      $services[$key]['rate'] = uc_price($quote['rate'], $context);
      $context['revision'] = 'formatted';
      $services[$key]['format'] = uc_price($quote['rate'], $context);
      $services[$key]['option_label'] = theme('uc_usps_option_label', $quote['label'], $packages);
    }
  }

  // Separate debug data out of $services.  This is necessary because
  // $services['data'] is not sortable by a 'rate' key, so it has to be
  // kept separate from the reset of the $services array until after the sort.
  $debug_data = $services['data'];
  unset($services['data']);

  uasort($services, 'uc_quote_price_sort');

  if (isset($debug_data['debug']) ||
      (isset($debug_data['error']) && count($debug_data['error']))) {
    $services['data'] = $debug_data;
  }

  return $services;
}

/**
 * Theme function to format the USPS service name and rate amount line-item
 * shown to the customer.
 *
 * @param $service
 *   The USPS service name.
 * @param $packages
 *   Package information.
 *
 * @ingroup themeable
 */
function theme_uc_usps_option_label($service, $packages) {
  // Start with USPS logo
  $output  = theme('image',
    drupal_get_path('module', 'uc_usps') . '/uc_usps_logo.gif',
    t('USPS logo'),
    '',
    array('class' => 'usps-logo')
  );

  // Add USPS service name, removing the first nine characters
  // (== 'U.S.P.S. ') because these replicate the logo image
  $output .= substr($service, 9);

  // Add package information
  $output .= ' ('. format_plural(count($packages), '1 package', '@count packages') .')';

  return $output;
}

/**
 * Constructs a quote request for domestic shipments.
 *
 * @param $packages
 *   Array of packages received from the cart.
 * @param $origin
 *   Delivery origin address information.
 * @param $destination
 *   Delivery destination address information.
 *
 * @return
 *   RateV4Request XML document to send to USPS
 */
function uc_usps_rate_request($packages, $origin, $destination) {
  $request  = '<RateV4Request USERID="'. variable_get('uc_usps_user_id', '') .'">';
  $request .= '<Revision>2</Revision>';

  $rate_type = variable_get('uc_usps_online_rates', FALSE);

  $package_id = 0;
  foreach ($packages as $package) {
    $qty = $package->qty;
    for ($i = 0; $i < $qty; $i++) {
      $request .= '<Package ID="'. $package_id .'">'.
        '<Service>'. ($rate_type ? 'ONLINE' : 'ALL') .'</Service>'.
        '<ZipOrigination>'. substr(trim($origin->postal_code), 0, 5) .'</ZipOrigination>'.
        '<ZipDestination>'. substr(trim($destination->postal_code), 0, 5) .'</ZipDestination>'.
        '<Pounds>'. intval($package->pounds) .'</Pounds>'.
        '<Ounces>'. number_format($package->ounces, 1, '.', '') .'</Ounces>'.
        '<Container>'. $package->container .'</Container>'.
        '<Size>'. $package->size .'</Size>'.
        '<Machinable>'. ($package->machinable ? 'True' : 'False') .'</Machinable>'.
        '</Package>';

      $package_id++;
    }
  }
  $request .= '</RateV4Request>';

  return 'API=RateV4&XML='. drupal_urlencode($request);
}

/**
 * Constructs a quote request for international shipments.
 *
 * @param $packages
 *   Array of packages received from the cart.
 * @param $origin
 *   Delivery origin address information.
 * @param $destination
 *   Delivery destination address information.
 *
 * @return
 *   IntlRateRequest XML document to send to USPS
 */
function uc_usps_intl_rate_request($packages, $origin, $destination) {
  module_load_include('inc', 'uc_usps', 'uc_usps.countries');
  $request  = '<IntlRateV2Request USERID="'. variable_get('uc_usps_user_id', '') .'">';
  $request .= '<Revision>2</Revision>';

  // USPS does not use ISO 3166 country name in some cases so we
  // need to transform ISO country name into one USPS understands.
  $shipto_country = uc_usps_country_map($destination->country);

  $package_id = 0;
  foreach ($packages as $package) {
    $qty = $package->qty;
    for ($i = 0; $i < $qty; $i++) {
      $request .= '<Package ID="'. $package_id .'">'.
        '<Pounds>'. intval($package->pounds) .'</Pounds>'.
        '<Ounces>'. ceil($package->ounces) .'</Ounces>'.
        '<MailType>All</MailType>'.
        '<ValueOfContents>'. $package->price .'</ValueOfContents>'.
        '<Country>'. $shipto_country .'</Country>'.
        '<Container>'. 'RECTANGULAR' .'</Container>'.
        '<Size>'. 'REGULAR' .'</Size>'.
        '<Width>'. '</Width>'.
        '<Length>'. '</Length>'.
        '<Height>'. '</Height>'.
        '<Girth>'. '</Girth>'.
        '<OriginZip>'. substr(trim($origin->postal_code), 0, 5) .'</OriginZip>';

        // Check if we need to add any special services to this package
        if (variable_get('uc_usps_insurance', FALSE)) {
          $request .= '<ExtraServices><ExtraService>1</ExtraService></ExtraServices>';
        }

        // Close off Package tag
        $request .= '</Package>';

      $package_id++;
    }
  }
  $request .= '</IntlRateV2Request>';

  $request = 'API=IntlRateV2&XML='. drupal_urlencode($request);
  return $request;
}

/**
 * Modifies the rate received from USPS before displaying to the customer.
 *
 * @param $rate
 *   Shipping rate without any rate markup.
 *
 * @return
 *   Shipping rate after markup.
 */
function uc_usps_markup($rate) {
  $markup = variable_get('uc_usps_markup', '0');
  $type = variable_get('uc_usps_markup_type', 'percentage');
  if (is_numeric(trim($markup))) {
    switch ($type) {
      case 'percentage':
        return $rate + $rate * floatval(trim($markup)) / 100;
      case 'multiplier':
        return $rate * floatval(trim($markup));
      case 'currency':
        return $rate + floatval(trim($markup));
    }
  }
  else {
    return $rate;
  }
}

/**
 * Organizes products into packages for shipment.
 *
 * @param $products
 *   An array of product objects as they are represented in the cart or order.
 * @param &$addresses
 *   Reference to an array of addresses which are the pickup locations of each
 *   package. They are determined from the shipping addresses of their
 *   component products.
 *
 * @return
 *   Array of packaged products. Packages are separated by shipping address and
 *   weight or quantity limits imposed by the shipping method or the products.
 */
function _uc_usps_package_products($products, &$addresses) {
  $last_key = 0;
  $packages = array();
  if (variable_get('uc_usps_all_in_one', TRUE) && count($products) > 1) {
    // "All in one" packaging strategy
    // Only need to do this if more than one product line item in order
    foreach ($products as $product) {
      if ($product->nid) {
        $address = (array)uc_quote_get_default_shipping_address($product->nid);
        $key = array_search($address, $addresses);
        if ($key === FALSE) {
          $addresses[++$last_key] = $address;
          $key = $last_key;
          $packages[$key][0] = new stdClass();
        }
      }
      $packages[$key][0]->price += $product->price * $product->qty;
      $packages[$key][0]->weight += $product->weight * $product->qty * uc_weight_conversion($product->weight_units, 'lb');
    }
    foreach ($packages as $key => $package) {
      $packages[$key][0]->pounds = floor($package[0]->weight);
      $packages[$key][0]->ounces = LB_TO_OZ * ($package[0]->weight - $packages[$key][0]->pounds);
      $packages[$key][0]->container = 'VARIABLE';
      $packages[$key][0]->size = 'REGULAR';
      // Packages are "machinable" if heavier than 6oz. and less than 35lbs.
      $packages[$key][0]->machinable = (
        ($packages[$key][0]->pounds == 0 ? $packages[$key][0]->ounces >= 6 : TRUE) &&
        $packages[$key][0]->pounds <= 35 &&
        ($packages[$key][0]->pounds == 35 ? $packages[$key][0]->ounces == 0 : TRUE)
      );
      $packages[$key][0]->qty = 1;
    }
  }
  else {
    // !variable_get('uc_usps_all_in_one', TRUE) || count($products) = 1
    // "Each in own" packaging strategy, or only one product line item in order
    foreach ($products as $product) {
      if ($product->nid) {
        $address = (array)uc_quote_get_default_shipping_address($product->nid);
        $key = array_search($address, $addresses);
        if ($key === FALSE) {
          $addresses[++$last_key] = $address;
          $key = $last_key;
        }
      }
      if (!$product->pkg_qty) {
        $product->pkg_qty = 1;
      }
      $num_of_pkgs = (int)($product->qty / $product->pkg_qty);
      if ($num_of_pkgs) {
        $package = drupal_clone($product);
        $package->description = $product->model;
        $weight = $product->weight * $product->pkg_qty;
        switch ($product->weight_units) {
          case 'g':
            // Convert to kg and fall through
            $weight = $weight * G_TO_KG;
          case 'kg':
            // Convert to lb and fall through
            $weight = $weight * KG_TO_LB;
          case 'lb':
            $package->pounds = floor($weight);
            $package->ounces = LB_TO_OZ * ($weight - $package->pounds);
            break;
          case 'oz':
            $package->pounds = floor($weight * OZ_TO_LB);
            $package->ounces = $weight - $package->pounds * LB_TO_OZ;
            break;
        }
        $package->container = $product->usps['container'];
        $length_conversion = uc_length_conversion($product->length_units, 'in');
        $package->length = max($product->length, $product->width) * $length_conversion;
        $package->width = min($product->length, $product->width) * $length_conversion;
        $package->height = $product->height * $length_conversion;
        if ($package->length < $package->height) {
          list($package->length, $package->height) = array($package->height, $package->length);
        }
        $package->girth = 2 * $package->width + 2 * $package->height;
        $package->size = $package->length + $package->girth;
        if ($package->size <= 84) {
          $package->size = 'REGULAR';
        }
        elseif ($package->size <= 108) {
          $package->size = 'LARGE';
        }
        elseif ($package->size <= 130) {
          $package->size = 'OVERSIZE';
        }
        else {
          $package->size = 'GI-HUGE-IC'; // Too big for the U.S. Postal service.
        }
        $package->machinable = (
          $package->length >= 6 && $package->length <= 34 &&
          $package->width >= 0.25 && $package->width <= 17 &&
          $package->height >= 3.5 && $package->height <= 17 &&
          ($package->pounds == 0 ? $package->ounces >= 6 : TRUE) &&
          $package->pounds <= 35 &&
          ($package->pounds == 35 ? $package->ounces == 0 : TRUE)
        );
        $package->price = $product->price * $product->pkg_qty;
        $package->qty = $num_of_pkgs;
        $packages[$key][] = $package;
      }
      $remaining_qty = $product->qty % $product->pkg_qty;
      if ($remaining_qty) {
        $package = drupal_clone($product);
        $package->description = $product->model;
        $weight = $product->weight * $remaining_qty;
        switch ($product->weight_units) {
          case 'g':
            // Convert to kg and fall through
            $weight = $weight * G_TO_KG;
          case 'kg':
            // Convert to lb and fall through
            $weight = $weight * KG_TO_LB;
          case 'lb':
            $package->pounds = floor($weight);
            $package->ounces = LB_TO_OZ * ($weight - $package->pounds);
            break;
          case 'oz':
            $package->pounds = floor($weight * OZ_TO_LB);
            $package->ounces = $weight - $package->pounds * LB_TO_OZ;
            break;
        }
        $package->container = $product->usps['container'];
        $length_conversion = uc_length_conversion($product->length_units, 'in');
        $package->length = max($product->length, $product->width) * $length_conversion;
        $package->width = min($product->length, $product->width) * $length_conversion;
        $package->height = $product->height * $length_conversion;
        if ($package->length < $package->height) {
          list($package->length, $package->height) = array($package->height, $package->length);
        }
        $package->girth = 2 * $package->width + 2 * $package->height;
        $package->size = $package->length + $package->girth;
        if ($package->size <= 84) {
          $package->size = 'REGULAR';
        }
        elseif ($package->size <= 108) {
          $package->size = 'LARGE';
        }
        elseif ($package->size <= 130) {
          $package->size = 'OVERSIZE';
        }
        else {
          $package->size = 'GI-HUGE-IC'; // Too big for the U.S. Postal service.
        }
        $package->machinable = (
          $package->length >= 6 && $package->length <= 34 &&
          $package->width >= 0.25 && $package->width <= 17 &&
          $package->height >= 3.5 && $package->height <= 17 &&
          ($package->pounds == 0 ? $package->ounces >= 6 : TRUE) &&
          $package->pounds <= 35 &&
        ($package->pounds == 35 ? $package->ounces == 0 : TRUE)
        );
        $package->price = $product->price * $remaining_qty;
        $package->qty = 1;
        $packages[$key][] = $package;
      }
    }
  }
  return $packages;
}

/**
 * Convenience function for select form elements.
 */
function _uc_usps_pkg_types() {
  return array(
    'VARIABLE' => t('Variable'),
    'FLAT RATE BOX' => t('Flat rate box'),
    'LG FLAT RATE BOX' => t('Large flat rate box'),
    'FLAT RATE ENVELOPE' => t('Flat rate envelope'),
    'RECTANGULAR' => t('Rectangular'),
    'NONRECTANGULAR' => t('Non-rectangular'),
  );
}

/**
 * Maps envelope shipment services to their IDs.
 */
function _uc_usps_env_services() {
  return array(
    'zero' => t('U.S.P.S. First-Class Mail Postcard'),
    'zeroFlat' => t('U.S.P.S. First-Class Mail Letter'),
    12 => t('U.S.P.S. First-Class Postcard Stamped'),
    1 => t('U.S.P.S. Priority Mail'),
    16 => t('U.S.P.S. Priority Mail Flat-Rate Envelope'),
    3 => t('U.S.P.S. Express Mail'),
    13 => t('U.S.P.S. Express Mail Flat-Rate Envelope'),
    23 => t('U.S.P.S. Express Mail Sunday/Holiday Guarantee'),
    25 => t('U.S.P.S. Express Mail Flat-Rate Envelope Sunday/Holiday Guarantee'),
  );
}

/**
 * Maps parcel shipment services to their IDs.
 */
function _uc_usps_services() {
  return array(
    'zeroFlat' => t('U.S.P.S. First-Class Mail Letter'),
    'zeroParcel' => t('U.S.P.S. First-Class Mail Parcel'),
    1 => t('U.S.P.S. Priority Mail'),
    28 => t('U.S.P.S. Priority Mail Small Flat-Rate Box'),
    17 => t('U.S.P.S. Priority Mail Regular/Medium Flat-Rate Box'),
    22 => t('U.S.P.S. Priority Mail Large Flat-Rate Box'),
    3 => t('U.S.P.S. Express Mail'),
    23 => t('U.S.P.S. Express Mail Sunday/Holiday Guarantee'),
    4 => t('U.S.P.S. Parcel Post'),
    5 => t('U.S.P.S. Bound Printed Matter'),
    6 => t('U.S.P.S. Media Mail'),
    7 => t('U.S.P.S. Library'),
  );
}

/**
 * Maps international envelope services to their IDs.
 */
function _uc_usps_intl_env_services() {
  return array(
    13 => t('First Class Mail International Letter'),
    14 => t('First Class Mail International Large Envelope'),
    2 => t('Priority Mail International'),
    8 => t('Priority Mail International Flat Rate Envelope'),
    4 => t('Global Express Guaranteed'),
    12 => t('GXG Envelopes'),
    1 => t('Express Mail International (EMS)'),
    10 => t('Express Mail International (EMS) Flat Rate Envelope'),
  );
}

/**
 * Maps international parcel services to their IDs.
 */
function _uc_usps_intl_services() {
  return array(
    15 => t('First Class Mail International Package'),
    2 => t('Priority Mail International'),
    16 => t('Priority Mail International Small Flat-Rate Box'),
    9 => t('Priority Mail International Regular/Medium Flat-Rate Box'),
    11 => t('Priority Mail International Large Flat-Rate Box'),
    4 => t('Global Express Guaranteed'),
    6 => t('Global Express Guaranteed Non-Document Rectangular'),
    7 => t('Global Express Guaranteed Non-Document Non-Rectangular'),
    1 => t('Express Mail International (EMS)'),
  );
}
