You are here

uc_ups.module in Ubercart 8.4

UPS shipping quote module.

File

shipping/uc_ups/uc_ups.module
View source
<?php

/**
 * @file
 * UPS shipping quote module.
 */
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\uc_store\AddressInterface;
use Drupal\uc_ups\UPSUtilities;

/**
 * Implements hook_menu().
 */
function uc_ups_menu() {
  $items['admin/store/orders/%uc_order/shipments/ups'] = [
    'title' => 'UPS shipment',
    'page callback' => 'drupal_get_form',
    'page arguments' => [
      'uc_ups_confirm_shipment',
      3,
    ],
    'access arguments' => [
      'fulfill orders',
    ],
    'file' => 'src/Plugin/Ubercart/FulfillmentMethod/uc_ups.ship.inc',
  ];
  $items['admin/store/orders/%uc_order/shipments/labels/ups'] = [
    'page callback' => 'theme',
    'page arguments' => [
      'uc_ups_label_image',
    ],
    'access arguments' => [
      'fulfill orders',
    ],
    'file' => 'src/Plugin/Ubercart/FulfillmentMethod/uc_ups.ship.inc',
  ];
  return $items;
}

/**
 * Implements hook_cron().
 *
 * Deletes UPS shipping labels from the file system automatically
 * on a periodic basis. Cron must be enabled for automatic deletion.
 * Default is never delete the labels, keep them forever.
 */
function uc_ups_cron() {
  $ups_config = \Drupal::config('uc_ups.settings');
  $cutoff = \Drupal::time()
    ->getRequestTime() - $ups_config
    ->get('label_lifetime');
  if ($cutoff == \Drupal::time()
    ->getRequestTime()) {

    // Label lifetime is set to 0, meaning never delete.
    return;
  }

  // Loop over label files in public://ups_labels and test
  // creation date against 'uc_ups_label_lifetime'.
  $files = file_scan_directory('public://ups_labels', '/^label-/');
  foreach ($files as $file) {
    if ($cutoff > filectime($file->uri)) {
      \Drupal::service('file_system')
        ->unlink($file->uri);
      \Drupal::logger('uc_ups')
        ->notice('Removed uc_ups label file @file.', [
        '@file' => $file->uri,
      ]);
    }
  }
}

/**
 * Implements hook_theme().
 */
function uc_ups_theme() {
  return [
    'uc_ups_option_label' => [
      'variables' => [
        'service' => NULL,
        'packages' => NULL,
      ],
      'file' => 'uc_ups.theme.inc',
      'function' => 'theme_uc_ups_option_label',
    ],
    'uc_ups_confirm_shipment' => [
      'render element' => 'form',
      'file' => 'src/Plugin/Ubercart/FulfillmentMethod/uc_ups.ship.inc',
      'function' => 'theme_uc_ups_confirm_shipment',
    ],
    'uc_ups_label_image' => [
      'variables' => [],
      'file' => 'src/Plugin/Ubercart/FulfillmentMethod/uc_ups.ship.inc',
      'function' => 'theme_uc_ups_label_image',
    ],
  ];
}

/**
 * Implements hook_form_BASE_FORM_ID_alter() for node_form().
 *
 * Adds package type to products.
 *
 * @see uc_product_form()
 * @see uc_ups_product_alter_validate()
 */
function uc_ups_form_node_form_alter(&$form, FormStateInterface $form_state) {
  $ups_config = \Drupal::config('uc_ups.settings');
  $quote_config = \Drupal::config('uc_quote.settings');
  $node = $form_state
    ->getFormObject()
    ->getEntity();
  if (uc_product_is_product($node
    ->bundle())) {
    $enabled = $quote_config
      ->get('enabled') + [
      'ups' => FALSE,
    ];
    $weight = $quote_config
      ->get('method_weight') + [
      'ups' => 0,
    ];
    $ups = [
      '#type' => 'details',
      '#title' => t('UPS product description'),
      '#weight' => $weight['ups'],
      '#tree' => TRUE,
    ];
    $ups['pkg_type'] = [
      '#type' => 'select',
      '#title' => t('Package type'),
      '#options' => UPSUtilities::packageTypes(),
      '#default_value' => isset($node->ups['pkg_type']) ? $node->ups['pkg_type'] : $ups_config
        ->get('pkg_type'),
    ];
    $form['shipping']['ups'] = $ups;
    if ($enabled['ups']) {
      $form['#validate'][] = 'uc_ups_product_alter_validate';
    }
  }
}

