diff --git a/shipping/uc_usps/UPGRADING.md b/shipping/uc_usps/UPGRADING.md
new file mode 100644
index 00000000..f111d146
--- /dev/null
+++ b/shipping/uc_usps/UPGRADING.md
@@ -0,0 +1,104 @@
+# Upgrading to the new USPS API
+
+## Overview
+
+For many years, the Drupal 7 and Backdrop versions of Ubercart have used the USPS Web Tools API for postal rate lookups. [That API is now being deprecated by USPS](https://www.usps.com/business/web-tools-apis/welcome.htm) in favor of their [new OAuth2-based API](https://developers.usps.com). This new API makes many changes that will affect Ubercart stores that use the `uc_usps` module to do rate lookups:
+
+* There are options that are no longer available (like "online prices");
+* There are new options that are available (like a distinction between Retail, Commercial, and Contract rates).
+
+To support Ubercart stores that use the `uc_usps` module, this module now supports both the **Legacy** API (the old Web Tools API) and the **new API**. New installations can choose which API to use. However:
+
+* **The old Web Tools API will be turned off by USPS on January 25, 2026.**
+
+All Ubercart stores should switch to the new API before then. Future releases of Backdrop CMS Ubercart will only support the new USPS API going forward.
+
+## Background: the Legacy API
+
+The Ubercart USPS module `uc_usps` recognizes two options for the **Default product shipping type**: _Envelope_ and _Small package_. Shipping type _Small package_ is a default shipping type defined by the `uc_quote` module. Module `uc_usps` adds the additional shipping type of _Envelope_.
+
+You can then further set a package type for the product (labeled **Package type** on the product edit form); this select element has many options, such as _Variable_, _Flat rate envelope_, and more. However, only _Small package_ lets you set the package type to anything other than _Variable_.
+
+That means that any _Envelope_ products can still be offered to be shipped in a box. You can't actually specify that it can _only_ be shipped in some type of envelope. However, this is useful, because if you have selected the _All in one_ option in the quote options for USPS, it will permit both _Envelope_ and _Small package_ products to be shipped in the same box.
+
+Another characteristic of the existing USPS module is that while it offers the user a choice of several different rates, it will ship all packages via the same rate, even if it would be cheaper to ship, say, one product in an envelope and another in a box.
+
+(Note that there was a bug in the rendering of of the services presented to the user to choose from, which did not correctly list the number of packages being shipped. This has been fixed in the current version.)
+
+When you upgrade Ubercart to this version, your settings will be left using the legacy API. You will need to manually switch to the new API at a time that is convenient for you. You can switch back and forth between the two APIs in order to see the effects of changes.
+
+To upgrade to the new API for rate lookups, you will need to select the new API in the settings for USPS shipping and then choose various configuration options, some of which are similar to the legacy API options and some of which are new. We strongly recommend doing this at a time where there are no carts in checkout or other pending statuses, as the results of a new lookup versus an old lookup may well be different in price, type of mail, et cetera. We suggest taking the store offline, then changing the configuration settings, and, if possible, carrying out several test orders to verify that all lookups are working well before bringing the store back online.
+
+## Setting up OAuth2 Credentials with USPS
+
+The new USPS API uses OAuth2 validation, which you will need to set up on the [USPS Developer Portal](https://developers.usps.com). In the old API, you had a USPS User ID, which never changed and was all that was needed to do a rate lookup. With the OAuth2 API, every lookup contains an OAuth2 access token, which is time-limited and expires after several hours. The USPS module takes care of obtaining the necessary token and renewing it regularly; this is done transparently, so you don't need to worry about it. However, in order to obtain the token, you will need to obtain a Consumer Key and a Consumer Secret (which are both long strings, the latter longer than the former), and enter these into the settings.
+
+You will do that by creating an account on the USPS site (click "Sign up" on the Developer Portal), but you may already have an account (you needed one to create the original User ID for the Web Tools API).
+
+Once you are signed in, go to developers.usps.com, and you will see Apps in the top menu bar. Click on that—you will need to create an "App" for API access. Click on the "Add App" button, which will open up a form:
+
+* App name — give your App a name (which could be the name of your store).
+* Callback URL — This is not used by `uc_usps`, so you can leave it blank.
+* Description — make this whatever you want.
+* APIs — Check the "Public Access I" box (the only one). This is what will give you access to the rate lookup endpoints (and several others not used by `uc_usps`, though they might be added in the future).
+
+Then click "Add App." This will create the new App.
+
+When the App is created, you will see some details about it. Two are very important:
+
+* Consumer Key — this is the "Consumer Key" that you will need to enter into Ubercart settings.
+* Consumer Secret — this is the "Consumer Secret" that you will need to enter into Ubercart settings.
+
+Both of these are required for Ubercart to obtain an access token in order to do rate lookups. Copy them both, and keep them in a safe place (ideally, an encrypted safe place).
+
+### Configure Ubercart USPS
+
+Now you are ready to configure the USPS shipping settings.
+
+## Configuration
+
+The configuration options can be found at Admin > Store > Configuration > Shipping quotes > Settings > USPS, i.e., at path admin/store/settings/quotes/settings/usps. Click through each of the vertical tab sets as described below, then click "Save Configuration" to switch to the new API. Your legacy API settings will remain saved, so you can easily switch back and forth.
+
+### Credentials
+
+The first setting in the Credentials tab is **Method**: select _New API_ to switch to the new API. When you do this, the fields below **Method** will change. (New installations of Ubercart will automatically be set to use the new API.)
+
+#### USPS Consumer Key
+
+Enter the "Consumer Key" from your USPS account.
+
+#### USPS Consumer Secret
+
+Enter the "Consumer Secret" from your USPS account. This will be saved but not displayed when you save the configuration; you shouldn't need to enter it again.
+
+#### Connection address
+
+Select either "Testing" or "Production". Use the former while you are testing out the system, the latter once you are processing real transactions.
+
+Once you have entered those values, click "Save Configuration." You should see a success message posted to the top of the stage and the **Access token** field will be populated. This token expires after 8 hours but will be automatically updated as needed.
+
+### USPS domestic
+
+This tab lists all of the services for _Envelope_ and _Small package_ products. These are generally different from the services offered by the legacy API, although there is a rough correspondence. For example, "First Class Mail Stamped Letter" in the legacy API is "First Class Letter Metered" in the new API. Select the services that you wish to offer for each type of package; you should find rough equivalents to the legacy API settings in the new services lists.
+
+### USPS international
+
+The same goes for international services.
+
+### Quote options
+
+#### Product packages
+
+As in the legacy API, you can choose whether each product should be shipped in its own package or all products shipped in a single package. Note that with multiple packages, all packages will be shipped via the same service, so you should not choose service options for _Envelope_ and _Small package_ that are incompatible if there is a possibility of both products being shipped together.
+
+#### Price type
+
+This is a new setting, which replaced the previous "Use online rates" checkbox. You can select Retail, Commercial, or Contract rates to quote. (Retail rates are often a bit higher than Commercial rates.)
+
+#### Markups
+
+Markups are the same as they were in the legacy API: you can mark up products by a percentage of their price and/or mark up the weight sent to the rate lookup to account for the weight of shipping materials, etc.
+
+When you are done, click "Save Configuration." You're done! You and your customers can now continue to put items in the cart and checkout as usual. During checkout, customers will be presented with a choice of all qualifying services and their rates, so do make sure that you have only allowed those services that you are prepared to provide.
+
+
diff --git a/shipping/uc_usps/config/uc_usps.settings.json b/shipping/uc_usps/config/uc_usps.settings.json
index 96186897..24c1292d 100644
--- a/shipping/uc_usps/config/uc_usps.settings.json
+++ b/shipping/uc_usps/config/uc_usps.settings.json
@@ -11,5 +11,75 @@
"uc_usps_weight_markup_type": "percentage",
"uc_usps_weight_markup": 0,
"uc_usps_markup": "",
- "uc_usps_markup_type": ""
-}
\ No newline at end of file
+ "uc_usps_markup_type": "",
+ "uc_usps_authorization_method": "newapi",
+ "uc_usps_consumer_key": "",
+ "uc_usps_consumer_secret": "",
+ "uc_usps_connection_address": "https://apis-tem.usps.com",
+ "uc_usps_price_type": "COMMERCIAL",
+ "uc_usps_newapi_dom_env_services": {
+ "FCLM": "FCLM",
+ "FCP": "FCP",
+ "FCF": "FCF",
+ "PMFRE": "PMFRE",
+ "PMPFRE": "PMPFRE",
+ "PMLFRE": "PMLFRE",
+ "PMMS": "PMMS",
+ "PMEFRE": "PMEFRE",
+ "PMEPFRE": "PMEPFRE",
+ "PMELFRE": "PMELFRE",
+ "PMELFREHD": "PMELFREHD",
+ "PMEMS": "PMEMS",
+ "UGAMS": "UGAMS"
+ },
+ "uc_usps_newapi_dom_parcel_services": {
+ "FCLM": "FCLM",
+ "FCF": "FCF",
+ "PMMSFRB": "PMMSFRB",
+ "PMMMFRB": "PMMMFRB",
+ "PMMLFRB": "PMMLFRB",
+ "PMMLFRBA": "PMMLFRBA",
+ "PMMS": "PMMS",
+ "PMMCNPT1": "PMMCNPT1",
+ "PMMCSPT1": "PMMCSPT1",
+ "PMEMS": "PMEMS",
+ "UGAMS": "UGAMS",
+ "UGAMCNPT1": "UGAMCNPT1",
+ "UGAMCSPT1": "UGAMCSPT1",
+ "BPMMP": "BPMMP",
+ "BPMNP": "BPMNP",
+ "LMNB": "LMNB",
+ "LMNS": "LMNS",
+ "LMMS": "LMMS",
+ "LMM5": "LMM5",
+ "LMN5": "LMN5",
+ "MMNB": "MMNB",
+ "MMNS": "MMNS",
+ "MMMS": "MMMS",
+ "MMM5": "MMM5",
+ "MMN5": "MMN5"
+ },
+ "uc_usps_newapi_intl_env_services": {
+ "FCLM": "FCLM",
+ "FCP": "FCP",
+ "FCF": "FCF",
+ "FPISMIS": "FPISMIS",
+ "PMIIFRE": "PMIIFRE",
+ "PMIMIPFRE": "PMIMIPFRE",
+ "PMIIS": "PMIIS",
+ "PMIMIS": "PMIMIS",
+ "PMIILFRE": "PMIILFRE",
+ "PMEIIFRE": "PMEIIFRE",
+ "PMEIILFRE": "PMEIILFRE",
+ "PMEIIS": "PMEIIS",
+ "PMEIIPFRE": "PMEIIPFRE"
+ },
+ "uc_usps_newapi_intl_parcel_services": {
+ "FPISMIS": "FPISMIS",
+ "PMIMILFRB": "PMIMILFRB",
+ "PMIMIMFRB": "PMIMIMFRB",
+ "PMIIS": "PMIIS",
+ "PMIMIS": "PMIMIS",
+ "PMEIIS": "PMEIIS"
+ }
+}
diff --git a/shipping/uc_usps/uc_usps.admin.inc b/shipping/uc_usps/uc_usps.admin.inc
index 83102dbc..27d75f18 100644
--- a/shipping/uc_usps/uc_usps.admin.inc
+++ b/shipping/uc_usps/uc_usps.admin.inc
@@ -24,77 +24,203 @@ function uc_usps_admin_settings($form, &$form_state) {
),
);
- // Container for credential forms.
+ // Container for credential fields.
$form['uc_usps_credentials'] = array(
'#type' => 'fieldset',
'#title' => t('Credentials'),
- '#description' => t('Account number and authorization information.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#group' => 'usps-settings',
);
- $form['uc_usps_credentials']['uc_usps_user_id'] = array(
+ $options = array(
+ 'legacy' => t('Legacy'),
+ 'newapi' => t('New API'),
+ );
+ $form['uc_usps_credentials']['uc_usps_authorization_method'] = array(
+ '#type' => 'radios',
+ '#title' => t('Method'),
+ '#description' => t('Select the method for authorization with USPS. Legacy credentials (user ID, used with the USPS Web Tools API) will be discontinued on 2026-01-25. You should update to the new API and OAuth2 credentials before then. More details here.'),
+ '#options' => $options,
+ '#default_value' => $config->get('uc_usps_authorization_method'),
+ );
+ $form['uc_usps_credentials']['legacy'] = array(
+ '#type' => 'fieldset',
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="uc_usps_authorization_method"]' => array('value' => 'legacy'),
+ ),
+ ),
+ );
+ $form['uc_usps_credentials']['legacy']['uc_usps_user_id'] = array(
'#type' => 'textfield',
'#title' => t('USPS user ID'),
'#description' => t('To acquire or locate your user ID, refer to the USPS documentation.', array('!url' => 'http://drupal.org/node/1308256')),
'#default_value' => $config->get('uc_usps_user_id'),
);
+ $form['uc_usps_credentials']['newapi'] = array(
+ '#type' => 'fieldset',
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="uc_usps_authorization_method"]' => array('value' => 'newapi'),
+ ),
+ ),
+ );
+ $form['uc_usps_credentials']['newapi']['uc_usps_consumer_key'] = array(
+ '#type' => 'textfield',
+ '#title' => t('USPS Consumer Key'),
+ '#description' => t('Enter your Consumer Key for OAuth2 authorization.'),
+ '#default_value' => $config->get('uc_usps_consumer_key'),
+ '#size' => 120,
+ );
+ $consumer_secret = $config->get('uc_usps_consumer_secret');
+ $form['uc_usps_credentials']['newapi']['uc_usps_consumer_secret'] = array(
+ '#type' => 'password',
+ '#title' => t('USPS Consumer Secret'),
+ '#description' => empty($consumer_secret) ?
+ t('Enter your Consumer Secret for token generation. It will be saved when you save settings but will not be shown after saving for security purposes.') :
+ t('You can update your Consumer Secret here if needed, but if the previously saved value is correct, you do not have to enter anything.'),
+ '#placeholder' => empty($consumer_secret) ? t('Enter the Consumer Secret') : t(''),
+ '#size' => 120,
+ );
+ $form['uc_usps_credentials']['newapi']['uc_usps_connection_address'] = array(
+ '#type' => 'select',
+ '#title' => t('Connection address'),
+ '#description' => t('Select Testing or Production mode for USPS API connection.'),
+ '#options' => array(
+ 'https://apis.usps.com' => t('Production'),
+ 'https://apis-tem.usps.com' => t('Testing'),
+ ),
+ '#default_value' => $config->get('uc_usps_connection_address'),
+ );
+ $access_token = uc_usps_access_token();
+ $token_expiry = new DateTime('@' . state_get('uc_usps_token_expiry'));
+ global $user;
+ $token_expiry->setTimeZone(new DateTimeZone($user->timezone));
+ $form['uc_usps_credentials']['newapi']['uc_usps_access_token'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Access token'),
+ '#description' => t('This token is automatically retrieved via OAuth authentication. The current token expires @expiry but will be renewed as needed.', array('@expiry' => date_format($token_expiry, 'F j, g:i a T'))),
+ '#default_value' => $access_token,
+ '#disabled' => TRUE,
+ '#size' => 120,
+ );
+ // Container for domestic quotes
$form['domestic'] = array(
'#type' => 'fieldset',
- '#title' => t('USPS Domestic'),
- '#description' => t('Set the conditions that will return a USPS quote.'),
+ '#title' => t('USPS domestic'),
+ '#description' => t('Set the conditions that will return a USPS domestic quote.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#group' => 'usps-settings',
);
- $form['domestic']['uc_usps_online_rates'] = array(
+ $form['domestic']['legacy'] = array(
+ '#type' => 'fieldset',
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="uc_usps_authorization_method"]' => array('value' => 'legacy'),
+ ),
+ ),
+ );
+ $form['domestic']['legacy']['uc_usps_online_rates'] = array(
'#type' => 'checkbox',
'#title' => t('Display USPS "online" rates'),
'#default_value' => $config->get('uc_usps_online_rates'),
'#description' => t('Show your customer standard USPS rates (default) or discounted "online" rates. Online rates apply only if you, the merchant, pay for and print out postage from the USPS Click-N-Ship web site.'),
);
-
- $form['domestic']['uc_usps_env_services'] = array(
+ $form['domestic']['legacy']['uc_usps_env_services'] = array(
'#type' => 'checkboxes',
'#title' => t('USPS envelope services'),
- '#default_value' => $config->get('uc_usps_env_services'),
'#options' => _uc_usps_env_services(),
+ '#default_value' => $config->get('uc_usps_env_services'),
'#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'),
);
- $form['domestic']['uc_usps_services'] = array(
+ $form['domestic']['legacy']['uc_usps_services'] = array(
'#type' => 'checkboxes',
- '#title' => t('USPS parcel services'),
- '#default_value' => $config->get('uc_usps_services'),
+ '#title' => t('USPS small package services'),
'#options' => _uc_usps_services(),
+ '#default_value' => $config->get('uc_usps_services'),
+ '#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'),
+ );
+ $form['domestic']['newapi'] = array(
+ '#type' => 'fieldset',
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="uc_usps_authorization_method"]' => array('value' => 'newapi'),
+ ),
+ ),
+ );
+ $form['domestic']['newapi']['uc_usps_newapi_dom_env_services'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('USPS envelope services'),
+ '#options' => uc_usps_newapi_dom_env_all_services(),
+ '#default_value' => $config->get('uc_usps_newapi_dom_env_services'),
'#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'),
);
+ $form['domestic']['newapi']['uc_usps_newapi_dom_parcel_services'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('USPS small package services'),
+ '#options' => uc_usps_newapi_dom_parcel_all_services(),
+ '#default_value' => $config->get('uc_usps_newapi_dom_parcel_services'),
+ '#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'),
+ );
+
+ // Container for international quotes
$form['international'] = array(
'#type' => 'fieldset',
- '#title' => t('USPS International'),
- '#description' => t('Set the conditions that will return a USPS International quote.'),
+ '#title' => t('USPS international'),
+ '#description' => t('Set the conditions that will return a USPS international quote.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#group' => 'usps-settings',
);
-
- $form['international']['uc_usps_intl_env_services'] = array(
+ $form['international']['legacy'] = array(
+ '#type' => 'fieldset',
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="uc_usps_authorization_method"]' => array('value' => 'legacy'),
+ ),
+ ),
+ );
+ $form['international']['legacy']['uc_usps_intl_env_services'] = array(
'#type' => 'checkboxes',
'#title' => t('USPS international envelope services'),
- '#default_value' => $config->get('uc_usps_intl_env_services'),
'#options' => _uc_usps_intl_env_services(),
+ '#default_value' => $config->get('uc_usps_intl_env_services'),
'#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'),
);
- $form['international']['uc_usps_intl_services'] = array(
+ $form['international']['legacy']['uc_usps_intl_services'] = array(
'#type' => 'checkboxes',
'#title' => t('USPS international parcel services'),
- '#default_value' => $config->get('uc_usps_intl_services'),
'#options' => _uc_usps_intl_services(),
+ '#default_value' => $config->get('uc_usps_intl_services'),
+ '#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'),
+ );
+ $form['international']['newapi'] = array(
+ '#type' => 'fieldset',
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="uc_usps_authorization_method"]' => array('value' => 'newapi'),
+ ),
+ ),
+ );
+ $form['international']['newapi']['uc_usps_newapi_intl_env_services'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('USPS international envelope services'),
+ '#options' => uc_usps_newapi_intl_env_all_services(),
+ '#default_value' => $config->get('uc_usps_newapi_intl_env_services'),
+ '#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'),
+ );
+ $form['international']['newapi']['uc_usps_newapi_intl_parcel_services'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('USPS international parcel services'),
+ '#options' => uc_usps_newapi_intl_parcel_all_services(),
+ '#default_value' => $config->get('uc_usps_newapi_intl_parcel_services'),
'#description' => t('Select the USPS services that are available to customers. Be sure to include the services that the Postal Service agrees are available to you.'),
);
@@ -108,6 +234,7 @@ function uc_usps_admin_settings($form, &$form_state) {
'#group' => 'usps-settings',
);
+ // All in one
$form['uc_usps_quote_options']['uc_usps_all_in_one'] = array(
'#type' => 'radios',
'#title' => t('Product packages'),
@@ -118,35 +245,51 @@ function uc_usps_admin_settings($form, &$form_state) {
),
'#description' => t('Indicate whether each product is quoted as shipping separately or all in one package. Orders with one kind of product will still use the package quantity to determine the number of packages needed, however.'),
);
-
+ // Price type (New API only).
+ $options = array(
+ 'RETAIL' => t('Retail'),
+ 'COMMERCIAL' => t('Commercial'),
+ 'CONTRACT' => t('Contract'),
+ );
+ $form['uc_usps_quote_options']['uc_usps_price_type'] = array(
+ '#type' => 'select',
+ '#title' => t('Price type'),
+ '#description' => t('Select the price type for the price quote from USPS.'),
+ '#default_value' => $config->get('uc_usps_price_type'),
+ '#options' => $options,
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="uc_usps_authorization_method"]' => array('value' => 'newapi'),
+ ),
+ ),
+ );
// Insurance.
$form['uc_usps_quote_options']['uc_usps_insurance'] = array(
'#type' => 'checkbox',
'#title' => t('Package insurance'),
'#default_value' => $config->get('uc_usps_insurance'),
'#description' => t('When enabled, the quotes presented to the customer will include the cost of insurance for the full sales price of all products in the order.'),
- '#disabled' => TRUE,
);
-
- // Delivery Confirmation.
+ // Delivery confirmation (legacy only)
$form['uc_usps_quote_options']['uc_usps_delivery_confirmation'] = array(
'#type' => 'checkbox',
'#title' => t('Delivery confirmation'),
'#default_value' => $config->get('uc_usps_delivery_confirmation'),
'#description' => t('When enabled, the quotes presented to the customer will include the cost of delivery confirmation for all packages in the order.'),
- '#disabled' => TRUE,
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="uc_usps_authorization_method"]' => array('value' => 'legacy'),
+ ),
+ ),
);
-
- // Signature Confirmation.
+ // Signature confirmation.
$form['uc_usps_quote_options']['uc_usps_signature_confirmation'] = array(
'#type' => 'checkbox',
'#title' => t('Signature confirmation'),
'#default_value' => $config->get('uc_usps_signature_confirmation'),
'#description' => t('When enabled, the quotes presented to the customer will include the cost of signature confirmation for all packages in the order.'),
- '#disabled' => TRUE,
);
-
- // Container for markup forms.
+ // Container for markup fields.
$form['uc_usps_markups'] = array(
'#type' => 'fieldset',
'#title' => t('Markups'),
@@ -156,24 +299,6 @@ function uc_usps_admin_settings($form, &$form_state) {
'#group' => 'usps-settings',
);
- $form['uc_usps_markups']['uc_usps_rate_markup_type'] = array(
- '#type' => 'select',
- '#title' => t('Rate markup type'),
- '#default_value' => $config->get('uc_usps_rate_markup_type'),
- '#options' => array(
- 'percentage' => t('Percentage (%)'),
- 'multiplier' => t('Multiplier (×)'),
- 'currency' => t('Addition (!currency)', array('!currency' => config_get('uc_store.settings', 'uc_currency_sign'))),
- ),
- );
- $form['uc_usps_markups']['uc_usps_rate_markup'] = array(
- '#type' => 'textfield',
- '#title' => t('Shipping rate markup'),
- '#default_value' => $config->get('uc_usps_rate_markup'),
- '#description' => t('Markup shipping rate quote by dollar amount, percentage, or multiplier.'),
- );
-
- // Form to select type of weight markup.
$form['uc_usps_markups']['uc_usps_weight_markup_type'] = array(
'#type' => 'select',
'#title' => t('Weight markup type'),
@@ -185,8 +310,6 @@ function uc_usps_admin_settings($form, &$form_state) {
),
'#disabled' => TRUE,
);
-
- // Form to select weight markup amount.
$form['uc_usps_markups']['uc_usps_weight_markup'] = array(
'#type' => 'textfield',
'#title' => t('Shipping weight markup'),
@@ -195,6 +318,23 @@ function uc_usps_admin_settings($form, &$form_state) {
'#disabled' => TRUE,
);
+ $form['uc_usps_markups']['uc_usps_rate_markup_type'] = array(
+ '#type' => 'select',
+ '#title' => t('Rate markup type'),
+ '#default_value' => $config->get('uc_usps_rate_markup_type'),
+ '#options' => array(
+ 'percentage' => t('Percentage (%)'),
+ 'multiplier' => t('Multiplier (×)'),
+ 'currency' => t('Addition (!currency)', array('!currency' => config_get('uc_store.settings', 'uc_currency_sign'))),
+ ),
+ );
+ $form['uc_usps_markups']['uc_usps_rate_markup'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Shipping rate markup'),
+ '#default_value' => $config->get('uc_usps_rate_markup'),
+ '#description' => t('Markup shipping rate quote by dollar amount, percentage, or multiplier.'),
+ );
+
// Taken from system_settings_form(). Only, don't use its submit handler.
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = array(
@@ -237,13 +377,27 @@ function uc_usps_admin_settings_validate($form, &$form_state) {
* @see uc_usps_admin_settings_validate()
*/
function uc_usps_admin_settings_submit($form, &$form_state) {
+ $config = config('uc_usps.settings');
+
+ // Record whether the testing/production server changed before we save new
+ // values.
+ $server_changed = $form_state['values']['uc_usps_connection_address'] != $config->get('uc_usps_connection_address');
+
$fields = array(
+ 'uc_usps_authorization_method',
'uc_usps_user_id',
+ 'uc_usps_consumer_key',
+ 'uc_usps_connection_address',
'uc_usps_online_rates',
'uc_usps_env_services',
'uc_usps_services',
'uc_usps_intl_env_services',
'uc_usps_intl_services',
+ 'uc_usps_newapi_dom_env_services',
+ 'uc_usps_newapi_dom_parcel_services',
+ 'uc_usps_newapi_intl_env_services',
+ 'uc_usps_newapi_intl_parcel_services',
+ 'uc_usps_price_type',
'uc_usps_rate_markup_type',
'uc_usps_rate_markup',
'uc_usps_weight_markup_type',
@@ -256,15 +410,44 @@ function uc_usps_admin_settings_submit($form, &$form_state) {
foreach ($fields as $key) {
$value = $form_state['values'][$key];
-
if (is_array($value) && isset($form_state['values']['array_filter'])) {
$value = array_keys(array_filter($value));
}
- config_set('uc_usps.settings', $key, $value);
+ $config->set($key, $value);
}
+ // Update OAuth2 client secret if we entered a value.
+ $consumer_secret = $form_state['values']['uc_usps_consumer_secret'];
+ if (!empty($consumer_secret)) {
+ $config->set('uc_usps_consumer_secret', $consumer_secret);
+ }
+ // Save all config changes now because _uc_usps_fetch_access_token() will also
+ // modify the config file.
+ $config->save();
backdrop_set_message(t('The configuration options have been saved.'));
+ // Check token expiration if we're using the new API.
+ if ($form_state['values']['uc_usps_authorization_method'] == 'newapi') {
+ $token_expiry = state_get('uc_usps_token_expiry');
+ $request_time = REQUEST_TIME;
+ $cron_interval = config_get('system.core', 'cron_safe_threshold');
+ // Get a new token if the current one will expire before the next cron run
+ // or if the server changed.
+ if (($token_expiry < $request_time + $cron_interval) || $server_changed) {
+ $token = _uc_usps_fetch_access_token();
+ if ($token) {
+ backdrop_set_message(t('OAuth token successfully retrieved.'));
+ }
+ else {
+ watchdog('uc_usps', 'Failed to retrieve OAuth token.', [], WATCHDOG_ERROR);
+ backdrop_set_message(t('Failed to retrieve OAuth token. Check the logs for details.'), 'error');
+ }
+ }
+ else {
+ backdrop_set_message(t('OAuth token is still valid. No refresh needed. Expires in @time minutes.', array('@time' => round(($token_expiry - $request_time) / 60))));
+ }
+ }
+
cache_clear_all();
backdrop_theme_rebuild();
}
diff --git a/shipping/uc_usps/uc_usps.admin.js b/shipping/uc_usps/uc_usps.admin.js
index a19eace0..6b15736b 100644
--- a/shipping/uc_usps/uc_usps.admin.js
+++ b/shipping/uc_usps/uc_usps.admin.js
@@ -7,15 +7,95 @@
Backdrop.behaviors.uspsAdminFieldsetSummaries = {
attach: function (context) {
+ // Credentials
+ $('fieldset#edit-uc-usps-credentials', context).backdropSetSummary(function(context) {
+ if ($('#edit-uc-usps-authorization-method-legacy').is(':checked')) {
+ return Backdrop.t('Using legacy API');
+ }
+ else {
+ return Backdrop.t('Using new API (OAuth2 credentials)');
+ }
+ });
+
+ // USPS Domestic
$('fieldset#edit-domestic', context).backdropSetSummary(function(context) {
- if ($('#edit-uc-usps-online-rates').is(':checked')) {
- return Backdrop.t('Using "online" rates');
+ if ($('#edit-uc-usps-authorization-method-legacy').is(':checked')) {
+ if ($('#edit-uc-usps-online-rates').is(':checked')) {
+ return Backdrop.t('Using "online" rates');
+ }
+ else {
+ return Backdrop.t('Using standard rates');
+ }
+ }
+ else {
+ var count_env = 0;
+ $('#edit-uc-usps-newapi-dom-env-services input').each(function() {
+ if ($(this).is(':checked')) {
+ count_env++;
+ }
+ });
+ var count_parcel = 0;
+ $('#edit-uc-usps-newapi-dom-parcel-services input').each(function() {
+ if ($(this).is(':checked')) {
+ count_parcel++;
+ }
+ });
+ return Backdrop.t('' + count_env + ' envelope services
' + count_parcel + ' small package services');
+ }
+ });
+
+ // USPS International
+ $('fieldset#edit-international', context).backdropSetSummary(function(context) {
+ if ($('#edit-uc-usps-authorization-method-legacy').is(':checked')) {
+ if ($('#edit-uc-usps-online-rates').is(':checked')) {
+ return Backdrop.t('Using "online" rates');
+ }
+ else {
+ return Backdrop.t('Using standard rates');
+ }
}
else {
- return Backdrop.t('Using standard rates');
+ var count_env = 0;
+ $('#edit-uc-usps-newapi-intl-env-services input').each(function() {
+ if ($(this).is(':checked')) {
+ count_env++;
+ }
+ });
+ var count_parcel = 0;
+ $('#edit-uc-usps-newapi-intl-parcel-services input').each(function() {
+ if ($(this).is(':checked')) {
+ count_parcel++;
+ }
+ });
+ return Backdrop.t('' + count_env + ' envelope services
' + count_parcel + ' small package services');
+ }
+ });
+
+ // Quote options
+ $('fieldset#edit-uc-usps-quote-options', context).backdropSetSummary(function(context) {
+ var options;
+ if ($('#edit-uc-usps-all-in-one-0').is(':checked')) {
+ options = Backdrop.t('Each in own');
+ }
+ else {
+ options = Backdrop.t('All in one');
+ }
+ if ($('#edit-uc-usps-authorization-method-newapi').is(':checked')) {
+ options += '
Price type: ' + $('#edit-uc-usps-price-type', context).val().toLowerCase();
+ }
+ if ($('#edit-uc-usps-insurance').is(':checked')) {
+ options += '
' + Backdrop.t('Package insurance');
+ }
+ if ($('#edit-uc-usps-authorization-method-legacy').is(':checked') && $('#edit-uc-usps-delivery-confirmation').is(':checked')) {
+ options += '
' + Backdrop.t('Delivery confirmation');
+ }
+ if ($('#edit-uc-usps-signature-confirmation').is(':checked')) {
+ options += '
' + Backdrop.t('Signature confirmation');
}
+ return options;
});
+ // Markups
$('fieldset#edit-uc-usps-markups', context).backdropSetSummary(function(context) {
return Backdrop.t('Rate markup') + ': '
+ $('#edit-uc-usps-rate-markup', context).val() + ' '
diff --git a/shipping/uc_usps/uc_usps.install b/shipping/uc_usps/uc_usps.install
index 898711c6..299b074d 100644
--- a/shipping/uc_usps/uc_usps.install
+++ b/shipping/uc_usps/uc_usps.install
@@ -57,6 +57,95 @@ function uc_usps_update_last_removed() {
return 7300;
}
+/**
+ * Implements hook_install().
+ */
+function uc_usps_install() {
+ $config = config('uc_usps.settings');
+ $config->set('uc_usps_authorization_method', 'newapi');
+ $config->save();
+
+ state_set('uc_usps_access_token', '');
+ state_set('uc_usps_token_expiry', 0);
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function uc_usps_uninstall() {
+ state_del('uc_usps_access_token');
+ state_del('uc_usps_token_expiry');
+}
+
+/**
+ * Implements hook_requirements().
+ */
+function uc_usps_requirements($phase) {
+ if ($phase != 'runtime') {
+ return array();
+ }
+ $config = config('uc_usps.settings');
+ $method = $config->get('uc_usps_authorization_method');
+ if ($method == 'legacy') {
+ $api_shutdown_time = 1769299200; // January 25, 2026
+ $days = ($api_shutdown_time - REQUEST_TIME) / 86400;
+ if ($days > 0) {
+ $description = t('You are using the legacy API, which will be shut down on January 25, 2026. You have @days days to upgrade to OAuth2 credentials at USPS and switch to the new API.', array('@days' => round($days)));
+ $severity = REQUIREMENT_WARNING;
+ }
+ else {
+ $description = t('You are using the legacy API, which is no longer supported by USPS. Please upgrade your USPS credentials to OAuth2 and switch to the new API in the UC USPS module settings.');
+ $severity = REQUIREMENT_ERROR;
+ }
+ return array(
+ 'uc_usps' => array(
+ 'title' => t('Ubercart USPS'),
+ 'value' => t('Using deprecated legacy authorization and API'),
+ 'description' => $description,
+ 'severity' => $severity,
+ ),
+ );
+ }
+ else { // $method == 'newapi'
+ $description = array();
+ $consumer_key = $config->get('uc_usps_consumer_key');
+ $consumer_secret = $config->get('uc_usps_consumer_secret');
+ $access_token = state_get('uc_usps_access_token');
+ $token_expiry = state_get('uc_usps_token_expiry');
+ if (empty($consumer_key)) {
+ $description[] = t('The Consumer Key is missing.');
+ }
+ if (empty($consumer_secret)) {
+ $description[] = t('The Consumer Secret is missing.');
+ }
+ if (empty($access_token)) {
+ $description[] = t('The access token is missing.');
+ }
+ if ($token_expiry < REQUEST_TIME) {
+ $description[] = t('The access token has expired.');
+ }
+ if (!empty($description)) {
+ $value = t('OAuth2 is not fully configured.');
+ $description = theme('item_list', array('items' => $description));
+ $severity = REQUIREMENT_WARNING;
+ }
+ else {
+ $value = t('OAuth2 configuration has been set.');
+ $description = '';
+ $severity = REQUIREMENT_OK;
+ }
+ $ret = array(
+ 'uc_usps' => array(
+ 'title' => t('Ubercart USPS'),
+ 'value' => $value,
+ 'description' => $description,
+ 'severity' => $severity,
+ ),
+ );
+ return $ret;
+ }
+}
+
/**
* Convert Ubercart USPS settings to config.
*/
@@ -98,11 +187,18 @@ function uc_usps_update_1000() {
}
/**
- * Implements hook_install().
+ * Add support for the new API and OAuth2 authorization while keeping legacy
+ * settings.
*/
-function uc_usps_install() {
- config_set('uc_usps.settings', 'uc_usps_env_services', array_keys(_uc_usps_env_services()));
- config_set('uc_usps.settings', 'uc_usps_services', array_keys(_uc_usps_services()));
- config_set('uc_usps.settings', 'uc_usps_intl_env_services', array_keys(_uc_usps_intl_env_services()));
- config_set('uc_usps.settings', 'uc_usps_intl_services', array_keys(_uc_usps_intl_services()));
+function uc_usps_update_1001() {
+ $config = config('uc_usps.settings');
+ $config->set('uc_usps_authorization_method', 'legacy');
+ $config->set('uc_usps_consumer_key', '');
+ $config->set('uc_usps_consumer_secret', '');
+ $config->set('uc_usps_connection_address', 'https://apis-tem.usps.com');
+ $config->set('uc_usps_price_type', 'COMMERCIAL');
+ $config->save();
+
+ state_set('uc_usps_token_expiry', 0);
+ state_set('uc_usps_access_token', '');
}
diff --git a/shipping/uc_usps/uc_usps.module b/shipping/uc_usps/uc_usps.module
index d7e51123..54c79761 100644
--- a/shipping/uc_usps/uc_usps.module
+++ b/shipping/uc_usps/uc_usps.module
@@ -4,8 +4,8 @@
* United States Postal Service (USPS) shipping quote module.
*/
-/******************************************************************************
- * Backdrop Hooks *
+/*******************************************************************************
+ * Backdrop Hooks *
******************************************************************************/
/**
@@ -119,8 +119,16 @@ function uc_usps_node_load($nodes, $types) {
}
$vids = array();
- $shipping_type = config_get('uc_shipping', 'uc_store_shipping_type');
- $shipping_types = db_query("SELECT id, shipping_type FROM {uc_quote_shipping_types} WHERE id_type = :type AND id IN (:ids)", array(':type' => 'product', ':ids' => array_keys($nodes)))->fetchAllKeyed();
+ $shipping_type = config_get('uc_shipping.settings', 'uc_store_shipping_type');
+ $shipping_types = db_query("
+ SELECT id, shipping_type
+ FROM {uc_quote_shipping_types}
+ WHERE id_type = :type
+ AND id IN (:ids)
+ ", array(
+ ':type' => 'product',
+ ':ids' => array_keys($nodes),
+ ))->fetchAllKeyed();
foreach ($nodes as $nid => $node) {
if (!in_array($node->type, $product_types)) {
@@ -165,6 +173,18 @@ function uc_usps_node_revision_delete($node) {
->execute();
}
+/**
+ * Implements hook_config_info().
+ */
+function uc_usps_config_info() {
+ $prefixes['uc_usps.settings'] = array(
+ 'label' => t('Ubercart USPS settings'),
+ 'group' => t('Configuration'),
+ );
+
+ return $prefixes;
+}
+
/******************************************************************************
* Ubercart Hooks *
******************************************************************************/
@@ -202,74 +222,115 @@ function uc_usps_uc_shipping_method() {
),
);
- $methods = array(
- 'usps_env' => array(
- 'id' => 'usps_env',
- 'module' => 'uc_usps',
- 'title' => t('U.S. Postal Service (Envelope)'),
- 'operations' => $operations,
- 'quote' => array(
- 'type' => 'envelope',
- 'callback' => 'uc_usps_quote',
- 'accessorials' => _uc_usps_env_services(),
+ // Check whether we're using the legacy API or the new API.
+ if (config_get('uc_usps.settings', 'uc_usps_authorization_method') == 'legacy') {
+ return array(
+ 'usps_env' => array(
+ 'id' => 'usps_env',
+ 'module' => 'uc_usps',
+ 'title' => t('U.S. Postal Service (Envelope)'),
+ 'operations' => $operations,
+ 'quote' => array(
+ 'type' => 'envelope',
+ 'callback' => 'uc_usps_quote',
+ 'accessorials' => _uc_usps_env_services(),
+ ),
),
- ),
- 'usps' => array(
- 'id' => 'usps',
- 'module' => 'uc_usps',
- 'title' => t('U.S. Postal Service (Parcel)'),
- 'operations' => $operations,
- 'quote' => array(
- 'type' => 'small_package',
- 'callback' => 'uc_usps_quote',
- 'accessorials' => _uc_usps_services(),
+ 'usps' => array(
+ 'id' => 'usps',
+ 'module' => 'uc_usps',
+ 'title' => t('U.S. Postal Service (Parcel)'),
+ 'operations' => $operations,
+ 'quote' => array(
+ 'type' => 'small_package',
+ 'callback' => 'uc_usps_quote',
+ 'accessorials' => _uc_usps_services(),
+ ),
),
- ),
- 'usps_intl_env' => array(
- 'id' => 'usps_intl_env',
- 'module' => 'uc_usps',
- 'title' => t('U.S. Postal Service (Intl., Envelope)'),
- 'operations' => $operations,
- 'quote' => array(
- 'type' => 'envelope',
- 'callback' => 'uc_usps_quote',
- 'accessorials' => _uc_usps_intl_env_services(),
+ 'usps_intl_env' => array(
+ 'id' => 'usps_intl_env',
+ 'module' => 'uc_usps',
+ 'title' => t('U.S. Postal Service (Intl., Envelope)'),
+ 'operations' => $operations,
+ 'quote' => array(
+ 'type' => 'envelope',
+ 'callback' => 'uc_usps_quote',
+ 'accessorials' => _uc_usps_intl_env_services(),
+ ),
+ 'weight' => 1,
),
- 'weight' => 1,
- ),
- 'usps_intl' => array(
- 'id' => 'usps_intl',
- 'module' => 'uc_usps',
- 'title' => t('U.S. Postal Service (Intl., Parcel)'),
- 'operations' => $operations,
- 'quote' => array(
- 'type' => 'small_package',
- 'callback' => 'uc_usps_quote',
- 'accessorials' => _uc_usps_intl_services(),
+ 'usps_intl' => array(
+ 'id' => 'usps_intl',
+ 'module' => 'uc_usps',
+ 'title' => t('U.S. Postal Service (Intl., Parcel)'),
+ 'operations' => $operations,
+ 'quote' => array(
+ 'type' => 'small_package',
+ 'callback' => 'uc_usps_quote',
+ 'accessorials' => _uc_usps_intl_services(),
+ ),
+ 'weight' => 1,
),
- 'weight' => 1,
- ),
- );
-
- return $methods;
+ );
+ }
+ else { // authorization_method == 'newapi'
+ // Use same IDs as legacy but different callback and services lists
+ // (accessorials).
+ return array(
+ 'usps_env' => array(
+ 'id' => 'usps_env',
+ 'module' => 'uc_usps',
+ 'title' => t('U.S. Postal Service (Envelope)'),
+ 'operations' => $operations,
+ 'quote' => array(
+ 'type' => 'envelope',
+ 'callback' => 'uc_usps_newapi_quote',
+ 'accessorials' => uc_usps_newapi_dom_env_all_services(),
+ ),
+ ),
+ 'usps' => array(
+ 'id' => 'usps',
+ 'module' => 'uc_usps',
+ 'title' => t('U.S. Postal Service (Parcel)'),
+ 'operations' => $operations,
+ 'quote' => array(
+ 'type' => 'small_package',
+ 'callback' => 'uc_usps_newapi_quote',
+ 'accessorials' => uc_usps_newapi_dom_parcel_all_services(),
+ ),
+ ),
+ 'usps_intl_env' => array(
+ 'id' => 'usps_intl_env',
+ 'module' => 'uc_usps',
+ 'title' => t('U.S. Postal Service (Intl., Envelope)'),
+ 'operations' => $operations,
+ 'quote' => array(
+ 'type' => 'envelope',
+ 'callback' => 'uc_usps_newapi_quote',
+ 'accessorials' => uc_usps_newapi_intl_env_all_services(),
+ ),
+ 'weight' => 1,
+ ),
+ 'usps_intl' => array(
+ 'id' => 'usps_intl',
+ 'module' => 'uc_usps',
+ 'title' => t('U.S. Postal Service (Intl., Parcel)'),
+ 'operations' => $operations,
+ 'quote' => array(
+ 'type' => 'small_package',
+ 'callback' => 'uc_usps_newapi_quote',
+ 'accessorials' => uc_usps_newapi_intl_parcel_all_services(),
+ ),
+ 'weight' => 1,
+ ),
+ );
+ }
}
/******************************************************************************
* Module Functions *
******************************************************************************/
-/**
- * Implements hook_config_info().
- */
-function uc_usps_config_info() {
- $prefixes['uc_usps.settings'] = array(
- 'label' => t('Ubercart USPS settings'),
- 'group' => t('Configuration'),
- );
-
- return $prefixes;
-}
-
/**
* Callback for retrieving USPS shipping quote.
*
@@ -280,8 +341,12 @@ function uc_usps_config_info() {
* @param $method
* The shipping method to create the quote.
*
- * @return
- * JSON object containing rate, error, and debugging information.
+ * @return array
+ * array of services, keyed on service code, each element being an array with
+ * the following keys:
+ * - rate - the rate with markup for this service
+ * - label — a text string used as the line-item label
+ * - option_label — formatted HTML for the option presented for user choice
*/
function uc_usps_quote($products, $details, $method) {
// The uc_quote AJAX query can fire before the customer has completely
@@ -579,6 +644,522 @@ function uc_usps_intl_rate_request($packages, $origin, $destination) {
return $request;
}
+/**
+ * Callback for retrieving USPS shipping quote using the new API.
+ *
+ * @param array $products
+ * Array of cart contents.
+ * @param array $details
+ * Order details other than product information, which in this case is the
+ * destination address object (UcAddress).
+ * @param array $method
+ * The shipping method to create the quote.
+ *
+ * @return array
+ * list of services, keyed on service_key, each element being an array with
+ * the following keys:
+ * - 'rate' - the rate with markup for this service
+ * - 'label' — a text string used as the line-item label
+ * - 'option_label' — formatted HTML for the option presented for user choice
+ * If there are any errors, information about them will returned in an array
+ * with the key 'data' and one or both of the following keyed elements:
+ * - 'debug' - a string of debug information
+ * - 'errors' - an array of strings of the individual errors received.
+ */
+function uc_usps_newapi_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();
+ }
+
+ $debug = user_access('configure quotes') && config_get('uc_quote.settings', 'uc_quote_display_debug');
+ // Initialize $debug_data, which we will fill in as we get quotes.
+ // $debug_data['debug'] is used for admin debugging; if $debug is TRUE, it
+ // will contain all requests and results formatted as a single HTML string.
+ // $debug_data['error'] will contain a list of any error messages that will be
+ // displayed to the end user. We only report the existence of errors, leaving
+ // it to admins to check the watchdog log for details.
+ $debug_data = array(
+ 'debug' => NULL,
+ 'error' => array(),
+ );
+ $services = array();
+ $addresses = array(
+ UcAddress::__set_state(config_get('uc_quote.settings', 'uc_quote_store_default_address')),
+ );
+
+ // $packages will be a structured array of packages grouped by pickup address.
+ $packages = _uc_usps_package_products($products, $addresses);
+ if (!count($packages)) {
+ return array();
+ }
+ $num_packages = 0; // The total number of packages.
+
+ foreach ($packages as $key => $ship_packages) {
+ $origin = $addresses[$key];
+ $origin->email = uc_store_email();
+
+ foreach ($ship_packages as $package) {
+ // USPS new API only lets us do a rate request for one package at a
+ // time so we'll have to do a rate lookup for each package and then
+ // combine the results.
+ $rate_info = uc_usps_newapi_package_quote($package, $origin, $destination, $method, $debug, $debug_data);
+ $num_packages++;
+ foreach ($rate_info as $key => $info) {
+ $rate = $info['totalBasePrice'];
+ if (!isset($services[$key])) {
+ $services[$key] = array(
+ 'rate' => uc_usps_rate_markup((string) $rate),
+ 'label' => $info['description'],
+ 'option_label' => theme('uc_usps_option_label', array(
+ 'service' => $info['description'],
+ 'packages' => $packages,
+ )),
+ 'count' => 1,
+ );
+ }
+ else {
+ $services[$key]['rate'] += $rate;
+ $services[$key]['count']++;
+ }
+ }
+ }
+ }
+
+ // USPS module ships all packages via the same rate, so filter the list to
+ // include only those services that were quoted for all packages.
+ foreach ($services as $key => $service) {
+ if ($service['count'] != $num_packages) {
+ unset($services[$key]);
+ }
+ else {
+ unset($services[$key]['count']);
+ }
+ }
+
+ // Filter the list to include only those services that were checked in the
+ // settings.
+ $method_services = array(
+ 'usps_env' => 'uc_usps_newapi_dom_env_services',
+ 'usps' => 'uc_usps_newapi_dom_parcel_services',
+ 'usps_intl_env' => 'uc_usps_newapi_intl_env_services',
+ 'usps_intl' => 'uc_usps_newapi_intl_parcel_services',
+ )[$method['id']];
+ $usps_services = array_filter(config_get('uc_usps.settings', $method_services));
+ foreach ($services as $service => $quote) {
+ if (!in_array($service, $usps_services)) {
+ unset($services[$service]);
+ }
+ }
+ if ($debug && empty($debug_data['error'])) {
+ // No errors, but no matching services. Add debugging information.
+ $debug_data['debug'] .= t('None of the recommended services match those chosen in the settings.') . "
\n";
+ }
+
+ // Sort by price.
+ uasort($services, 'uc_quote_price_sort');
+
+ // Merge debug data into $services. This is necessary because $debug_data is
+ // not sortable by a 'rate' key, so it has to be kept separate from the
+ // $services data until this point.
+ if (isset($debug_data['debug']) ||
+ (isset($debug_data['error']) && count($debug_data['error']))) {
+ $services['data'] = $debug_data;
+ }
+
+ return $services;
+}
+
+/**
+ * Get a USPS quote on a single package using the new API.
+ *
+ * @param object $package - the package to be quoted.
+ * @param UcAddress $origin - its origin address.
+ * @param object $destination - its destination.
+ * @param array $method - the quote method.
+ * @param bool $debug - whether to record debug information in $debug_data.
+ * @param array $debug_data - diagnostic information (if requested) and errors.
+ *
+ * @return array
+ * An array of rate information for this package.
+ * The fields of each row are those returned by USPS and vary by mail class.
+ * Each rate has a key so that multi-package quotes can be combined by key.
+ */
+function uc_usps_newapi_package_quote($package, $origin, $destination, $method, $debug, &$debug_data) {
+ if (empty($package) || empty($destination) || empty($destination->country)) {
+ return array();
+ }
+ $domestic = $destination->country == 840;
+ if ($domestic && empty($destination->postal_code)) {
+ // Domestic destinations need a ZIP code.
+ return array();
+ }
+ if ($domestic && strpos($method['id'], 'intl')) {
+ // Mismatch between destination and selected method.
+ return array();
+ }
+ $config = config('uc_usps.settings');
+ $price_type = $config->get('uc_usps_price_type');
+ $access_token = uc_usps_access_token();
+ $mailing_date = (new DateTime('@' . (REQUEST_TIME)))->format('Y-m-d');
+ $rate_info = array();
+
+ // Get dimensions and sort them into canonical order no matter how the
+ // packager might have packed products into the package.
+ $dimensions = array(
+ (float) $package->length,
+ (float) $package->width,
+ (float) $package->height,
+ );
+ rsort($dimensions);
+
+ if ($domestic) { // Domestic
+ // Get extra services for domestic.
+ $extra_services = array();
+ if ($config->get('uc_usps_insurance')) {
+ if ($package->price <= 500.0) {
+ $extra_services[] = 930; // Insurance <= $500
+ }
+ else {
+ $extra_services[] = 931; // Insurance > $500
+ }
+ }
+ if ($config->get('uc_usps_signature_confirmation')) {
+ $extra_services[] = 921; // Signature Confirmation
+ }
+
+ // Always do a parcel quote because flat-rate envelopes will only show up
+ // here.
+ $connection_url = $config->get('uc_usps_connection_address') . '/prices/v3/total-rates/search';
+ $request = array(
+ 'originZIPCode' => substr(trim($origin->postal_code), 0, 5),
+ 'destinationZIPCode' => substr(trim($destination->postal_code), 0, 5),
+ 'weight' => uc_usps_weight_markup($package->weight),
+ 'length' => $dimensions[0],
+ 'width' => $dimensions[1],
+ 'height' => $dimensions[2],
+ 'mailClasses' => [ 'ALL' ],
+ 'priceType' => $price_type,
+ 'mailingDate' => $mailing_date,
+ 'hasNonstandardCharacteristics' => $package->non_standard ?? FALSE,
+ 'extraServices' => $extra_services,
+ );
+ $request_data = backdrop_json_encode($request);
+ if ($debug) {
+ $debug_data['debug'] .= t("REQUEST (DOM,PARCEL):\n@request
", array('@request' => json_encode($request, JSON_PRETTY_PRINT)));
+ }
+ $result = backdrop_http_request($connection_url, array(
+ 'method' => 'POST',
+ 'data' => $request_data,
+ 'headers' => array(
+ 'Content-Type' => 'application/json',
+ 'Authorization' => 'Bearer ' . $access_token,
+ ),
+ ));
+ $data = backdrop_json_decode($result->data);
+ if ($debug) {
+ $debug_data['debug'] .= t("QUOTES:\n@data
", array('@data' => json_encode($data, JSON_PRETTY_PRINT)));
+ }
+ if (isset($data['error'])) {
+ $errors[] = $data['error'];
+ }
+ else {
+ // Accumulate $rate_info entries.
+ if (isset($data['rateOptions'])) {
+ foreach ($data['rateOptions'] as $rate_option) {
+ $rates = $rate_option['rates'];
+ $rate_row = $rate_option['rates'][0] + array(
+ 'totalBasePrice' => $rate_option['totalBasePrice'],
+ );
+ // Don't include special destination facilities for domestic mail.
+ if ($rate_row['destinationEntryFacilityType'] != 'NONE') {
+ continue;
+ }
+ // Don't include mailClass of SA, or USPS_MARKETING_MAIL.
+ if (in_array($rate_row['mailClass'], array('SA', 'USPS_MARKETING_MAIL'))) {
+ continue;
+ }
+ // If package is not machinable, don't include MACHINABLE processing.
+ if ($rate_row['processingCategory'] == 'MACHINABLE' && !$package->machinable) {
+ continue;
+ }
+ // If package is non-standard, only include NONSTANDARD processing.
+ if ($rate_row['processingCategory'] != 'NONSTANDARD' && package->non_standard) {
+ continue;
+ }
+ // And if we're still here, include the rate.
+ $service_key = _uc_usps_newapi_service_key($rate_row);
+ $rate_info[$service_key] = $rate_row;
+ }
+ }
+ }
+ // Add first-class letter/card/flat lookups only if the size of the package
+ // qualifies for the mail class to be looked-up.
+ $connection_url = $config->get('uc_usps_connection_address') . '/prices/v3/letter-rates/search';
+ $request = array(
+ 'weight' => uc_usps_weight_markup($package->weight),
+ 'length' => $dimensions[0],
+ 'height' => $dimensions[1],
+ 'thickness' => $dimensions[2],
+ 'mailingDate' => $mailing_date,
+ );
+ // Letter rates have to be requested one-by-one, so make sure that the
+ // rate is requested and the dimensions qualify to avoid unnecessary
+ // lookups.
+ foreach (array('LETTERS', 'CARDS', 'FLATS') as $rate) {
+ if (_uc_usps_envelope_size_qualifies($dimensions, $rate)) {
+ $request['processingCategory'] = $rate;
+ $request_data = backdrop_json_encode($request);
+ if ($debug) {
+ $debug_data['debug'] .= t("REQUEST (DOM,@rate):\n@request
", array(
+ '@rate' => $rate,
+ '@request' => json_encode($request, JSON_PRETTY_PRINT)),
+ );
+ }
+ $result = backdrop_http_request($connection_url, array(
+ 'method' => 'POST',
+ 'data' => $request_data,
+ 'headers' => array(
+ 'Content-Type' => 'application/json',
+ 'Authorization' => 'Bearer ' . $access_token,
+ ),
+ ));
+ $data = backdrop_json_decode($result->data);
+ if ($debug) {
+ $debug_data['debug'] .= t("QUOTES:\n@data
", array('@data' => json_encode($data, JSON_PRETTY_PRINT)));
+ }
+ if (isset($data['error'])) {
+ $errors[] = $data['error'];
+ }
+ else {
+ if (!empty($data['rates'][0])) {
+ $rate_row = $data['rates'][0] + array('totalBasePrice' => $data['totalBasePrice']);
+ $service_key = _uc_usps_newapi_service_key($rate_row);
+ $rate_info[$service_key] = $rate_row;
+ }
+ }
+ }
+ }
+ if ($debug) {
+ // Check whether any returned rates aren't in our lists of allowed rates.
+ uc_usps_check_returned_rates($rate_info, array_merge(uc_usps_newapi_dom_env_all_services(), uc_usps_newapi_dom_parcel_all_services()));
+ }
+ }
+ else { // International
+ // Parcel quote
+ $connection_url = $config->get('uc_usps_connection_address') . '/international-prices/v3/total-rates/search';
+
+ // New API requires 2-letter ISO country codes.
+ $country_code = db_query('
+ SELECT country_iso_code_2
+ FROM {uc_countries}
+ WHERE country_id = :id
+ ', array(':id' => $destination->country))
+ ->fetchField();
+
+ $request = array(
+ 'originZIPCode' => substr(trim($origin->postal_code), 0, 5),
+ 'foreignPostalCode' => trim($destination->postal_code),
+ 'destinationCountryCode' => $country_code,
+ 'weight' => uc_usps_weight_markup($package->weight),
+ 'length' => $dimensions[0],
+ 'width' => $dimensions[1],
+ 'height' => $dimensions[2],
+ 'mailClass' => 'ALL',
+ 'priceType' => $price_type,
+ 'mailingDate' => $mailing_date,
+ 'hasNonstandardCharacteristics' => $package->non_standard ?? FALSE,
+ 'extraServices' => [],
+ );
+ $request_data = backdrop_json_encode($request);
+ if ($debug) {
+ $debug_data['debug'] .= t("REQUEST (INTL,PARCEL):\n@request
", array('@request' => json_encode($request, JSON_PRETTY_PRINT)));
+ }
+ $result = backdrop_http_request($connection_url, array(
+ 'method' => 'POST',
+ 'data' => $request_data,
+ 'headers' => array(
+ 'Content-Type' => 'application/json',
+ 'Authorization' => 'Bearer ' . $access_token,
+ ),
+ ));
+ $data = backdrop_json_decode($result->data);
+ if ($debug) {
+ $debug_data['debug'] .= t("QUOTES:\n@data
", array('@data' => json_encode($data, JSON_PRETTY_PRINT)));
+ }
+ if (isset($data['error'])) {
+ $errors[] = $data['error'];
+ }
+ else {
+ // Accumulate $rate_info entries.
+ if (isset($data['rateOptions'])) {
+ foreach ($data['rateOptions'] as $rate_option) {
+ $rates = $rate_option['rates'];
+ $rate_row = $rate_option['rates'][0] + array(
+ 'totalBasePrice' => $rate_option['totalBasePrice'],
+ );
+ // If package is not machinable, don't include MACHINABLE processing.
+ if ($rate_row['processingCategory'] == 'MACHINABLE' && !$package->machinable) {
+ continue;
+ }
+ // If package is non-standard, only include NONSTANDARD processing.
+ if ($rate_row['processingCategory'] != 'NONSTANDARD' && $package->non_standard) {
+ continue;
+ }
+ // And if we're still here, include the rate.
+ $service_key = _uc_usps_newapi_service_key($rate_row);
+ $rate_info[$service_key] = $rate_row;
+ }
+ }
+ }
+ // Letter/card/flat quote
+ $connection_url = $config->get('uc_usps_connection_address') . '/international-prices/v3/letter-rates/search';
+ $request = array(
+ 'destinationCountryCode' => $country_code,
+ 'weight' => uc_usps_weight_markup($package->weight),
+ 'length' => $dimensions[0],
+ 'height' => $dimensions[1],
+ 'thickness' => $dimensions[2],
+ 'mailingDate' => $mailing_date,
+ );
+ // Letter rates have to be requested one-by-one, so make sure that the
+ // dimensions qualify to avoid unnecessary lookups.
+ foreach (array('LETTERS', 'CARDS', 'FLATS') as $rate) {
+ if (_uc_usps_envelope_size_qualifies($dimensions, $rate)) {
+ $request['processingCategory'] = $rate;
+ $request_data = backdrop_json_encode($request);
+ if ($debug) {
+ $debug_data['debug'] .= t("REQUEST (INTL,@rate):\n@request
", array(
+ '@rate' => $rate,
+ '@request' => json_encode($request, JSON_PRETTY_PRINT)),
+ );
+ }
+ $result = backdrop_http_request($connection_url, array(
+ 'method' => 'POST',
+ 'data' => $request_data,
+ 'headers' => array(
+ 'Content-Type' => 'application/json',
+ 'Authorization' => 'Bearer ' . $access_token,
+ ),
+ ));
+ $data = backdrop_json_decode($result->data);
+ if ($debug) {
+ $debug_data['debug'] .= t("QUOTES:\n@data
", array('@data' => json_encode($data, JSON_PRETTY_PRINT)));
+ }
+ if (isset($data['error'])) {
+ $errors[] = $data['error'];
+ }
+ else {
+ if (!empty($data['rates'][0])) {
+ $rate_row = $data['rates'][0] + array('totalBasePrice' => $data['totalBasePrice']);
+ $service_key = _uc_usps_newapi_service_key($rate_row);
+ $rate_info[$service_key] = $rate_row;
+ }
+ }
+ }
+ }
+ if ($debug) {
+ // Check whether any returned rates aren't in our lists of allowed rates.
+ uc_usps_check_returned_rates($rate_info, array_merge(uc_usps_newapi_intl_env_all_services(), uc_usps_newapi_intl_parcel_all_services()));
+ }
+ }
+
+ // In the legacy API, we passed the product packaging type to the USPS rate
+ // lookup, but there is no provision for that in the new API. Instead, we
+ // filter the rate quotes according to their description text.
+ $filter_text = _uc_usps_pkg_type_filter_text()[$package->container];
+ if ($filter_text != '*') {
+ if ($filter_text == '') {
+ // This package type is no longer supported.
+ $rate_info = array();
+ }
+ else {
+ // Particular packaging type is called for; filter description text.
+ foreach ($rate_info as $service_key => $info) {
+ if (strpos(strtolower($info['description']), $filter_text) === FALSE) {
+ unset($rate_info[$service_key]);
+ }
+ }
+ }
+ }
+
+ // Reformat errors as individual strings and report them.
+ if (!empty($errors)) {
+ foreach ($errors as $error) {
+ $quote_error = $error['code'] . ' ' . $error['message'];
+ // This will put the quote error into the output buffer, where it will be
+ // picked up and reported to watchdog if we're turned on error logging.
+ // See uc_quote_action_get_quote().
+ echo $quote_error;
+ }
+ }
+
+ return $rate_info;
+}
+
+/**
+ * Debugging utility to check whether returned rates aren't in our lists of
+ * allowed rates. If such rates are missing, post a notice in the watchdog
+ * log that contains the PHP code to add the missing rates to the appropriate
+ * list of supported rates (env or parcel, dom or intl).
+ *
+ * @param array $rate_info - The formatted rates returned by USPS.
+ * @param array $allowed_rates - The list of rates that this module supports.
+ */
+function uc_usps_check_returned_rates($rate_info, $allowed_rates) {
+ $missing = array();
+ foreach ($rate_info as $key => $data) {
+ if (!isset($allowed_rates[$key])) {
+ $missing[] = check_plain("'" . $key . "' => '" . $data['description'] . "',");
+ }
+ }
+ if (!empty($missing)) {
+ watchdog('uc_usps', "Missing rates:
!rates", array('!rates' => implode("
", $missing)), WATCHDOG_WARNING);
+ }
+}
+
+/**
+ * Utility to construct the service key from a rate_info row.
+ *
+ * The service key needs to be distinguish between all possible services
+ * returned from a rate lookup and must be constructible from the parameters
+ * returned by a rate lookup. Some of the obvious candidates don't work:
+ * 'productName' is blank for many services. 'SKU' can be the same for multiple
+ * services and can be different for the same service for different 'priceType'
+ * settings. The 'description' field exists for all lookups and is the same for
+ * the same service for different 'priceType' settings; for brevity, we create
+ * a code from the first letter of each description (which is still unique for
+ * each service).
+ *
+ * @param array $info
+ * A single row returned from a USPS rate request via the new API.
+ *
+ * @return string
+ * The service key, which is used as the row key for rate requests and the
+ * list of services returned by uc_usps_newapi_quote().
+ */
+function _uc_usps_newapi_service_key($info) {
+ $ret = '';
+ $dwords = explode(' ', ucwords($info['description']));
+ foreach ($dwords as $dword) {
+ $ret .= substr($dword, 0, 1);
+ }
+ return $ret;
+}
+
/**
* Modifies the rate received from USPS before displaying to the customer.
*
@@ -641,6 +1222,85 @@ function uc_usps_weight_markup($weight) {
}
}
+/**
+ * Get a valid access token from config or, if the old one has expired, fetch
+ * a new token from the USPS server.
+ *
+ * @return string | FALSE - the token (if successful), FALSE if not.
+ */
+function uc_usps_access_token() {
+ $config = config('uc_usps.settings');
+ $token_expiry = state_get('uc_usps_token_expiry');
+ if ($token_expiry >= REQUEST_TIME + 60) {
+ // If expiration is more than a minute away, return the currently stored
+ // token. Tokens are valid for 8 hours after issuance.
+ return state_get('uc_usps_access_token');
+ }
+ else {
+ // If expiration is less than minute away, get a new token and return it.
+ // The new token will be stored and the expiration updated.
+ return _uc_usps_fetch_access_token();
+ }
+}
+
+/**
+ * Fetch an OAuth2 access token from the USPS website and store it in config.
+ *
+ * @return string|false
+ * Return the token if successful, otherwise return FALSE.
+ * FALSE.
+ *
+ * @see uc_ups_get_access_token()
+ */
+function _uc_usps_fetch_access_token() {
+ $config = config('uc_usps.settings');
+ $consumer_key = $config->get('uc_usps_consumer_key');
+ $consumer_secret = $config->get('uc_usps_consumer_secret');
+ $auth_url = $config->get('uc_usps_connection_address') . '/oauth2/v3/token';
+
+ if (empty($consumer_key) || empty($consumer_secret)) {
+ watchdog('uc_usps', 'ERROR: Missing USPS Consumer Key or Consumer Secret.', [], WATCHDOG_ERROR);
+ return FALSE;
+ }
+
+ $data = array(
+ 'grant_type' => 'client_credentials',
+ 'client_id' => $consumer_key,
+ 'client_secret' => $consumer_secret,
+ );
+ $options = array(
+ 'method' => 'POST',
+ 'data' => backdrop_http_build_query($data),
+ 'headers' => array(
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ ),
+ );
+
+ $result = backdrop_http_request($auth_url, $options);
+ if ($result->code == 200) {
+ $response = backdrop_json_decode($result->data);
+ if (!empty($response['access_token'])) {
+ // Store token in Backdrop state.
+ state_set('uc_usps_access_token', $response['access_token']);
+ state_set('uc_usps_token_expiry', round($response['issued_at'] / 1000) + $response['expires_in']);
+ // Debug: Confirm token storage
+ watchdog('uc_usps', '✅ Successfully retrieved and stored USPS OAuth token.', [], WATCHDOG_NOTICE);
+ return $response['access_token'];
+ }
+ else {
+ watchdog('uc_usps', '❌ USPS OAuth response missing access token. Response: @response', ['@response' => print_r($response, TRUE)], WATCHDOG_ERROR);
+ }
+ }
+ else {
+ watchdog('uc_usps', '❌ USPS OAuth request failed with HTTP Code @code. Response: @response', [
+ '@code' => $result->code,
+ '@response' => print_r($result, TRUE),
+ ], WATCHDOG_ERROR);
+ }
+
+ return FALSE;
+}
+
/**
* Organizes products into packages for shipment.
*
@@ -700,6 +1360,21 @@ function _uc_usps_package_products($products, &$addresses = array()) {
$product->length_units = $temp->length_units;
$product->usps['container'] = isset($temp->usps['container']) ? $temp->usps['container'] : 'VARIABLE';
+ if (config_get('uc_usps.settings', 'uc_usps_authorization_method') == 'newapi') {
+ // Stack products of varying size to get required package dimensions to
+ // accommodate all products in the package.
+ $dimensions = array(
+ $product->length,
+ $product->width,
+ $product->height,
+ );
+ rsort($dimensions);
+ $packages[$key][0]->length = max($packages[$key][0]->length, $dimensions[0]);
+ $packages[$key][0]->width = max($packages[$key][0]->width, $dimensions[1]);
+ $packages[$key][0]->height += $dimensions[2];
+ $packages[$key][0]->girth = 2 * $packages[$key][0]->width + 2 * $packages[$key][0]->height;
+ }
+
$packages[$key][0]->price += $product->price * $product->qty;
$packages[$key][0]->weight += $product->weight * $product->qty * uc_weight_conversion($product->weight_units, 'lb');
}
@@ -754,13 +1429,13 @@ function _uc_usps_package_products($products, &$addresses = array()) {
// 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);
+ $package->pounds = floor($weight); // DEPRECATED
+ $package->ounces = LB_TO_OZ * ($weight - $package->pounds); // DEPRECATED
break;
case 'oz':
- $package->pounds = floor($weight * OZ_TO_LB);
- $package->ounces = $weight - $package->pounds * LB_TO_OZ;
+ $package->pounds = floor($weight * OZ_TO_LB); // DEPRECATED
+ $package->ounces = $weight - $package->pounds * LB_TO_OZ; // DEPRECATED
break;
}
@@ -844,34 +1519,136 @@ function _uc_usps_package_products($products, &$addresses = array()) {
}
}
}
+ // Certain packages are non-standard, and we'll need to supply that to the
+ // parcel quote.
+ foreach ($packages as $idx => $package_group) {
+ foreach ($package_group as $jdx => $product) {
+ if ($product->container == 'NONRECTANGULAR') {
+ $packages[$idx][$jdx]->non_standard = TRUE;
+ }
+ else {
+ $packages[$idx][$jdx]->non_standard = FALSE;
+ }
+ }
+ }
return $packages;
}
/**
- * Convenience function for select form elements.
+ * Select form elements for package types, which are a setting for each product.
+ * This value is passed to rate lookup for the legacy API, but there is no
+ * equivalent value passed to the new API. We'll have to map these to the rates
+ * returned from the lookup function.
+ *
+ * @return array
*/
function _uc_usps_pkg_types() {
+ if (config_get('uc_usps.settings', 'uc_usps_authorization_method') == 'legacy') {
+ // Return the original set of packaging types, but note which ones are no
+ // longer selectable in the new API.
+ return array(
+ 'VARIABLE' => t('Variable'),
+ 'FLAT RATE ENVELOPE' => t('Flat rate envelope'),
+ 'PADDED FLAT RATE ENVELOPE' => t('Padded flat rate envelope'),
+ 'LEGAL FLAT RATE ENVELOPE' => t('Legal flat rate envelope'),
+ 'SMALL FLAT RATE ENVELOPE' => t('Small flat rate envelope (DEPRECATED)'),
+ 'WINDOW FLAT RATE ENVELOPE' => t('Window flat rate envelope (DEPRECATED)'),
+ 'GIFT CARD FLAT RATE BOX' => t('Gift card flat rate box (DEPRECATED)'),
+ 'FLAT RATE BOX' => t('Flat rate box'),
+ 'SM FLAT RATE BOX' => t('Small flat rate box'),
+ 'MD FLAT RATE BOX' => t('Medium flat rate box'),
+ 'LG FLAT RATE BOX' => t('Large flat rate box'),
+ 'REGIONALRATEBOXA' => t('Regional rate box A (DEPRECATED)'),
+ 'REGIONALRATEBOXB' => t('Regional rate box B (DEPRECATED)'),
+ 'RECTANGULAR' => t('Rectangular'),
+ 'NONRECTANGULAR' => t('Non-rectangular'),
+ );
+ }
+ else { // New API, only include the supported package types.
+ return array(
+ 'VARIABLE' => t('Variable'),
+ 'FLAT RATE ENVELOPE' => t('Flat rate envelope'),
+ 'PADDED FLAT RATE ENVELOPE' => t('Padded flat rate envelope'),
+ 'LEGAL FLAT RATE ENVELOPE' => t('Legal flat rate envelope'),
+ 'FLAT RATE BOX' => t('Flat rate box'),
+ 'SM FLAT RATE BOX' => t('Small flat rate box'),
+ 'MD FLAT RATE BOX' => t('Medium flat rate box'),
+ 'LG FLAT RATE BOX' => t('Large flat rate box'),
+ 'RECTANGULAR' => t('Rectangular'),
+ 'NONRECTANGULAR' => t('Non-rectangular'),
+ );
+ }
+}
+
+/**
+ * Return whether a set of dimensions qualifies for a letter/card/flat rate.
+ * @param array $dimensions - the array of length, width, thickness, as floats.
+ * @param string $rate - one of 'LETTERS', 'CARDS', or 'FLATS'.
+ *
+ * @return array|false
+ */
+function _uc_usps_envelope_size_qualifies($dimensions, $rate) {
+ switch ($rate) {
+ case 'LETTERS':
+ return
+ $dimensions[0] >= 5.0 && $dimensions[0] <= 11.5 &&
+ $dimensions[1] >= 2.5 && $dimensions[1] <= 6.125 &&
+ $dimensions[2] >= 0.007 && $dimensions[2] <= 0.25;
+ break;
+
+ case 'CARDS':
+ return
+ $dimensions[0] >= 5.0 && $dimensions[0] <= 6.0 &&
+ $dimensions[1] >= 3.5 && $dimensions[1] <= 4.25 &&
+ $dimensions[2] >= 0.007 && $dimensions[2] <= 0.016;
+ break;
+
+ case 'FLATS':
+ return
+ ($dimensions[0] >= 11.5 || $dimensions[1] >= 6.125 || $dimensions[2] >= 0.25) &&
+ $dimensions[0] <= 15.0 &&
+ $dimensions[1] <= 12.0 &&
+ $dimensions[2] <= 0.75;
+ break;
+
+ default:
+ return FALSE;
+ }
+}
+
+/**
+ * Returns a list of filter text for the possible package types. For each type,
+ * check the description field of the rate quote against the filter text:
+ * '*' — all descriptions are acceptable.
+ * '' — no descriptions are acceptable (this type is no longer supported).
+ * '' — must be present in the description, case-insensitive.
+ *
+ * @return array
+ */
+function _uc_usps_pkg_type_filter_text() {
return array(
- 'VARIABLE' => t('Variable'),
- 'FLAT RATE ENVELOPE' => t('Flat rate envelope'),
- 'PADDED FLAT RATE ENVELOPE' => t('Padded flat rate envelope'),
- 'LEGAL FLAT RATE ENVELOPE' => t('Legal flat rate envelope'),
- 'SMALL FLAT RATE ENVELOPE' => t('Small flat rate envelope'),
- 'WINDOW FLAT RATE ENVELOPE' => t('Window flat rate envelope'),
- 'GIFT CARD FLAT RATE BOX' => t('Gift card flat rate box'),
- 'FLAT RATE BOX' => t('Flat rate box'),
- 'SM FLAT RATE BOX' => t('Small flat rate box'),
- 'MD FLAT RATE BOX' => t('Medium flat rate box'),
- 'LG FLAT RATE BOX' => t('Large flat rate box'),
- 'REGIONALRATEBOXA' => t('Regional rate box A'),
- 'REGIONALRATEBOXB' => t('Regional rate box B'),
- 'RECTANGULAR' => t('Rectangular'),
- 'NONRECTANGULAR' => t('Non-rectangular'),
+ 'VARIABLE' => '*',
+ 'FLAT RATE ENVELOPE' => 'flat rate envelope',
+ 'PADDED FLAT RATE ENVELOPE' => 'padded flat rate envelope',
+ 'LEGAL FLAT RATE ENVELOPE' => 'legal flat rate envelope',
+ 'SMALL FLAT RATE ENVELOPE' => '',
+ 'WINDOW FLAT RATE ENVELOPE' => '',
+ 'GIFT CARD FLAT RATE BOX' => '',
+ 'FLAT RATE BOX' => 'flat rate box',
+ 'SM FLAT RATE BOX' => 'small flat rate box',
+ 'MD FLAT RATE BOX' => 'medium flat rate box',
+ 'LG FLAT RATE BOX' => 'large flat rate box',
+ 'REGIONALRATEBOXA' => '',
+ 'REGIONALRATEBOXB' => '',
+ 'RECTANGULAR' => '*',
+ 'NONRECTANGULAR' => '*',
);
}
/**
- * Maps envelope shipment services to their IDs.
+ * Maps envelope shipment services to their IDs (legacy).
+ *
+ * @return array
*/
function _uc_usps_env_services() {
return array(
@@ -888,7 +1665,9 @@ function _uc_usps_env_services() {
}
/**
- * Maps parcel shipment services to their IDs.
+ * Maps parcel shipment services to their IDs (legacy).
+ *
+ * @return array
*/
function _uc_usps_services() {
return array(
@@ -908,7 +1687,9 @@ function _uc_usps_services() {
}
/**
- * Maps international envelope services to their IDs.
+ * Returns international envelope services to their IDs (legacy).
+ *
+ * @return array
*/
function _uc_usps_intl_env_services() {
return array(
@@ -924,7 +1705,9 @@ function _uc_usps_intl_env_services() {
}
/**
- * Maps international parcel services to their IDs.
+ * Maps international parcel services to their IDs (legacy).
+ *
+ * @return array
*/
function _uc_usps_intl_services() {
return array(
@@ -940,6 +1723,98 @@ function _uc_usps_intl_services() {
);
}
+/**
+ * Maps domestic envelope services to their IDs (newapi).
+ */
+function uc_usps_newapi_dom_env_all_services() {
+ return array(
+ 'FCLM' => 'First Class Letter Metered',
+ 'FCP' => 'First Class Postcards',
+ 'FCF' => 'First Class Flats',
+ 'PMFRE' => 'Priority Mail Flat Rate Envelope',
+ 'PMPFRE' => 'Priority Mail Padded Flat Rate Envelope',
+ 'PMLFRE' => 'Priority Mail Legal Flat Rate Envelope',
+ 'PMMS' => 'Priority Mail Machinable Single-piece',
+ 'PMEFRE' => 'Priority Mail Express Flat Rate Envelope',
+ 'PMEPFRE' => 'Priority Mail Express Padded Flat Rate Envelope',
+ 'PMELFRE' => 'Priority Mail Express Legal Flat Rate Envelope',
+ 'PMELFREHD' => 'Priority Mail Express Legal Flat Rate Envelope Holiday Delivery',
+ 'PMEMS' => 'Priority Mail Express Machinable Single-piece',
+ 'UGAMS' => 'USPS Ground Advantage Machinable Single-piece',
+ );
+}
+
+/**
+ * Maps domestic parcel services to their IDs (newapi).
+ */
+function uc_usps_newapi_dom_parcel_all_services() {
+ return array(
+ 'FCLM' => 'First Class Letter Metered',
+ 'FCF' => 'First Class Flats',
+ 'PMMSFRB' => 'Priority Mail Machinable Small Flat Rate Box',
+ 'PMMMFRB' => 'Priority Mail Machinable Medium Flat Rate Box',
+ 'PMMLFRB' => 'Priority Mail Machinable Large Flat Rate Box',
+ 'PMMLFRBA' => 'Priority Mail Machinable Large Flat Rate Box APO/FPO/DPO',
+ 'PMMS' => 'Priority Mail Machinable Single-piece',
+ 'PMNS' => 'Priority Mail Nonstandard Single-piece',
+ 'PMMCNPT1' => 'Priority Mail Machinable Cubic Non-Soft Pack Tier 1',
+ 'PMMCSPT1' => 'Priority Mail Machinable Cubic Soft Pack Tier 1',
+ 'PMEMS' => 'Priority Mail Express Machinable Single-piece',
+ 'UGAMS' => 'USPS Ground Advantage Machinable Single-piece',
+ 'UGANS' => 'USPS Ground Advantage Nonstandard Single-piece',
+ 'UGAMCNPT1' => 'USPS Ground Advantage Machinable Cubic Non-Soft Pack Tier 1',
+ 'UGAMCSPT1' => 'USPS Ground Advantage Machinable Cubic Soft Pack Tier 1',
+ 'BPMMP' => 'Bound Printed Matter Machinable Presorted',
+ 'BPMNP' => 'Bound Printed Matter Nonstandard Presorted',
+ 'LMNB' => 'Library Mail Nonstandard Basic',
+ 'LMNS' => 'Library Mail Nonstandard Single-piece',
+ 'LMMS' => 'Library Mail Machinable Single-piece',
+ 'LMM5' => 'Library Mail Machinable 5-digit',
+ 'LMN5' => 'Library Mail Nonstandard 5-digit',
+ 'MMNB' => 'Media Mail Nonstandard Basic',
+ 'MMNS' => 'Media Mail Nonstandard Single-piece',
+ 'MMMS' => 'Media Mail Machinable Single-piece',
+ 'MMM5' => 'Media Mail Machinable 5-digit',
+ 'MMN5' => 'Media Mail Nonstandard 5-digit',
+ );
+}
+
+/**
+ * Maps international envelope services to their IDs (newapi).
+ */
+function uc_usps_newapi_intl_env_all_services() {
+ return array(
+ 'FCLM' => 'First Class Letter Metered',
+ 'FCP' => 'First Class Postcards',
+ 'FCF' => 'First Class Flats',
+ 'FPISMIS' => 'First-Class Package International Service Machinable ISC Single-piece',
+ 'PMIIFRE' => 'Priority Mail International ISC Flat Rate Envelope',
+ 'PMIMIPFRE' => 'Priority Mail International Machinable ISC Padded Flat Rate Envelope',
+ 'PMIIS' => 'Priority Mail International ISC Single-piece',
+ 'PMIMIS' => 'Priority Mail International Machinable ISC Single-piece',
+ 'PMIILFRE' => 'Priority Mail International ISC Legal Flat Rate Envelope',
+ 'PMEIIFRE' => 'Priority Mail Express International ISC Flat Rate Envelope',
+ 'PMEIILFRE' => 'Priority Mail Express International ISC Legal Flat Rate Envelope',
+ 'PMEIIS' => 'Priority Mail Express International ISC Single-piece',
+ 'PMEIIPFRE' => 'Priority Mail Express International ISC Padded Flat Rate Envelope',
+ );
+}
+
+/**
+ * Maps international parcel services to their IDs (newapi).
+ */
+function uc_usps_newapi_intl_parcel_all_services() {
+ return array(
+ 'FPISMIS' => 'First-Class Package International Service Machinable ISC Single-piece',
+ 'PMIMILFRB' => 'Priority Mail International Machinable ISC Large Flat Rate Box',
+ 'PMIMIMFRB' => 'Priority Mail International Machinable ISC Medium Flat Rate Box',
+ 'PMIIS' => 'Priority Mail International ISC Single-piece',
+ 'PMIMIS' => 'Priority Mail International Machinable ISC Single-piece',
+ 'PMINIS' => 'Priority Mail International Nonstandard ISC Single-piece',
+ 'PMEIIS' => 'Priority Mail Express International ISC Single-piece',
+ );
+}
+
/**
* Pseudo-constructor to set default values of a package.
*/
@@ -950,6 +1825,7 @@ function _uc_usps_new_package() {
$package->qty = 1;
$package->pounds = 0;
$package->ounces = 0;
+ $package->weight = 0;
$package->container = 0;
$package->size = 0;
$package->machinable = TRUE;
diff --git a/shipping/uc_usps/uc_usps.theme.inc b/shipping/uc_usps/uc_usps.theme.inc
index 3bf27c6c..4dd72c22 100644
--- a/shipping/uc_usps/uc_usps.theme.inc
+++ b/shipping/uc_usps/uc_usps.theme.inc
@@ -30,8 +30,12 @@ function theme_uc_usps_option_label($variables) {
// Add USPS service name, removing any 'U.S.P.S.' prefix.
$output .= preg_replace('/^U\.S\.P\.S\./', '', $service);
- // Add package information
- $output .= ' (' . format_plural(count($packages), '1 package', '@count packages') . ')';
+ // Add package information.
+ $num_packages = 0;
+ foreach ($packages as $package_group) {
+ $num_packages += count($package_group);
+ }
+ $output .= ' (' . format_plural($num_packages, '1 package', '@count packages') . ')';
return $output;
}
diff --git a/uc_reports/uc_reports.admin.inc b/uc_reports/uc_reports.admin.inc
index b9e06dfa..080780c0 100644
--- a/uc_reports/uc_reports.admin.inc
+++ b/uc_reports/uc_reports.admin.inc
@@ -1521,7 +1521,7 @@ function uc_reports_store_csv($report_id, $rows) {
$user_id = empty($user->uid) ? session_id() : $user->uid;
foreach ($rows as $row) {
foreach ($row as $index => $column) {
- $row[$index] = is_null($column) ? '""' : '"' . str_replace('"', '""', $column) . '"';
+ $row[$index] = empty($column) ? '""' : '"' . str_replace('"', '""', $column) . '"';
}
$csv_output .= implode(',', $row) . "\n";
}