'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;
}
/**
* Implementation of hook_theme().
*/
function uc_usps_theme() {
return array(
'uc_usps_option_label' => array(
'arguments' => array('service' => NULL),
),
);
}
/**
* Implementation of 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' => $node->usps['container'] ? $node->usps['container'] : 'RECTANGULAR',
);
}
}
/**
* Implementation of 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 *
******************************************************************************/
/**
* Implementation of 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 *
******************************************************************************/
/**
* Implementation of 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;
}
/**
* Implementation of 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) {
//drupal_set_message('
'. print_r($products, TRUE) .'
');
//drupal_set_message(''. print_r($details, TRUE) .'
');
$services = 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();
}
$dest = (object)$details;
$usps_server = 'production.shippingapis.com';
$api_dll = 'ShippingAPI.dll';
$connection_url = 'http://'. $usps_server .'/'. $api_dll;
foreach ($packages as $key => $ship_packages) {
$orig = (object)$addresses[$key];
$orig->email = variable_get('uc_store_email', '');
if (strpos($method['id'], 'intl')) {
$request = uc_usps_intl_rate_request($ship_packages, $orig, $dest);
}
else {
$request = uc_usps_rate_request($ship_packages, $orig, $dest);
}
if (user_access('configure quotes') && variable_get('uc_quote_display_debug', FALSE)) {
$services['data']['debug'] .= htmlentities(urldecode($request)) ."
\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) ."
\n";
}
$rate_type = variable_get('uc_usps_online_rates', FALSE);
$response = new SimpleXMLElement($result->data);
if (isset($response->Package)) {
foreach ($response->Package as $package) {
if (isset($package->Error)) {
$services['data']['error'] .= (string)$package->Error[0]->Description .'
';
}
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));
$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 Flat") {
$classid = 'zeroFlat';
}
else {
$classid = 'zero';
}
}
$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']);
}
}
uasort($services, 'uc_quote_price_sort');
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.
* @ingroup themeable
*/
function theme_uc_usps_option_label($service) {
// Start with logo
$output = ' ';
// Add USPS service name, removing the first nine characters
// (== 'U.S.P.S. ') because these replicate the logo image
$output .= substr($service, 9);
return $output;
}
/**
* Construct 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
* RateV3Request XML document to send to USPS
*/
function uc_usps_rate_request($packages, $origin, $destination) {
$request = '';
$services_count = 0;
$rate_type = variable_get('uc_usps_online_rates', FALSE);
foreach ($packages as $package) {
$qty = $package->qty;
for ($i = 0; $i < $qty; $i++) {
$request .= ''.
''. ($rate_type ? 'ONLINE' : 'ALL') .''.
''. substr($origin->postal_code, 0, 5) .''.
''. substr($destination->postal_code, 0, 5) .''.
''. intval($package->pounds) .''.
''. number_format($package->ounces, 1, '.', '') .''.
''. $package->container .''.
''. $package->size .''.
''. ($package->machinable ? 'True' : 'False') .''.
'';
$services_count++;
}
}
$request .= '';
return 'API=RateV3&XML='. drupal_urlencode($request);
}
/**
* Construct 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 = '';
$services_count = 0;
// This needs to be international name per USPS website. See http://pe.usps.com/text/Imm/immctry.htm
$shipto_country = uc_usps_country_map($destination->country);
foreach ($packages as $package) {
$qty = $package->qty;
for ($i = 0; $i < $qty; $i++) {
$request .= ''.
''. intval($package->pounds) .''.
''. ceil($package->ounces) .''.
'Package'.
''. $shipto_country .''.
'';
$services_count++;
}
}
$request .= '';
$request = 'API=IntlRate&XML='. drupal_urlencode($request);
return $request;
}
/**
* Modify the rate received from USPS before displaying to the customer.
*/
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;
}
}
/**
* Organize 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) {
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 = 16 * ($package[0]->weight - $packages[$key][0]->pounds);
$packages[$key][0]->container = 'RECTANGULAR';
$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 {
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':
$weight = $weight / 1000;
case 'kg':
$weight = $weight * 2.2;
case 'lb':
$package->pounds = floor($weight);
$package->ounces = 16 * ($weight - $package->pounds);
break;
case 'oz':
$package->pounds = floor($weight / 16);
$package->ounces = $weight - $package->pounds * 16;
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->width) {
list($package->length, $package->width) = array($package->width, $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':
$weight = $weight / 1000;
case 'kg':
$weight = $weight * 2.2;
case 'lb':
$package->pounds = floor($weight);
$package->ounces = 16 * ($weight - $package->pounds);
break;
case 'oz':
$package->pounds = floor($weight / 16);
$package->ounces = $weight - $package->pounds * 16;
break;
}
$package->container = $product->usps['container'];
$package->length = max($product->length, $product->width) * $length_conversion;
$package->width = min($product->length, $product->width) * $length_conversion;
$package->height = $product->height * $length_conversion;
$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'),
);
}
/**
* Map envelope shipment services to their IDs.
*/
function _uc_usps_env_services() {
return array(
'zero' => t('U.S.P.S. First-Class Mail'),
'zeroFlat' => t('U.S.P.S. First-Class Flat'),
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'),
);
}
/**
* Map parcel shipment services to their IDs.
*/
function _uc_usps_services() {
return array(
'zero' => t('U.S.P.S. First-Class Mail'),
'zeroParcel' => t('U.S.P.S. First-Class 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'),
);
}
/**
* Map 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'),
);
}
/**
* Map 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)'),
);
}