/**
 * Validation handler for UPS product fields.
 *
 * @see uc_ups_form_alter()
 */
function uc_ups_product_alter_validate($form, FormStateInterface $form_state) {
  $ups_config = \Drupal::config('uc_ups.settings');
  if ($form_state
    ->hasValue('shippable') && ($form_state
    ->getValue('shipping_type') == 'small_package' || $form_state
    ->isValueEmpty('shipping_type') && $ups_config
    ->get('uc_store_shipping_type') == 'small_package')) {
    if ($form_state
      ->getValue([
      'ups',
      'pkg_type',
    ]) == '02' && ($form_state
      ->isValueEmpty('dim_length') || $form_state
      ->isValueEmpty('dim_width')) || $form_state
      ->isValueEmpty('dim_height')) {
      $form_state
        ->setErrorByName('base][dimensions', t('Dimensions are required for custom packaging.'));
    }
  }
}

/**
 * Implements hook_node_insert().
 */
function uc_ups_node_insert($node) {
  uc_ups_node_update($node);
}

/**
 * Implements hook_node_update().
 */
function uc_ups_node_update($node) {
  if (uc_product_is_product($node)) {
    if (isset($node->ups)) {
      $ups_values = $node->ups;
      $connection = \Drupal::database();
      if (!$node
        ->isNewRevision()) {
        $connection
          ->delete('uc_ups_products')
          ->condition('vid', $node
          ->getRevisionId())
          ->execute();
      }
      $connection
        ->insert('uc_ups_products')
        ->fields([
        'vid' => $node
          ->getRevisionId(),
        'nid' => $node
          ->id(),
        'pkg_type' => $ups_values['pkg_type'],
      ])
        ->execute();
    }
  }
}

/**
 * Implements hook_node_load().
 */
function uc_ups_node_load($nodes) {
  $nids = [];
  foreach ($nodes as $node) {
    if (uc_product_is_product($node)) {
      $nids[] = $node
        ->id();
    }
  }
  if (empty($nids)) {
    return;
  }
  $vids = [];
  $connection = \Drupal::database();
  $ups_config = \Drupal::config('uc_ups.settings');
  $shipping_type = $ups_config
    ->get('uc_store_shipping_type');
  $shipping_types = $connection
    ->query("SELECT id, shipping_type FROM {uc_quote_shipping_types} WHERE id_type = :type AND id IN (:ids[])", [
    ':type' => 'product',
    ':ids[]' => $nids,
  ])
    ->fetchAllKeyed();
  foreach ($nids as $nid) {
    if (isset($shipping_types[$nid])) {
      $nodes[$nid]->shipping_type = $shipping_types[$nid];
    }
    else {
      $nodes[$nid]->shipping_type = $shipping_type;
    }
    if ($nodes[$nid]->shipping_type == 'small_package') {
      $vids[$nid] = $nodes[$nid]
        ->getRevisionId();
    }
  }
  if ($vids) {
    $result = $connection
      ->query("SELECT * FROM {uc_ups_products} WHERE vid IN (:vids[])", [
      ':vids[]' => $vids,
    ], [
      'fetch' => PDO::FETCH_ASSOC,
    ]);
    foreach ($result as $ups) {
      $nodes[$ups['nid']]->ups = $ups;
    }
  }
}

/**
 * Implements hook_node_delete().
 */
function uc_ups_node_delete($node) {
  $connection = \Drupal::database();
  $connection
    ->delete('uc_ups_products')
    ->condition('nid', $node
    ->id())
    ->execute();
}

/**
 * Implements hook_node_revision_delete().
 */
function uc_ups_node_revision_delete($node) {
  $connection = \Drupal::database();
  $connection
    ->delete('uc_ups_products')
    ->condition('vid', $node
    ->getRevisionId())
    ->execute();
}

/**
 * Implements hook_uc_shipping_type().
 */
function uc_ups_uc_shipping_type() {
  $quote_config = \Drupal::config('uc_quote.settings');
  $weight = $quote_config
    ->get('type_weight');
  $types = [];
  $types['small_package'] = [
    'id' => 'small_package',
    'title' => t('Small packages'),
    'weight' => $weight['small_package'],
  ];
  return $types;
}

/**
 * Implements hook_uc_shipping_method().
 */
function uc_ups_uc_shipping_method() {
  $methods['ups'] = [
    'id' => 'ups',
    'module' => 'uc_ups',
    'title' => t('UPS'),
    'operations' => [
      'configure' => [
        'title' => t('configure'),
        'url' => Url::fromRoute('uc_ups.settings')
          ->toString(),
      ],
    ],
    'quote' => [
      'type' => 'small_package',
      'callback' => 'uc_ups_quote',
      'accessorials' => UPSUtilities::services(),
    ],
    'ship' => [
      'type' => 'small_package',
      'callback' => 'uc_ups_fulfill_order',
      'file' => 'src/Plugin/Ubercart/FulfillmentMethod/uc_ups.ship.inc',
      'pkg_types' => UPSUtilities::packageTypes(),
    ],
    'cancel' => 'uc_ups_void_shipment',
  ];
  return $methods;
}

/**
 * Implements hook_uc_store_status().
 *
 * Lets the administrator know that the UPS account information has not been
 * filled out.
 */
function uc_ups_uc_store_status() {
  $messages = [];
  $ups_config = \Drupal::config('uc_ups.settings');
  $access = $ups_config
    ->get('access_license') != '';
  $ups_account = $ups_config
    ->get('shipper_number') != '';
  $user = $ups_config
    ->get('user_id') != '';
  $password = $ups_config
    ->get('password') != '';
  if ($access && $ups_account && $user && $password) {
    $messages[] = [
      'status' => 'ok',
      'title' => t('UPS Online Tools'),
      'desc' => t('Information needed to access UPS Online Tools has been entered.'),
    ];
  }
  else {
    $messages[] = [
      'status' => 'error',
      'title' => t('UPS Online Tools'),
      'desc' => t('More information is needed to access UPS Online Tools. Please enter it <a href=":url">here</a>.', [
        ':url' => Url::fromRoute('uc_ups.settings')
          ->toString(),
      ]),
    ];
  }
  return $messages;
}

/**
 * Prepares XML access request string.
 *
 * @return string
 *   XML access request to be prepended to all requests to the UPS webservice.
 */
function uc_ups_access_request() {
  $ups_config = \Drupal::config('uc_ups.settings');
  $access = $ups_config
    ->get('access_license');
  $user = $ups_config
    ->get('user_id');
  $password = $ups_config
    ->get('password');
  $xml = new \XMLWriter();
  $xml
    ->openMemory();
  $xml
    ->startDocument('1.0', 'UTF-8');
  $xml
    ->startElement('AccessRequest');
  $xml
    ->writeAttribute('xml:lang', 'en-US');
  $xml
    ->writeElement('AccessLicenseNumber', $access);
  $xml
    ->writeElement('UserId', $user);
  $xml
    ->writeElement('Password', $password);
  $xml
    ->endElement();
  $xml
    ->endDocument();
  return $xml
    ->outputMemory(TRUE);
}

/**
 * Constructs an XML quote request.
 *
 * @param array $packages
 *   Array of packages received from the cart.
 * @param \Drupal\uc_store\AddressInterface $origin
 *   Delivery origin address information.
 * @param \Drupal\uc_store\AddressInterface $destination
 *   Delivery destination address information.
 * @param string $ups_service
 *   UPS service code (refers to UPS Ground, Next-Day Air, etc.).
 *
 * @return string
 *   RatingServiceSelectionRequest XML document to send to UPS.
 */
function uc_ups_shipping_quote(array $packages, AddressInterface $origin, AddressInterface $destination, $ups_service) {
  $ups_config = \Drupal::config('uc_ups.settings');
  $ua = explode(' ', $_SERVER['HTTP_USER_AGENT']);
  $user_agent = $ua[0];
  $services = UPSUtilities::services();
  $service = [
    'code' => $ups_service,
    'description' => $services[$ups_service],
  ];
  $pkg_types = UPSUtilities::packageTypes();
  $store_config = \Drupal::config('uc_store.settings');
  $shipper_zone = $store_config
    ->get('address.zone');
  $shipper_country = $store_config
    ->get('address.country');
  $shipper_zip = $store_config
    ->get('address.postal_code');
  $shipto_zone = $destination
    ->getZone();
  $shipto_country = $destination
    ->getCountry();
  $shipto_zip = $destination
    ->getPostalCode();
  $shipfrom_zone = $origin
    ->getZone();
  $shipfrom_country = $origin
    ->getCountry();
  $shipfrom_zip = $origin
    ->getPostalCode();
  $ups_units = $ups_config
    ->get('unit_system');
  switch ($ups_units) {
    case 'in':
      $units = 'LBS';
      $unit_name = 'Pounds';
      break;
    case 'cm':
      $units = 'KGS';
      $unit_name = 'Kilograms';
      break;
  }
  $shipment_weight = 0;
  $package_schema = '';
  foreach ($packages as $package) {

    // Determine length conversion factor and weight conversion factor
    // for this shipment.
    $length_factor = uc_length_conversion($package->length_units, $ups_config
      ->get('unit_system'));
    switch ($ups_units) {
      case 'in':
        $weight_factor = uc_weight_conversion($package->weight_units, 'lb');
        break;
      case 'cm':
        $weight_factor = uc_weight_conversion($package->weight_units, 'kg');
        break;
    }

    // Loop over quantity of packages in this shipment.
    $qty = $package->qty;
    for ($i = 0; $i < $qty; $i++) {

      // Build XML for this package.
      $package_type = [
        'code' => $package->pkg_type,
        'description' => $pkg_types[$package->pkg_type],
      ];
      $package_schema .= "<Package>";
      $package_schema .= "<PackagingType>";
      $package_schema .= "<Code>" . $package_type['code'] . "</Code>";
      $package_schema .= "</PackagingType>";
      if ($package->pkg_type == '02' && $package->length && $package->width && $package->height) {
        if ($package->length < $package->width) {
          list($package->length, $package->width) = [
            $package->width,
            $package->length,
          ];
        }
        $package_schema .= "<Dimensions>";
        $package_schema .= "<UnitOfMeasurement>";
        $package_schema .= "<Code>" . strtoupper($ups_config
          ->get('unit_system')) . "</Code>";
        $package_schema .= "</UnitOfMeasurement>";
        $package_schema .= "<Length>" . number_format($package->length * $length_factor, 2, '.', '') . "</Length>";
        $package_schema .= "<Width>" . number_format($package->width * $length_factor, 2, '.', '') . "</Width>";
        $package_schema .= "<Height>" . number_format($package->height * $length_factor, 2, '.', '') . "</Height>";
        $package_schema .= "</Dimensions>";
      }
      $weight = max(1, $package->weight * $weight_factor);
      $shipment_weight += $weight;
      $package_schema .= "<PackageWeight>";
      $package_schema .= "<UnitOfMeasurement>";
      $package_schema .= "<Code>" . $units . "</Code>";
      $package_schema .= "<Description>" . $unit_name . "</Description>";
      $package_schema .= "</UnitOfMeasurement>";
      $package_schema .= "<Weight>" . number_format($weight, 1, '.', '') . "</Weight>";
      $package_schema .= "</PackageWeight>";
      $size = $package->length * $length_factor + 2 * $length_factor * ($package->width + $package->height);
      if ($size > 130 && $size <= 165) {
        $package_schema .= "<LargePackageIndicator/>";
      }
      if ($ups_config
        ->get('insurance')) {
        $package_schema .= "<PackageServiceOptions>";
        $package_schema .= "<InsuredValue>";
        $package_schema .= "<CurrencyCode>" . $store_config
          ->get('currency.code') . "</CurrencyCode>";
        $package_schema .= "<MonetaryValue>" . $package->price . "</MonetaryValue>";
        $package_schema .= "</InsuredValue>";
        $package_schema .= "</PackageServiceOptions>";
      }
      $package_schema .= "</Package>";
    }
  }
  $schema = uc_ups_access_request() . "\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RatingServiceSelectionRequest xml:lang=\"en-US\">\n  <Request>\n    <TransactionReference>\n      <CustomerContext>Complex Rate Request</CustomerContext>\n      <XpciVersion>1.0001</XpciVersion>\n    </TransactionReference>\n    <RequestAction>Rate</RequestAction>\n    <RequestOption>rate</RequestOption>\n  </Request>\n  <PickupType>\n    <Code>" . $ups_config
    ->get('pickup_type') . "</Code>\n  </PickupType>\n  <CustomerClassification>\n    <Code>" . $ups_config
    ->get('classification') . "</Code>\n  </CustomerClassification>\n  <Shipment>\n    <Shipper>\n      <ShipperNumber>" . $ups_config
    ->get('shipper_number') . "</ShipperNumber>\n      <Address>\n        <City>" . $store_config
    ->get('address.city') . "</City>\n        <StateProvinceCode>{$shipper_zone}</StateProvinceCode>\n        <PostalCode>{$shipper_zip}</PostalCode>\n        <CountryCode>{$shipper_country}</CountryCode>\n      </Address>\n    </Shipper>\n    <ShipTo>\n      <Address>\n        <StateProvinceCode>{$shipto_zone}</StateProvinceCode>\n        <PostalCode>{$shipto_zip}</PostalCode>\n        <CountryCode>{$shipto_country}</CountryCode>\n      ";
  if ($ups_config
    ->get('residential_quotes')) {
    $schema .= "<ResidentialAddressIndicator/>\n      ";
  }
  $schema .= "</Address>\n    </ShipTo>\n    <ShipFrom>\n      <Address>\n        <StateProvinceCode>{$shipfrom_zone}</StateProvinceCode>\n        <PostalCode>{$shipfrom_zip}</PostalCode>\n        <CountryCode>{$shipfrom_country}</CountryCode>\n      </Address>\n    </ShipFrom>\n    <ShipmentWeight>\n      <UnitOfMeasurement>\n        <Code>{$units}</Code>\n        <Description>{$unit_name}</Description>\n      </UnitOfMeasurement>\n      <Weight>" . number_format($shipment_weight, 1, '.', '') . "</Weight>\n    </ShipmentWeight>\n    <Service>\n      <Code>{$service['code']}</Code>\n      <Description>{$service['description']}</Description>\n    </Service>\n    ";
  $schema .= $package_schema;
  if ($ups_config
    ->get('negotiated_rates')) {
    $schema .= "<RateInformation>\n          <NegotiatedRatesIndicator/>\n        </RateInformation>";
  }
  $schema .= "</Shipment>\n</RatingServiceSelectionRequest>";
  return $schema;
}

/**
 * Callback for retrieving a UPS shipping quote.
 *
 * Requests a quote for each enabled UPS Service. Therefore, the quote will
 * take longer to display to the user for each option the customer has
 * available.
 *
 * @param array $products
 *   Array of cart contents.
 * @param $details
 *   Order details other than product information.
 * @param $method
 *   The shipping method to create the quote.
 *
 * @return array
 *   JSON object containing rate, error, and debugging information.
 */
function uc_ups_quote(array $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;
  if (empty($destination
    ->getZone()) || empty($destination
    ->getPostalCode()) || empty($destination
    ->getCountry())) {

    // Skip this shipping method.
    return [];
  }
  $quotes = [];
  $ups_config = \Drupal::config('uc_ups.settings');
  $quote_config = \Drupal::config('uc_quote.settings');
  $store_config = \Drupal::config('uc_store.settings');
  $addresses = [
    $quote_config
      ->get('store_default_address'),
  ];
  $key = 0;
  $last_key = 0;
  $packages = [];
  if ($ups_config
    ->get('all_in_one') && count($products) > 1) {
    foreach ($products as $product) {
      if ($product->nid) {

        // Packages are grouped by the address from which they will be
        // shipped. We will keep track of the different addresses in an array
        // and use their keys for the array of packages.
        $key = NULL;
        $address = uc_quote_get_default_shipping_address($product->nid);
        foreach ($addresses as $index => $value) {
          if ($address
            ->isSamePhysicalLocation($value)) {

            // This is an existing address.
            $key = $index;
            break;
          }
        }
        if (!isset($key)) {

          // This is a new address. Increment the address counter $last_key
          // instead of using [] so that it can be used in $packages and
          // $addresses.
          $addresses[++$last_key] = $address;
          $key = $last_key;
        }
      }

      // Add this product to the last package from the found address or start
      // a new package.
      if (isset($packages[$key]) && count($packages[$key])) {
        $package = array_pop($packages[$key]);
      }
      else {
        $package = _uc_ups_new_package();
      }

      // Grab some product properties directly from the (cached) product
      // data. They are not normally available here because the $product
      // object is being read out of the $order object rather than from
      // the database, and the $order object only carries around a limited
      // number of product properties.
      $temp = Node::load($product->nid);
      $product->length = $temp->length;
      $product->width = $temp->width;
      $product->height = $temp->height;
      $product->length_units = $temp->length_units;
      $product->ups['pkg_type'] = isset($temp->ups) ? $temp->ups['pkg_type'] : '02';
      $weight = $product->weight * $product->qty * uc_weight_conversion($product->weight_units, 'lb');
      $package->weight += $weight;
      $package->price += $product->price * $product->qty;
      $length_factor = uc_length_conversion($product->length_units, 'in');
      $package->length = max($product->length * $length_factor, $package->length);
      $package->width = max($product->width * $length_factor, $package->width);
      $package->height = max($product->height * $length_factor, $package->height);
      $packages[$key][] = $package;
    }
    foreach ($packages as $addr_key => $shipment) {
      foreach ($shipment as $key => $package) {
        if (!$package->weight) {
          unset($packages[$addr_key][$key]);
          continue;
        }
        elseif ($package->weight > 150) {

          // UPS has a weight limit on packages of 150 lbs. Pretend the
          // products can be divided into enough packages.
          $qty = floor($package->weight / 150) + 1;
          $package->qty = $qty;
          $package->weight /= $qty;
          $package->price /= $qty;
        }
      }
    }
  }
  else {
    foreach ($products as $product) {
      $key = 0;
      if ($product->nid) {
        $address = uc_quote_get_default_shipping_address($product->nid);
        if (in_array($address, $addresses)) {

          // This is an existing address.
          foreach ($addresses as $index => $value) {
            if ($address == $value) {
              $key = $index;
              break;
            }
          }
        }
        else {

          // This is a new address.
          $addresses[++$last_key] = $address;
          $key = $last_key;
        }
      }
      if (!isset($product->pkg_qty) || !$product->pkg_qty) {
        $product->pkg_qty = 1;
      }
      $num_of_pkgs = (int) ($product->qty / $product->pkg_qty);

      // Grab some product properties directly from the (cached) product
      // data. They are not normally available here because the $product
      // object is being read out of the $order object rather than from
      // the database, and the $order object only carries around a limited
      // number of product properties.
      $temp = Node::load($product->nid);
      $product->length = $temp->length;
      $product->width = $temp->width;
      $product->height = $temp->height;
      $product->length_units = $temp->length_units;
      $product->ups['pkg_type'] = isset($temp->ups) ? $temp->ups['pkg_type'] : '02';
      if ($num_of_pkgs) {
        $package = clone $product;
        $package->description = $product->model;
        $package->weight = $product->weight * $product->pkg_qty;
        $package->price = $product->price * $product->pkg_qty;
        $package->qty = $num_of_pkgs;
        $package->pkg_type = $product->ups['pkg_type'];
        if ($package->weight) {
          $packages[$key][] = $package;
        }
      }
      $remaining_qty = $product->qty % $product->pkg_qty;
      if ($remaining_qty) {
        $package = clone $product;
        $package->description = $product->model;
        $package->weight = $product->weight * $remaining_qty;
        $package->price = $product->price * $remaining_qty;
        $package->qty = 1;
        $package->pkg_type = $product->ups['pkg_type'];
        if ($package->weight) {
          $packages[$key][] = $package;
        }
      }
    }
  }
  if (!count($packages)) {
    return [];
  }
  $dest = (object) $details;
  foreach ($packages as $key => $ship_packages) {
    $orig = $addresses[$key];
    $orig->email = uc_store_email();
    foreach (array_keys(array_filter($ups_config
      ->get('services'))) as $ups_service) {
      $request = uc_ups_shipping_quote($ship_packages, $orig, $dest, $ups_service);
      $resp = \Drupal::httpClient()
        ->post($ups_config
        ->get('connection_address') . 'Rate', NULL, $request)
        ->send();
      $account = \Drupal::currentUser();
      if ($account
        ->hasPermission('configure quotes') && $ups_config
        ->get('uc_quote_display_debug')) {
        if (!isset($debug_data[$ups_service]['debug'])) {
          $debug_data[$ups_service]['debug'] = '';
        }
        $debug_data[$ups_service]['debug'] .= htmlentities($request) . ' <br /><br /> ' . htmlentities($resp
          ->getBody(TRUE));
      }
      $response = new \SimpleXMLElement($resp
        ->getBody(TRUE));
      if (isset($response->Response->Error)) {
        foreach ($response->Response->Error as $error) {
          if ($account
            ->hasPermission('configure quotes') && $ups_config
            ->get('uc_quote_display_debug')) {
            $debug_data[$ups_service]['error'][] = (string) $error->ErrorSeverity . ' ' . (string) $error->ErrorCode . ': ' . (string) $error->ErrorDescription;
          }
          if (strpos((string) $error->ErrorSeverity, 'Hard') !== FALSE) {

            // All or nothing quote. If some products can't be shipped by
            // a certain service, no quote is given for that service. If
            // that means no quotes are given at all, they'd better call in.
            unset($quotes[$ups_service]['rate']);
          }
        }
      }

      // If NegotiatedRates exist, quote based on those, otherwise, use
      // TotalCharges.
      if (isset($response->RatedShipment)) {
        $charge = $response->RatedShipment->TotalCharges;
        if (isset($response->RatedShipment->NegotiatedRates)) {
          $charge = $response->RatedShipment->NegotiatedRates->NetSummaryCharges->GrandTotal;
        }
        if (!isset($charge->CurrencyCode) || (string) $charge->CurrencyCode == $store_config
          ->get('currency.code')) {

          // Markup rate before customer sees it.
          if (!isset($quotes[$ups_service]['rate'])) {
            $quotes[$ups_service]['rate'] = 0;
          }
          $rate = $this
            ->rateMarkup((string) $charge->MonetaryValue);
          $quotes[$ups_service]['rate'] += $rate;
        }
      }
    }
  }

  // Sort quotes by price, lowest to highest.
  uasort($quotes, 'uc_quote_price_sort');
  foreach ($quotes as $key => $quote) {
    if (isset($quote['rate'])) {
      $quotes[$key]['rate'] = $quote['rate'];
      $quotes[$key]['label'] = $method['quote']['accessorials'][$key];
      $quotes[$key]['option_label'] = theme('uc_ups_option_label', [
        'service' => $method['quote']['accessorials'][$key],
        'packages' => $packages,
      ]);
    }
  }

  // Merge debug data into $quotes. This is necessary because
  // $debug_data is not sortable by a 'rate' key, so it has to be
  // kept separate from the $quotes data until this point.
  if (isset($debug_data)) {
    foreach ($debug_data as $key => $data) {
      if (isset($quotes[$key])) {

        // This is debug data for successful quotes.
        $quotes[$key]['debug'] = $debug_data[$key]['debug'];
        if (isset($debug_data[$key]['error'])) {
          $quotes[$key]['error'] = $debug_data[$key]['error'];
        }
      }
      else {

        // This is debug data for quotes that returned error responses from UPS.
        $quotes[$key] = $debug_data[$key];
      }
    }
  }
  return $quotes;
}

/**
 * Constructs a void shipment request.
 *
 * @param string $shipment_number
 *   The UPS shipment tracking number.
 * @param array $tracking_numbers
 *   Array of tracking numbers for individual packages in the shipment.
 *   Optional for shipments of only one package, as they have the same tracking
 *   number.
 *
 * @return string
 *   XML VoidShipmentRequest message.
 */
function uc_ups_void_shipment_request($shipment_number, array $tracking_numbers = []) {
  $customer_context = t('Void shipment @ship_number and tracking numbers @track_list', [
    '@ship_number' => $shipment_number,
    '@track_list' => implode(', ', $tracking_numbers),
  ]);
  $xml = new \XMLWriter();
  $xml
    ->openMemory();
  $xml
    ->startDocument('1.0', 'UTF-8');
  $xml
    ->startElement('VoidShipmentRequest');
  $xml
    ->writeAttribute('xml:lang', 'en-US');
  $xml
    ->startElement('Request');
  $xml
    ->writeElement('RequestAction', 'Void');
  $xml
    ->startElement('TransactionReference');
  $xml
    ->writeElement('CustomerContext', $customer_context);
  $xml
    ->writeElement('XpciVersion', '1.0');
  $xml
    ->endElement();

  //   TransactionReference
  $xml
    ->endElement();

  // Request
  $xml
    ->startElement('ExpandedVoidShipment');
  $xml
    ->writeElement('ShipmentIdentificationNumber', $shipment_number);
  foreach ($tracking_numbers as $number) {
    $xml
      ->writeElement('TrackingNumber', $number);
  }
  $xml
    ->endElement();

  // ExpandedVoidShipment
  $xml
    ->endElement();

  // VoidShipmentRequest
  $xml
    ->endDocument();
  return uc_ups_access_request() . $xml
    ->outputMemory(TRUE);
}

/**
 * Instructs UPS to cancel (in whole or in part) a shipment.
 *
 * @param string $shipment_number
 *   The UPS shipment tracking number.
 * @param array $tracking_numbers
 *   Array of tracking numbers for individual packages in the shipment.
 *   Optional for shipments of only one package, as they have the same tracking
 *   number.
 *
 * @return bool
 *   TRUE if the shipment or packages were successfully voided.
 */
function uc_ups_void_shipment($shipment_number, array $tracking_numbers = []) {
  $success = FALSE;
  $request = uc_ups_void_shipment_request($shipment_number, $tracking_numbers);
  $ups_config = \Drupal::config('uc_ups.settings');
  $resp = \Drupal::httpClient()
    ->post($ups_config
    ->get('connection_address') . 'Void', NULL, $request)
    ->send();
  $response = new \SimpleXMLElement($resp
    ->getBody(TRUE));
  if (isset($response->Response)) {
    if (isset($response->Response->ResponseStatusCode)) {
      $success = (string) $response->Response->ResponseStatusCode;
    }
    if (isset($response->Response->Error)) {
      foreach ($response->Response->Error as $error) {
        \Drupal::messenger()
          ->addError((string) $error->ErrorSeverity . ' ' . (string) $error->ErrorCode . ': ' . (string) $error->ErrorDescription);
      }
    }
  }
  if (isset($response->Status)) {
    if (isset($response->Status->StatusType)) {
      $success = (string) $response->Status->StatusType->Code;
    }
  }
  return (bool) $success;
}

Functions

Namesort descending Description
uc_ups_access_request Prepares XML access request string.
uc_ups_cron Implements hook_cron().
uc_ups_form_node_form_alter Implements hook_form_BASE_FORM_ID_alter() for node_form().
uc_ups_menu Implements hook_menu().
uc_ups_node_delete Implements hook_node_delete().
uc_ups_node_insert Implements hook_node_insert().
uc_ups_node_load Implements hook_node_load().
uc_ups_node_revision_delete Implements hook_node_revision_delete().
uc_ups_node_update Implements hook_node_update().
uc_ups_product_alter_validate Validation handler for UPS product fields.
uc_ups_quote Callback for retrieving a UPS shipping quote.
uc_ups_shipping_quote Constructs an XML quote request.
uc_ups_theme Implements hook_theme().
uc_ups_uc_shipping_method Implements hook_uc_shipping_method().
uc_ups_uc_shipping_type Implements hook_uc_shipping_type().
uc_ups_uc_store_status Implements hook_uc_store_status().
uc_ups_void_shipment Instructs UPS to cancel (in whole or in part) a shipment.
uc_ups_void_shipment_request Constructs a void shipment request.