rewrite estimator helper logic
This commit is contained in:
924
.llmdump
Normal file
924
.llmdump
Normal file
@@ -0,0 +1,924 @@
|
|||||||
|
|
||||||
|
//- - - - - - - - - - START: usps_api_bridge/override/modules/zh_uspslabels/zh_uspslabels.php - - - - - - - - - -//
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if (!defined('_PS_VERSION_')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Zh_UspsLabelsOverride extends Zh_UspsLabels
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Intercept the rate calculation call.
|
||||||
|
*/
|
||||||
|
public function getPackageShippingCost($params, $shipping_cost, $products)
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
// 1. Check if Bridge Module exists and is active
|
||||||
|
/** @var Usps_Api_Bridge $bridge */
|
||||||
|
$bridge = Module::getInstanceByName('usps_api_bridge');
|
||||||
|
|
||||||
|
if ($bridge && $bridge->active) {
|
||||||
|
|
||||||
|
// 2. Check Debug IP Logic
|
||||||
|
// If configured, only these IPs use the new API.
|
||||||
|
// Everyone else continues using the old logic (parent).
|
||||||
|
if ($bridge->isIpAllowed()) {
|
||||||
|
|
||||||
|
// 3. Attempt to calculate rate via Bridge
|
||||||
|
// We pass '$this' (the original module instance) to access its config settings
|
||||||
|
$newRate = $bridge->calculateRate($params, $shipping_cost, $products, $this);
|
||||||
|
|
||||||
|
// If Bridge returns a valid numeric rate, use it.
|
||||||
|
// If it returns FALSE (api error, no token, etc), fall back to old logic.
|
||||||
|
if ($newRate !== false && $newRate !== null) {
|
||||||
|
return $newRate;
|
||||||
|
}
|
||||||
|
$bridge->externalLog(['calculateRate Failed' => ['params' => $params, 'shipping_cost' => $shipping_cost, 'products' => $products, 'this' => $this]]);
|
||||||
|
$bridge->log("Bridge returned false/null. Falling back to Legacy API.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback to Legacy Logic
|
||||||
|
return parent::getPackageShippingCost($params, $shipping_cost, $products);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercept the "Check API" button in Back Office
|
||||||
|
*/
|
||||||
|
public function ajaxProcessCheckApiConnection()
|
||||||
|
{
|
||||||
|
/** @var Usps_Api_Bridge $bridge */
|
||||||
|
|
||||||
|
$bridge = Module::getInstanceByName('usps_api_bridge');
|
||||||
|
|
||||||
|
if ($bridge && $bridge->active && $bridge->isIpAllowed()) {
|
||||||
|
// We can implement a specific test function in the bridge later
|
||||||
|
// For now, we just let it connect to OAuth
|
||||||
|
// $bridge->testApiConnection();
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::ajaxProcessCheckApiConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//- - - - - - - - - - END: usps_api_bridge/override/modules/zh_uspslabels/zh_uspslabels.php - - - - - - - - - -//
|
||||||
|
|
||||||
|
|
||||||
|
//- - - - - - - - - - START: usps_api_bridge/usps_api_bridge.php - - - - - - - - - -//
|
||||||
|
<?php
|
||||||
|
if (!defined('_PS_VERSION_')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
|
|
||||||
|
class Usps_Api_Bridge extends Module
|
||||||
|
{
|
||||||
|
// Runtime cache to prevent duplicate API calls during a single page load
|
||||||
|
private $apiRuntimeCache = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->name = 'usps_api_bridge';
|
||||||
|
$this->tab = 'shipping_logistics';
|
||||||
|
$this->version = '1.0.0';
|
||||||
|
$this->author = 'Panariga';
|
||||||
|
$this->need_instance = 0;
|
||||||
|
$this->bootstrap = true;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->displayName = $this->l('USPS API Bridge (OAuth2)');
|
||||||
|
$this->description = $this->l('Modern OAuth2 Bridge for the legacy ZH USPS Labels module.');
|
||||||
|
|
||||||
|
$this->confirmUninstall = $this->l('Are you sure? This will disable the connection to the new USPS API.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function install()
|
||||||
|
{
|
||||||
|
return parent::install() &&
|
||||||
|
$this->registerHook('actionAdminControllerSetMedia') && // Just in case we need JS later
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_LIVE_MODE', 0) &&
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_DEBUG_IPS', '') &&
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_LOGGING', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uninstall()
|
||||||
|
{
|
||||||
|
// Uninstall the override automatically to prevent errors
|
||||||
|
return parent::uninstall() &&
|
||||||
|
Configuration::deleteByName('USPS_BRIDGE_CLIENT_ID') &&
|
||||||
|
Configuration::deleteByName('USPS_BRIDGE_CLIENT_SECRET') &&
|
||||||
|
Configuration::deleteByName('USPS_BRIDGE_ACCESS_TOKEN');
|
||||||
|
Configuration::deleteByName('USPS_BRIDGE_EXTERNAL_DEBUG_URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent()
|
||||||
|
{
|
||||||
|
if (Tools::isSubmit('submitUspsBridgeConf')) {
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_CLIENT_ID', Tools::getValue('USPS_BRIDGE_CLIENT_ID'));
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_CLIENT_SECRET', Tools::getValue('USPS_BRIDGE_CLIENT_SECRET'));
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_LIVE_MODE', Tools::getValue('USPS_BRIDGE_LIVE_MODE'));
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_DEBUG_IPS', Tools::getValue('USPS_BRIDGE_DEBUG_IPS'));
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_LOGGING', Tools::getValue('USPS_BRIDGE_LOGGING'));
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_EXTERNAL_DEBUG_URL', Tools::getValue('USPS_BRIDGE_EXTERNAL_DEBUG_URL'));
|
||||||
|
|
||||||
|
// Clear token on save to force refresh with new credentials
|
||||||
|
Configuration::deleteByName('USPS_BRIDGE_ACCESS_TOKEN');
|
||||||
|
Configuration::deleteByName('USPS_BRIDGE_TOKEN_EXPIRY');
|
||||||
|
|
||||||
|
return $this->displayConfirmation($this->l('Settings updated & Token cache cleared'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderForm()
|
||||||
|
{
|
||||||
|
$fields_form = [
|
||||||
|
'form' => [
|
||||||
|
'legend' => [
|
||||||
|
'title' => $this->l('USPS OAuth2 Configuration'),
|
||||||
|
'icon' => 'icon-cogs',
|
||||||
|
],
|
||||||
|
'input' => [
|
||||||
|
[
|
||||||
|
'type' => 'switch',
|
||||||
|
'label' => $this->l('Live Mode (Production API)'),
|
||||||
|
'name' => 'USPS_BRIDGE_LIVE_MODE',
|
||||||
|
'is_bool' => true,
|
||||||
|
'values' => [
|
||||||
|
['id' => 'active_on', 'value' => 1, 'label' => $this->l('Yes')],
|
||||||
|
['id' => 'active_off', 'value' => 0, 'label' => $this->l('No')],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => $this->l('Consumer Key (Client ID)'),
|
||||||
|
'name' => 'USPS_BRIDGE_CLIENT_ID',
|
||||||
|
'required' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => $this->l('Consumer Secret'),
|
||||||
|
'name' => 'USPS_BRIDGE_CLIENT_SECRET',
|
||||||
|
'required' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'textarea',
|
||||||
|
'label' => $this->l('Debug Allowed IPs'),
|
||||||
|
'name' => 'USPS_BRIDGE_DEBUG_IPS',
|
||||||
|
'desc' => $this->l('Comma separated IPs. If set, ONLY these IPs will use the New API. Everyone else uses the old module logic. Leave empty to enable for everyone.'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'switch',
|
||||||
|
'label' => $this->l('Enable API Logging'),
|
||||||
|
'name' => 'USPS_BRIDGE_LOGGING',
|
||||||
|
'is_bool' => true,
|
||||||
|
'values' => [
|
||||||
|
['id' => 'active_on', 'value' => 1, 'label' => $this->l('Yes')],
|
||||||
|
['id' => 'active_off', 'value' => 0, 'label' => $this->l('No')],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'textarea',
|
||||||
|
'label' => $this->l('External URL for Debug Log'),
|
||||||
|
'name' => 'USPS_BRIDGE_EXTERNAL_DEBUG_URL',
|
||||||
|
'desc' => $this->l(''),
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
],
|
||||||
|
'submit' => [
|
||||||
|
'title' => $this->l('Save'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$helper = new HelperForm();
|
||||||
|
$helper->show_toolbar = false;
|
||||||
|
$helper->table = $this->table;
|
||||||
|
$helper->module = $this;
|
||||||
|
$helper->default_form_language = $this->context->language->id;
|
||||||
|
$helper->identifier = $this->identifier;
|
||||||
|
$helper->submit_action = 'submitUspsBridgeConf';
|
||||||
|
$helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false) . '&configure=' . $this->name . '&tab_module=' . $this->tab . '&module_name=' . $this->name;
|
||||||
|
$helper->token = Tools::getAdminTokenLite('AdminModules');
|
||||||
|
|
||||||
|
$helper->tpl_vars = [
|
||||||
|
'fields_value' => [
|
||||||
|
'USPS_BRIDGE_CLIENT_ID' => Configuration::get('USPS_BRIDGE_CLIENT_ID'),
|
||||||
|
'USPS_BRIDGE_CLIENT_SECRET' => Configuration::get('USPS_BRIDGE_CLIENT_SECRET'),
|
||||||
|
'USPS_BRIDGE_LIVE_MODE' => Configuration::get('USPS_BRIDGE_LIVE_MODE'),
|
||||||
|
'USPS_BRIDGE_DEBUG_IPS' => Configuration::get('USPS_BRIDGE_DEBUG_IPS'),
|
||||||
|
'USPS_BRIDGE_LOGGING' => Configuration::get('USPS_BRIDGE_LOGGING'),
|
||||||
|
'USPS_BRIDGE_EXTERNAL_DEBUG_URL' => Configuration::get('USPS_BRIDGE_EXTERNAL_DEBUG_URL'),
|
||||||
|
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $helper->generateForm([$fields_form]);
|
||||||
|
}
|
||||||
|
public function calculateRate($params, $shipping_cost, $products, $originalModule)
|
||||||
|
{
|
||||||
|
require_once(dirname(__FILE__) . '/classes/UspsV3Client.php');
|
||||||
|
|
||||||
|
// 1. Get OAuth Token
|
||||||
|
$token = $this->getAccessToken();
|
||||||
|
if (!$token) return false;
|
||||||
|
|
||||||
|
$carrierId = (int)$originalModule->id_carrier;
|
||||||
|
if (!$carrierId && isset($params->id_carrier)) {
|
||||||
|
$carrierId = (int)$params->id_carrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Get Method Code
|
||||||
|
$sql = 'SELECT code FROM `' . _DB_PREFIX_ . 'uspsl_method` WHERE id_carrier = ' . (int)$carrierId;
|
||||||
|
$methodCode = \Db::getInstance()->getValue($sql);
|
||||||
|
if (!$methodCode) return false;
|
||||||
|
|
||||||
|
// --- 4. CHECK LEGACY DB CACHE ---
|
||||||
|
$zhCache = false;
|
||||||
|
$canCache = class_exists('\UspsPsLabels\Cache') && class_exists('\UspsPsLabels\CacheRate');
|
||||||
|
|
||||||
|
if ($canCache) {
|
||||||
|
$zhCache = \UspsPsLabels\Cache::cacheCart($params->id);
|
||||||
|
if (\Validate::isLoadedObject($zhCache)) {
|
||||||
|
$sql = 'SELECT rate FROM `' . _DB_PREFIX_ . 'uspsl_cache_rate`
|
||||||
|
WHERE id_cache = ' . (int)$zhCache->id . '
|
||||||
|
AND id_carrier = ' . (int)$carrierId;
|
||||||
|
$cachedRate = \Db::getInstance()->getValue($sql);
|
||||||
|
if ($cachedRate !== false && $cachedRate !== null) {
|
||||||
|
return (float)$cachedRate + $shipping_cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -------------------------------
|
||||||
|
|
||||||
|
// 5. Determine International Status & Address Data (Cookie/Object Hybrid)
|
||||||
|
$destZip = '';
|
||||||
|
$destCountryIso = '';
|
||||||
|
|
||||||
|
if (!empty($params->id_address_delivery)) {
|
||||||
|
$address = new Address($params->id_address_delivery);
|
||||||
|
if (Validate::isLoadedObject($address)) {
|
||||||
|
$destZip = $address->postcode;
|
||||||
|
$destCountryIso = Country::getIsoById($address->id_country);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($destZip) && Tools::getIsset('postcode')) {
|
||||||
|
$destZip = Tools::getValue('postcode');
|
||||||
|
}
|
||||||
|
if (empty($destCountryIso) && Tools::getIsset('id_country')) {
|
||||||
|
$destCountryIso = Country::getIsoById((int)Tools::getValue('id_country'));
|
||||||
|
}
|
||||||
|
$context = Context::getContext();
|
||||||
|
if (empty($destZip) && isset($context->cookie->postcode)) {
|
||||||
|
$destZip = $context->cookie->postcode;
|
||||||
|
}
|
||||||
|
if (empty($destCountryIso) && isset($context->cookie->id_country)) {
|
||||||
|
$destCountryIso = Country::getIsoById((int)$context->cookie->id_country);
|
||||||
|
} else if (empty($destCountryIso) && isset($params->id_country)) {
|
||||||
|
$destCountryIso = Country::getIsoById((int)$params->id_country);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($destCountryIso)) {
|
||||||
|
$destCountryIso = 'US';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($destZip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean Data
|
||||||
|
$originZip = $this->getOriginZip($originalModule);
|
||||||
|
$originZip = substr(preg_replace('/[^0-9]/', '', $originZip), 0, 5);
|
||||||
|
$destZip = substr(preg_replace('/[^0-9]/', '', $destZip), 0, 5);
|
||||||
|
$isInternational = ($destCountryIso !== 'US');
|
||||||
|
|
||||||
|
// Map Code
|
||||||
|
$newApiClass = $this->mapServiceCodeToApiClass($methodCode, $isInternational);
|
||||||
|
if (!$newApiClass) return false;
|
||||||
|
|
||||||
|
// 6. Pack Products
|
||||||
|
$packedBoxes = $originalModule->getHelper()->getCarrierHelper()->packProducts($products, $params->id);
|
||||||
|
if (empty($packedBoxes)) return false;
|
||||||
|
|
||||||
|
// 7. Setup Client
|
||||||
|
$client = new UspsV3Client($token, (bool)\Configuration::get('USPS_BRIDGE_LIVE_MODE'));
|
||||||
|
$totalPrice = 0;
|
||||||
|
|
||||||
|
$legacyPriceSetting = (int)\Configuration::get('USPSL_COMMERCIAL');
|
||||||
|
$requestedPriceType = ($legacyPriceSetting > 0) ? 'COMMERCIAL' : 'RETAIL';
|
||||||
|
|
||||||
|
// 8. Loop through boxes
|
||||||
|
foreach ($packedBoxes as $packedBox) {
|
||||||
|
|
||||||
|
$weightInLbs = $this->convertUnit($packedBox->getWeight(), 'g', 'lbs', 3);
|
||||||
|
if ($weightInLbs < 0.1) $weightInLbs = 0.1;
|
||||||
|
|
||||||
|
$box = $packedBox->getBox();
|
||||||
|
$length = $this->convertUnit($box->getOuterLength(), 'mm', 'in', 2);
|
||||||
|
$width = $this->convertUnit($box->getOuterWidth(), 'mm', 'in', 2);
|
||||||
|
$height = $this->convertUnit($box->getOuterDepth(), 'mm', 'in', 2);
|
||||||
|
|
||||||
|
$category = 'NONSTANDARD';
|
||||||
|
if ($newApiClass === 'USPS_GROUND_ADVANTAGE') {
|
||||||
|
if ($length <= 22 && $width <= 18 && $height <= 15 && $weightInLbs >= 0.375 && $weightInLbs <= 25) {
|
||||||
|
$category = 'MACHINABLE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'originZIPCode' => $originZip,
|
||||||
|
'weight' => $weightInLbs,
|
||||||
|
'length' => $length,
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height,
|
||||||
|
'mailClass' => $newApiClass,
|
||||||
|
'priceType' => $requestedPriceType,
|
||||||
|
'mailingDate' => date('Y-m-d', strtotime('+1 day')),
|
||||||
|
'processingCategory' => $category,
|
||||||
|
'rateIndicator' => 'SP'
|
||||||
|
];
|
||||||
|
|
||||||
|
$flatRateIndicator = $this->mapBoxToRateIndicator($box->getReference());
|
||||||
|
if ($flatRateIndicator) {
|
||||||
|
$payload['rateIndicator'] = $flatRateIndicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->sendApiRequest($client, $payload, $isInternational, $destCountryIso, $destZip);
|
||||||
|
|
||||||
|
if (isset($response['error']) && $payload['priceType'] === 'COMMERCIAL') {
|
||||||
|
$payload['priceType'] = 'RETAIL';
|
||||||
|
$response = $this->sendApiRequest($client, $payload, $isInternational, $destCountryIso, $destZip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($response['error'])) {
|
||||||
|
// $this->log("API Fatal Error: " . $response['error']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($response['totalBasePrice'])) {
|
||||||
|
$totalPrice += (float)$response['totalBasePrice'];
|
||||||
|
} elseif (isset($response['rateOptions'][0]['totalBasePrice'])) {
|
||||||
|
$totalPrice += (float)$response['rateOptions'][0]['totalBasePrice'];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 9. SAVE TO LEGACY DB CACHE ---
|
||||||
|
if ($canCache && \Validate::isLoadedObject($zhCache)) {
|
||||||
|
$newCacheRate = new \UspsPsLabels\CacheRate();
|
||||||
|
$newCacheRate->id_cache = $zhCache->id;
|
||||||
|
$newCacheRate->id_carrier = $carrierId;
|
||||||
|
$newCacheRate->code = $methodCode;
|
||||||
|
$newCacheRate->rate = $totalPrice;
|
||||||
|
$newCacheRate->save();
|
||||||
|
}
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
return $totalPrice + $shipping_cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to send request with Runtime Caching & Domestic/Intl switching
|
||||||
|
*/
|
||||||
|
private function sendApiRequest($client, $payload, $isInternational, $destCountryIso, $destZip)
|
||||||
|
{
|
||||||
|
|
||||||
|
// 1. Prepare the specific payload for the cache key
|
||||||
|
// We simulate the modifications we are about to do to ensure the key is accurate
|
||||||
|
$cachePayload = $payload;
|
||||||
|
$cachePayload['destinationEntryFacilityType'] = 'NONE';
|
||||||
|
if ($isInternational) {
|
||||||
|
$cachePayload['destinationCountryCode'] = $destCountryIso; // Use string directly
|
||||||
|
$cachePayload['originZIPCode'] = $payload['originZIPCode']; // Ensure consistency
|
||||||
|
// unset($cachePayload['destinationEntryFacilityType']);
|
||||||
|
unset($cachePayload['destinationZIPCode']);
|
||||||
|
$endpointType = 'INT';
|
||||||
|
} else {
|
||||||
|
$cachePayload['destinationZIPCode'] = $destZip;
|
||||||
|
// $cachePayload['destinationEntryFacilityType'] = 'NONE';
|
||||||
|
$endpointType = 'DOM';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Generate Hash
|
||||||
|
// We include the endpoint type to ensure uniqueness
|
||||||
|
$cacheKey = md5(json_encode($cachePayload) . $endpointType);
|
||||||
|
|
||||||
|
// 3. Check Cache
|
||||||
|
if (isset($this->apiRuntimeCache[$cacheKey])) {
|
||||||
|
|
||||||
|
return $this->apiRuntimeCache[$cacheKey];
|
||||||
|
}
|
||||||
|
$this->externalLog(['sendApiRequest' => ['payload' => $payload, 'isInternational' => $isInternational, 'destCountryIso' => $destCountryIso, 'destZip' => $destZip]]);
|
||||||
|
|
||||||
|
// 4. Perform Request
|
||||||
|
if ($isInternational) {
|
||||||
|
$response = $client->getInternationalRate($cachePayload);
|
||||||
|
} else {
|
||||||
|
$response = $client->getDomesticRate($cachePayload);
|
||||||
|
}
|
||||||
|
$this->externalLog(['sendApiRequest' => ['response' => $response]]);
|
||||||
|
|
||||||
|
// 5. Determine if we should cache
|
||||||
|
// We DO cache API errors (like 400 Bad Request) because retrying them won't fix invalid data.
|
||||||
|
// We DO NOT cache Network/Transport errors (timeouts) so they can be retried.
|
||||||
|
$shouldCache = true;
|
||||||
|
|
||||||
|
if (isset($response['error'])) {
|
||||||
|
// Check for Guzzle/Symfony Transport errors strings defined in UspsV3Client
|
||||||
|
if (
|
||||||
|
strpos($response['error'], 'Network') !== false ||
|
||||||
|
strpos($response['error'], 'Connection') !== false ||
|
||||||
|
strpos($response['error'], 'Transport') !== false
|
||||||
|
) {
|
||||||
|
$shouldCache = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shouldCache) {
|
||||||
|
$this->apiRuntimeCache[$cacheKey] = $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Unit Converter to replace the dependency on the old module's class
|
||||||
|
*/
|
||||||
|
private function convertUnit($value, $from, $to, $precision = 2)
|
||||||
|
{
|
||||||
|
$units = [
|
||||||
|
'lb' => 453.59237,
|
||||||
|
'lbs' => 453.59237,
|
||||||
|
'oz' => 28.3495231,
|
||||||
|
'kg' => 1000,
|
||||||
|
'g' => 1,
|
||||||
|
'in' => 25.4,
|
||||||
|
'cm' => 10,
|
||||||
|
'mm' => 1
|
||||||
|
];
|
||||||
|
|
||||||
|
// Normalize to base unit (grams or mm)
|
||||||
|
$baseValue = $value * (isset($units[$from]) ? $units[$from] : 1);
|
||||||
|
|
||||||
|
// Convert to target unit
|
||||||
|
$converted = $baseValue / (isset($units[$to]) ? $units[$to] : 1);
|
||||||
|
|
||||||
|
return round($converted, $precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: Get Origin Zip from Old Module DB
|
||||||
|
*/
|
||||||
|
private function getOriginZip($originalModule)
|
||||||
|
{
|
||||||
|
// The old module stores addresses in `ps_uspsl_address`
|
||||||
|
// We look for the one marked 'origin' = 1
|
||||||
|
$sql = 'SELECT postcode FROM `' . _DB_PREFIX_ . 'uspsl_address` WHERE origin = 1 AND active = 1';
|
||||||
|
$zip = Db::getInstance()->getValue($sql);
|
||||||
|
return $zip ? $zip : '90210'; // Fallback if configuration is missing
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAPPING LOGIC: Old Module Codes -> New API Enums
|
||||||
|
*/
|
||||||
|
private function mapServiceCodeToApiClass(string $oldCode, bool $isInternational)
|
||||||
|
{
|
||||||
|
// $this->externalLog(['mapServiceCodeToApiClass' => ['oldCode' => $oldCode, 'isInternational' => $isInternational]]);
|
||||||
|
// 1. Define the Standard Map
|
||||||
|
if ($isInternational) {
|
||||||
|
$map = [
|
||||||
|
|
||||||
|
// INTERNATIONAL
|
||||||
|
'INT_1' => 'PRIORITY_MAIL_EXPRESS_INTERNATIONAL',
|
||||||
|
'INT_2' => 'PRIORITY_MAIL_INTERNATIONAL',
|
||||||
|
'INT_15' => 'FIRST-CLASS_PACKAGE_INTERNATIONAL_SERVICE',
|
||||||
|
'INT_4' => 'FIRST-CLASS_PACKAGE_INTERNATIONAL_SERVICE', // GXG is suspended/retired, fallback to First Class
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$map = [
|
||||||
|
// DOMESTIC
|
||||||
|
'USA_0' => 'USPS_GROUND_ADVANTAGE', // Was First-Class
|
||||||
|
'USA_1' => 'PRIORITY_MAIL',
|
||||||
|
'USA_3' => 'PRIORITY_MAIL_EXPRESS',
|
||||||
|
'USA_6' => 'MEDIA_MAIL',
|
||||||
|
'USA_7' => 'LIBRARY_MAIL',
|
||||||
|
'USA_1058' => 'USPS_GROUND_ADVANTAGE',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!isset($map[$oldCode])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiClass = $map[$oldCode];
|
||||||
|
|
||||||
|
// 2. International Override Logic
|
||||||
|
// If the destination is International, but the mapped class is Domestic, swap it.
|
||||||
|
/* if ($isInternational) {
|
||||||
|
switch ($apiClass) {
|
||||||
|
case 'PRIORITY_MAIL':
|
||||||
|
return 'PRIORITY_MAIL_INTERNATIONAL';
|
||||||
|
|
||||||
|
case 'PRIORITY_MAIL_EXPRESS':
|
||||||
|
return 'PRIORITY_MAIL_EXPRESS_INTERNATIONAL';
|
||||||
|
|
||||||
|
// Ground Advantage, Media, and Library do not exist internationally.
|
||||||
|
// The closest equivalent is First-Class Package International.
|
||||||
|
case 'USPS_GROUND_ADVANTAGE':
|
||||||
|
case 'MEDIA_MAIL':
|
||||||
|
case 'LIBRARY_MAIL':
|
||||||
|
return 'FIRST-CLASS_PACKAGE_INTERNATIONAL_SERVICE';
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
return $apiClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAPPING LOGIC: Flat Rate Boxes
|
||||||
|
* Maps the internal name of the box to the API 'rateIndicator'
|
||||||
|
*/
|
||||||
|
private function mapBoxToRateIndicator($boxReference)
|
||||||
|
{
|
||||||
|
// You provided the PredefinedBox.php file earlier.
|
||||||
|
// We map those names to New API 'rateIndicator' enum.
|
||||||
|
|
||||||
|
// Example Reference: "USPS Medium Flat Rate Box" or "MediumFlatRateBox"
|
||||||
|
// We do a loose match
|
||||||
|
|
||||||
|
if (stripos($boxReference, 'Medium Flat Rate Box') !== false) return 'FB';
|
||||||
|
if (stripos($boxReference, 'Large Flat Rate Box') !== false) return 'PL';
|
||||||
|
if (stripos($boxReference, 'Small Flat Rate Box') !== false) return 'FS';
|
||||||
|
if (stripos($boxReference, 'Flat Rate Envelope') !== false) return 'FE';
|
||||||
|
if (stripos($boxReference, 'Padded Flat Rate Envelope') !== false) return 'FP';
|
||||||
|
if (stripos($boxReference, 'Legal Flat Rate Envelope') !== false) return 'FA';
|
||||||
|
|
||||||
|
return false; // Not a flat rate box, uses standard rates
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages OAuth2 Token life cycle
|
||||||
|
*/
|
||||||
|
private function getAccessToken()
|
||||||
|
{
|
||||||
|
$token = Configuration::get('USPS_BRIDGE_ACCESS_TOKEN');
|
||||||
|
$expiry = Configuration::get('USPS_BRIDGE_TOKEN_EXPIRY');
|
||||||
|
|
||||||
|
// Add 60 seconds buffer
|
||||||
|
if ($token && $expiry > (time() + 60)) {
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->refreshAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function refreshAccessToken()
|
||||||
|
{
|
||||||
|
$clientId = Configuration::get('USPS_BRIDGE_CLIENT_ID');
|
||||||
|
$clientSecret = Configuration::get('USPS_BRIDGE_CLIENT_SECRET');
|
||||||
|
$isLive = (bool)Configuration::get('USPS_BRIDGE_LIVE_MODE');
|
||||||
|
|
||||||
|
// CORRECT URLs based on the OpenAPI Spec provided:
|
||||||
|
// Prod: https://apis.usps.com/oauth2/v3
|
||||||
|
// Test: https://apis-tem.usps.com/oauth2/v3
|
||||||
|
$url = $isLive
|
||||||
|
? 'https://apis.usps.com/oauth2/v3/token'
|
||||||
|
: 'https://apis-tem.usps.com/oauth2/v3/token';
|
||||||
|
|
||||||
|
$this->log("Requesting New Token from: " . $url);
|
||||||
|
|
||||||
|
// Create Symfony Client
|
||||||
|
$client = HttpClient::create([
|
||||||
|
'timeout' => 10,
|
||||||
|
'verify_peer' => true, // Set to true in strict production environments
|
||||||
|
'verify_host' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $client->request('POST', $url, [
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
// 'json' key automatically encodes the array to JSON and sets Content-Type
|
||||||
|
'json' => [
|
||||||
|
'client_id' => $clientId,
|
||||||
|
'client_secret' => $clientSecret,
|
||||||
|
'grant_type' => 'client_credentials',
|
||||||
|
// 'scope' => 'prices international-prices' // Specifying scope helps avoid ambiguity
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get status code
|
||||||
|
$statusCode = $response->getStatusCode();
|
||||||
|
|
||||||
|
// Convert response to array (pass false to prevent throwing exceptions on 4xx/5xx)
|
||||||
|
$data = $response->toArray(false);
|
||||||
|
|
||||||
|
if ($statusCode == 200 && isset($data['access_token'])) {
|
||||||
|
$expiresIn = isset($data['expires_in']) ? (int)$data['expires_in'] : 3599;
|
||||||
|
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_ACCESS_TOKEN', $data['access_token']);
|
||||||
|
Configuration::updateValue('USPS_BRIDGE_TOKEN_EXPIRY', time() + $expiresIn);
|
||||||
|
|
||||||
|
$this->log("Token refreshed successfully.");
|
||||||
|
return $data['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log detailed error from USPS
|
||||||
|
$this->log("Token Request Failed [HTTP $statusCode]: " . json_encode($data));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->log("Symfony HTTP Client Error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current visitor IP is allowed to use New API
|
||||||
|
*/
|
||||||
|
public function isIpAllowed()
|
||||||
|
{
|
||||||
|
$allowedIps = Configuration::get('USPS_BRIDGE_DEBUG_IPS');
|
||||||
|
|
||||||
|
// If empty, everyone is allowed (Production ready)
|
||||||
|
if (empty($allowedIps)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ips = array_map('trim', explode(',', $allowedIps));
|
||||||
|
$currentIp = Tools::getRemoteAddr();
|
||||||
|
|
||||||
|
$allowed = in_array($currentIp, $ips);
|
||||||
|
|
||||||
|
if (!$allowed) {
|
||||||
|
// Optional: Log that we skipped logic due to IP (might be too spammy)
|
||||||
|
// $this->log("IP $currentIp not in debug list. Using Old API.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function log($message)
|
||||||
|
{
|
||||||
|
if (Configuration::get('USPS_BRIDGE_LOGGING')) {
|
||||||
|
PrestaShopLogger::addLog(
|
||||||
|
'[USPS-BRIDGE] ' . (is_array($message) ? json_encode($message) : $message),
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
'Usps_Api_Bridge',
|
||||||
|
1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function externalLog(array $message)
|
||||||
|
{
|
||||||
|
if (!Validate::isUrl(Configuration::get('USPS_BRIDGE_EXTERNAL_DEBUG_URL'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = HttpClient::create([
|
||||||
|
'timeout' => 10,
|
||||||
|
'verify_peer' => true, // Set to true in strict production environments
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response[] = $client->request('POST', Configuration::get('USPS_BRIDGE_EXTERNAL_DEBUG_URL'), [
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
'json' => [
|
||||||
|
$message
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (TransportExceptionInterface $t) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//- - - - - - - - - - END: usps_api_bridge/usps_api_bridge.php - - - - - - - - - -//
|
||||||
|
|
||||||
|
|
||||||
|
//- - - - - - - - - - START: usps_api_bridge/config_uk.xml - - - - - - - - - -//
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<module>
|
||||||
|
<name>usps_api_bridge</name>
|
||||||
|
<displayName><![CDATA[USPS API Bridge (OAuth2)]]></displayName>
|
||||||
|
<version><![CDATA[1.0.0]]></version>
|
||||||
|
<description><![CDATA[Modern OAuth2 Bridge for the legacy ZH USPS Labels module.]]></description>
|
||||||
|
<author><![CDATA[Panariga]]></author>
|
||||||
|
<tab><![CDATA[shipping_logistics]]></tab>
|
||||||
|
<confirmUninstall><![CDATA[Are you sure? This will disable the connection to the new USPS API.]]></confirmUninstall>
|
||||||
|
<is_configurable>1</is_configurable>
|
||||||
|
<need_instance>0</need_instance>
|
||||||
|
</module>
|
||||||
|
//- - - - - - - - - - END: usps_api_bridge/config_uk.xml - - - - - - - - - -//
|
||||||
|
|
||||||
|
|
||||||
|
//- - - - - - - - - - START: usps_api_bridge/classes/UspsV3Client.php - - - - - - - - - -//
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||||
|
|
||||||
|
class UspsV3Client
|
||||||
|
{
|
||||||
|
private $token;
|
||||||
|
private $isLive;
|
||||||
|
private $baseUrl;
|
||||||
|
|
||||||
|
public function __construct($token, $isLive = false)
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
$this->isLive = $isLive;
|
||||||
|
// Base URLs per OpenAPI Spec
|
||||||
|
$this->baseUrl = $this->isLive
|
||||||
|
? 'https://apis.usps.com/prices/v3'
|
||||||
|
: 'https://apis-tem.usps.com/prices/v3';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Domestic Rate (Rates v3)
|
||||||
|
*/
|
||||||
|
public function getDomesticRate($payload)
|
||||||
|
{
|
||||||
|
return $this->post('/base-rates/search', $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get International Rate (Rates v3)
|
||||||
|
*/
|
||||||
|
public function getInternationalRate($payload)
|
||||||
|
{
|
||||||
|
// International endpoint uses a different base structure per spec
|
||||||
|
$intlBaseUrl = $this->isLive
|
||||||
|
? 'https://apis.usps.com/international-prices/v3'
|
||||||
|
: 'https://apis-tem.usps.com/international-prices/v3';
|
||||||
|
|
||||||
|
return $this->post('/base-rates/search', $payload, $intlBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal POST logic using Symfony HTTP Client
|
||||||
|
*/
|
||||||
|
private function post($endpoint, $payload, $overrideUrl = null)
|
||||||
|
{
|
||||||
|
$url = ($overrideUrl ? $overrideUrl : $this->baseUrl) . $endpoint;
|
||||||
|
|
||||||
|
$client = HttpClient::create([
|
||||||
|
'timeout' => 15,
|
||||||
|
'verify_peer' => false,
|
||||||
|
'verify_host' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $client->request('POST', $url, [
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Accept' => 'application/json'
|
||||||
|
],
|
||||||
|
'json' => $payload
|
||||||
|
]);
|
||||||
|
|
||||||
|
// toArray(false) prevents exception on 4xx/5xx responses so we can parse the error body
|
||||||
|
$data = $response->toArray(false);
|
||||||
|
$statusCode = $response->getStatusCode();
|
||||||
|
|
||||||
|
// Handle API Errors (400 Bad Request, 401 Unauthorized, etc)
|
||||||
|
if ($statusCode >= 400) {
|
||||||
|
$msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown Error';
|
||||||
|
|
||||||
|
// Try to extract deeper error detail (e.g., from 'errors' array)
|
||||||
|
if (isset($data['error']['errors'][0]['detail'])) {
|
||||||
|
$msg .= ' - ' . $data['error']['errors'][0]['detail'];
|
||||||
|
} elseif (isset($data['error']['code'])) {
|
||||||
|
$msg .= ' (' . $data['error']['code'] . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['error' => "API HTTP $statusCode: $msg"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
return ['error' => 'Network/Transport Error: ' . $e->getMessage()];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['error' => 'Client Error: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//- - - - - - - - - - END: usps_api_bridge/classes/UspsV3Client.php - - - - - - - - - -//
|
||||||
|
|
||||||
|
|
||||||
|
//- - - - - - - - - - START: usps_api_bridge/README.md - - - - - - - - - -//
|
||||||
|
# USPS API Bridge (OAuth2) for PrestaShop
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This module acts as a "Sidecar" or "Bridge" for the legacy **ZH USPS Labels (`zh_uspslabels`)** module.
|
||||||
|
|
||||||
|
**The Problem:** The legacy module relies on the deprecated USPS Web Tools API (XML/User+Pass), which is being retired in favor of the new USPS Connect API (JSON/OAuth2). The original developer is unresponsive.
|
||||||
|
|
||||||
|
**The Solution:** This module installs a PrestaShop **Override** that intercepts rate calculation requests destined for the old module. It routes them through this bridge, authenticates via OAuth2, queries the new USPS v3 API, and returns the rates to the cart seamlessly. It leaves the complex EasyPost logic (Label generation, Manifests) untouched.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
* **PrestaShop Version:** 1.7.x or 8.x.
|
||||||
|
* **Legacy Module:** `zh_uspslabels` must be **installed and active**. Do not uninstall it; this bridge relies on its database tables (`ps_uspsl_box`, `ps_uspsl_method`) and packing logic.
|
||||||
|
* **USPS Account:** A registered account on the [USPS Developer Portal](https://developer.usps.com/).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. **Upload:**
|
||||||
|
* Zip the `usps_api_bridge` folder.
|
||||||
|
* Upload via **Module Manager** in the Back Office.
|
||||||
|
* *Alternatively:* FTP the folder to `/modules/usps_api_bridge/`.
|
||||||
|
|
||||||
|
2. **Install:**
|
||||||
|
* Click **Install**.
|
||||||
|
* **CRITICAL:** Upon installation, the module attempts to copy a file to `/override/modules/zh_uspslabels/zh_uspslabels.php`. Ensure your file permissions allow this.
|
||||||
|
|
||||||
|
3. **Clear Cache:**
|
||||||
|
* Go to **Advanced Parameters > Performance** and click **Clear Cache**.
|
||||||
|
* *Manual Step:* If the bridge does not seem to work, manually delete `/var/cache/prod/class_index.php` (or `/cache/class_index.php` on older PS versions) to force PrestaShop to register the new override.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Go to **Module Manager > USPS API Bridge > Configure**.
|
||||||
|
|
||||||
|
### 1. Credentials
|
||||||
|
You cannot use your old Web Tools username (e.g., `123XY...`). You must generate new OAuth keys:
|
||||||
|
1. Log in to [developer.usps.com](https://developer.usps.com/).
|
||||||
|
2. Go to **Apps** -> **Add App**.
|
||||||
|
3. **Important:** In the API Products list, ensure you select:
|
||||||
|
* **Prices** (Domestic)
|
||||||
|
* **International Prices**
|
||||||
|
* **OAuth 2.0**
|
||||||
|
4. Copy the **Consumer Key** (Client ID) and **Consumer Secret**.
|
||||||
|
5. Paste them into the module configuration.
|
||||||
|
|
||||||
|
### 2. Deployment Settings
|
||||||
|
* **Live Mode:**
|
||||||
|
* `No`: Uses `https://apis-tem.usps.com` (Test Environment).
|
||||||
|
* `Yes`: Uses `https://apis.usps.com` (Production).
|
||||||
|
* *Note:* Your USPS App must be approved for Production access for Live Mode to work.
|
||||||
|
* **Debug Allowed IPs:**
|
||||||
|
* **Highly Recommended for Testing:** Enter your IP address here (comma-separated for multiple).
|
||||||
|
* If set, **ONLY** visitors from these IPs will use the new API logic. Real customers will continue using the old module logic (until the USPS cutoff date).
|
||||||
|
* Leave empty to go live for everyone.
|
||||||
|
* **Enable Logging:** Logs all API requests/responses to **Advanced Parameters > Logs**.
|
||||||
|
|
||||||
|
## Architecture & Logic flow
|
||||||
|
|
||||||
|
1. **The Interceptor:**
|
||||||
|
* The module overrides `Zh_UspsLabels::getPackageShippingCost`.
|
||||||
|
* It checks if `usps_api_bridge` is active and if the user's IP is allowed.
|
||||||
|
* If matched, it stops the legacy `CarrierHelper` -> `RateService` execution chain (which uses the broken XML API).
|
||||||
|
|
||||||
|
2. **The Bridge:**
|
||||||
|
* It retrieves the active **Boxes** and **Methods** from the old module's database tables.
|
||||||
|
* It re-uses the old module's `BoxPacker` logic to determine how many packages are needed.
|
||||||
|
* It maps the legacy Service Codes (e.g., `USA_1`) to new USPS Enums (e.g., `PRIORITY_MAIL`).
|
||||||
|
* It authenticates via `/oauth2/v3/token` (caching the token for 1 hour).
|
||||||
|
* It sends a JSON payload to `/rates/v3/calculate`.
|
||||||
|
|
||||||
|
3. **EasyPost:**
|
||||||
|
* Logic regarding Label Printing, Tracking, and Manifests handled by EasyPost is **not touched**. This traffic flows through the original module files as normal.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "API Connection Failed" in Config
|
||||||
|
If you click "Test Connection" in the legacy module and it fails:
|
||||||
|
* Ensure the Bridge module is active.
|
||||||
|
* Ensure your IP is in the "Debug Allowed IPs" list.
|
||||||
|
* The Bridge intercepts the test click; check the PrestaShop Logs for the specific error message from the new API.
|
||||||
|
|
||||||
|
### Rates are $0.00 or Not Showing
|
||||||
|
1. Enable **Logging** in the bridge module.
|
||||||
|
2. Check **Advanced Parameters > Logs**.
|
||||||
|
3. Common Errors:
|
||||||
|
* `401 Unauthorized`: Your Client ID/Secret is wrong, or you selected the wrong environment (Live vs Test).
|
||||||
|
* `400 Bad Request`: Usually due to invalid Zip Codes (USPS v3 requires 5-digit Zips for domestic) or invalid Enum mappings.
|
||||||
|
|
||||||
|
### Uninstalling
|
||||||
|
If you uninstall this module, the Override is removed, and the shop reverts entirely to the legacy XML API code.
|
||||||
|
//- - - - - - - - - - END: usps_api_bridge/README.md - - - - - - - - - -//
|
||||||
|
|
||||||
141
llmdumper.php
Normal file
141
llmdumper.php
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM Dumper for PrestaShop Modules
|
||||||
|
*
|
||||||
|
* This script recursively scans a PrestaShop module directory, concatenates all relevant source
|
||||||
|
* files into a single text file, and prepends each file's content with its relative path.
|
||||||
|
*
|
||||||
|
* PURPOSE: To gather all meaningful source code and template files from a module
|
||||||
|
* into a single file for easy pasting into a Large Language Model (LLM) context window.
|
||||||
|
*
|
||||||
|
* EXCLUSIONS:
|
||||||
|
* - The 'vendor' directory (Composer dependencies).
|
||||||
|
* - Image files (jpg, png, gif, svg, webp).
|
||||||
|
* - The script itself ('llmdumper.php').
|
||||||
|
* - The output file ('.llmdump').
|
||||||
|
*
|
||||||
|
* HOW TO USE:
|
||||||
|
* 1. Place this script in the root directory of the PrestaShop module you want to dump.
|
||||||
|
* (e.g., /modules/yourmodule/llmdumper.php)
|
||||||
|
* 2. Run it from the command line for best results:
|
||||||
|
* cd /path/to/prestashop/modules/yourmodule/
|
||||||
|
* php llmdumper.php
|
||||||
|
* 3. A hidden file named '.llmdump' will be created in the module's root directory.
|
||||||
|
* 4. Open '.llmdump', copy its contents, and paste it into your LLM prompt.
|
||||||
|
* 5. **IMPORTANT**: Delete this script and the '.llmdump' file from your server when you are done.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
|
||||||
|
// The name of the output file. Using a dotfile to keep it hidden.
|
||||||
|
define('OUTPUT_FILE', '.llmdump');
|
||||||
|
|
||||||
|
// The directory to exclude. Typically for Composer packages.
|
||||||
|
define('VENDOR_DIR', 'vendor');
|
||||||
|
|
||||||
|
define('GIT_DIR', '.git');
|
||||||
|
// A list of image file extensions to exclude. Case-insensitive.
|
||||||
|
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'];
|
||||||
|
|
||||||
|
// --- Script Logic ---
|
||||||
|
|
||||||
|
// Set a long execution time for large modules. 0 means no time limit.
|
||||||
|
set_time_limit(0);
|
||||||
|
|
||||||
|
// The directory where this script is running (the module's root).
|
||||||
|
$moduleRootPath = __DIR__;
|
||||||
|
|
||||||
|
// The name of the module's directory.
|
||||||
|
$moduleDirName = basename($moduleRootPath);
|
||||||
|
|
||||||
|
// The full path for the output file.
|
||||||
|
$outputFilePath = $moduleRootPath . DIRECTORY_SEPARATOR . OUTPUT_FILE;
|
||||||
|
|
||||||
|
// Delete the old dump file if it exists to start fresh.
|
||||||
|
if (file_exists($outputFilePath)) {
|
||||||
|
unlink($outputFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Starting LLM Dumper for module: '{$moduleDirName}'\n";
|
||||||
|
echo "---------------------------------------------------\n";
|
||||||
|
|
||||||
|
// Use RecursiveDirectoryIterator to scan all files and directories.
|
||||||
|
$directoryIterator = new RecursiveDirectoryIterator($moduleRootPath, RecursiveDirectoryIterator::SKIP_DOTS);
|
||||||
|
$fileIterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
|
||||||
|
$totalBytes = 0;
|
||||||
|
$fileCount = 0;
|
||||||
|
|
||||||
|
foreach ($fileIterator as $file) {
|
||||||
|
// --- Filtering Logic ---
|
||||||
|
|
||||||
|
// Get the full real path of the file.
|
||||||
|
$filePath = $file->getRealPath();
|
||||||
|
|
||||||
|
// 1. Exclude the script itself and the output file.
|
||||||
|
if ($file->getBasename() === 'llmdumper.php' || $file->getBasename() === OUTPUT_FILE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Exclude the entire 'vendor' directory.
|
||||||
|
// We check if the path contains '/vendor/'.
|
||||||
|
if (strpos($filePath, DIRECTORY_SEPARATOR . VENDOR_DIR . DIRECTORY_SEPARATOR) !== false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strpos($filePath, DIRECTORY_SEPARATOR . GIT_DIR . DIRECTORY_SEPARATOR) !== false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// We only care about files, not directories.
|
||||||
|
if ($file->isDir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Exclude image files based on their extension.
|
||||||
|
$extension = strtolower($file->getExtension());
|
||||||
|
if (in_array($extension, IMAGE_EXTENSIONS)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- File Processing ---
|
||||||
|
|
||||||
|
// Get the file path relative to the module's root directory.
|
||||||
|
$relativePath = str_replace($moduleRootPath . DIRECTORY_SEPARATOR, '', $filePath);
|
||||||
|
|
||||||
|
// Prepend the module directory name to the relative path as requested.
|
||||||
|
$finalPath = $moduleDirName . DIRECTORY_SEPARATOR . $relativePath;
|
||||||
|
|
||||||
|
// Read the file content.
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
if ($content === false) {
|
||||||
|
echo "WARNING: Could not read file: {$finalPath}\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a header for this file's content to identify it in the dump.
|
||||||
|
$fileHeader = "
|
||||||
|
//- - - - - - - - - - START: {$finalPath} - - - - - - - - - -//
|
||||||
|
";
|
||||||
|
$fileFooter = "
|
||||||
|
//- - - - - - - - - - END: {$finalPath} - - - - - - - - - -//
|
||||||
|
\n";
|
||||||
|
|
||||||
|
// Assemble the full string to append to the output file.
|
||||||
|
$dumpContent = $fileHeader . $content . $fileFooter;
|
||||||
|
|
||||||
|
// Append the content to the output file.
|
||||||
|
if (file_put_contents($outputFilePath, $dumpContent, FILE_APPEND)) {
|
||||||
|
echo "Appended: {$finalPath}\n";
|
||||||
|
$totalBytes += strlen($dumpContent);
|
||||||
|
$fileCount++;
|
||||||
|
} else {
|
||||||
|
echo "ERROR: Could not write to output file!\n";
|
||||||
|
exit(1); // Exit with an error code.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "---------------------------------------------------\n";
|
||||||
|
echo "Dump complete!\n";
|
||||||
|
echo "- Files processed: {$fileCount}\n";
|
||||||
|
echo "- Total size: " . round($totalBytes / 1024, 2) . " KB\n";
|
||||||
|
echo "- Output file: {$outputFilePath}\n";
|
||||||
@@ -6,6 +6,7 @@ if (!defined('_PS_VERSION_')) {
|
|||||||
|
|
||||||
class Zh_UspsLabelsOverride extends Zh_UspsLabels
|
class Zh_UspsLabelsOverride extends Zh_UspsLabels
|
||||||
{
|
{
|
||||||
|
public $overrideVersion = '1.0.0';
|
||||||
/**
|
/**
|
||||||
* Intercept the rate calculation call.
|
* Intercept the rate calculation call.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Usps_Api_Bridge extends Module
|
|||||||
{
|
{
|
||||||
$this->name = 'usps_api_bridge';
|
$this->name = 'usps_api_bridge';
|
||||||
$this->tab = 'shipping_logistics';
|
$this->tab = 'shipping_logistics';
|
||||||
$this->version = '1.0.0';
|
$this->version = '1.0.1';
|
||||||
$this->author = 'Panariga';
|
$this->author = 'Panariga';
|
||||||
$this->need_instance = 0;
|
$this->need_instance = 0;
|
||||||
$this->bootstrap = true;
|
$this->bootstrap = true;
|
||||||
@@ -159,7 +159,9 @@ class Usps_Api_Bridge extends Module
|
|||||||
|
|
||||||
// 1. Get OAuth Token
|
// 1. Get OAuth Token
|
||||||
$token = $this->getAccessToken();
|
$token = $this->getAccessToken();
|
||||||
if (!$token) return false;
|
if (!$token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$carrierId = (int)$originalModule->id_carrier;
|
$carrierId = (int)$originalModule->id_carrier;
|
||||||
if (!$carrierId && isset($params->id_carrier)) {
|
if (!$carrierId && isset($params->id_carrier)) {
|
||||||
@@ -169,7 +171,9 @@ class Usps_Api_Bridge extends Module
|
|||||||
// 3. Get Method Code
|
// 3. Get Method Code
|
||||||
$sql = 'SELECT code FROM `' . _DB_PREFIX_ . 'uspsl_method` WHERE id_carrier = ' . (int)$carrierId;
|
$sql = 'SELECT code FROM `' . _DB_PREFIX_ . 'uspsl_method` WHERE id_carrier = ' . (int)$carrierId;
|
||||||
$methodCode = \Db::getInstance()->getValue($sql);
|
$methodCode = \Db::getInstance()->getValue($sql);
|
||||||
if (!$methodCode) return false;
|
if (!$methodCode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// --- 4. CHECK LEGACY DB CACHE ---
|
// --- 4. CHECK LEGACY DB CACHE ---
|
||||||
$zhCache = false;
|
$zhCache = false;
|
||||||
@@ -187,12 +191,28 @@ class Usps_Api_Bridge extends Module
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// -------------------------------
|
|
||||||
|
|
||||||
// 5. Determine International Status & Address Data (Cookie/Object Hybrid)
|
// 5. Determine International Status & Address Data (Cookie/Object Hybrid)
|
||||||
$destZip = '';
|
$destZip = '';
|
||||||
$destCountryIso = '';
|
$destCountryIso = '';
|
||||||
|
$context = Context::getContext();
|
||||||
|
$isEstimator = (Tools::getValue('module') === 'zh_uspslabels' && Tools::getValue('controller') === 'carrier');
|
||||||
|
if ($isEstimator) {
|
||||||
|
// Prioritize POST data from the widget
|
||||||
|
$this->externalLog(['Estimator' => ['response' => $response]]);
|
||||||
|
|
||||||
|
if (Tools::getIsset('postcode')) {
|
||||||
|
$destZip = Tools::getValue('postcode');
|
||||||
|
} elseif (isset($context->cookie->postcode)) {
|
||||||
|
$destZip = $context->cookie->postcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Tools::getIsset('id_country')) {
|
||||||
|
$destCountryIso = Country::getIsoById((int)Tools::getValue('id_country'));
|
||||||
|
} elseif (isset($context->cookie->id_country)) {
|
||||||
|
$destCountryIso = Country::getIsoById((int)$context->cookie->id_country);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!empty($params->id_address_delivery)) {
|
if (!empty($params->id_address_delivery)) {
|
||||||
$address = new Address($params->id_address_delivery);
|
$address = new Address($params->id_address_delivery);
|
||||||
if (Validate::isLoadedObject($address)) {
|
if (Validate::isLoadedObject($address)) {
|
||||||
@@ -201,13 +221,6 @@ class Usps_Api_Bridge extends Module
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($destZip) && Tools::getIsset('postcode')) {
|
|
||||||
$destZip = Tools::getValue('postcode');
|
|
||||||
}
|
|
||||||
if (empty($destCountryIso) && Tools::getIsset('id_country')) {
|
|
||||||
$destCountryIso = Country::getIsoById((int)Tools::getValue('id_country'));
|
|
||||||
}
|
|
||||||
$context = Context::getContext();
|
|
||||||
if (empty($destZip) && isset($context->cookie->postcode)) {
|
if (empty($destZip) && isset($context->cookie->postcode)) {
|
||||||
$destZip = $context->cookie->postcode;
|
$destZip = $context->cookie->postcode;
|
||||||
}
|
}
|
||||||
@@ -227,8 +240,8 @@ class Usps_Api_Bridge extends Module
|
|||||||
|
|
||||||
// Clean Data
|
// Clean Data
|
||||||
$originZip = $this->getOriginZip($originalModule);
|
$originZip = $this->getOriginZip($originalModule);
|
||||||
$originZip = substr(preg_replace('/[^0-9]/', '', $originZip), 0, 5);
|
// $originZip = substr(preg_replace('/[^0-9]/', '', $originZip), 0, 5);
|
||||||
$destZip = substr(preg_replace('/[^0-9]/', '', $destZip), 0, 5);
|
// $destZip = substr(preg_replace('/[^0-9]/', '', $destZip), 0, 5);
|
||||||
$isInternational = ($destCountryIso !== 'US');
|
$isInternational = ($destCountryIso !== 'US');
|
||||||
|
|
||||||
// Map Code
|
// Map Code
|
||||||
@@ -321,7 +334,7 @@ class Usps_Api_Bridge extends Module
|
|||||||
/**
|
/**
|
||||||
* Helper to send request with Runtime Caching & Domestic/Intl switching
|
* Helper to send request with Runtime Caching & Domestic/Intl switching
|
||||||
*/
|
*/
|
||||||
private function sendApiRequest($client, $payload, $isInternational, $destCountryIso, $destZip)
|
private function sendApiRequest(UspsV3Client $client, array $payload, bool $isInternational, string $destCountryIso, string $destZip)
|
||||||
{
|
{
|
||||||
|
|
||||||
// 1. Prepare the specific payload for the cache key
|
// 1. Prepare the specific payload for the cache key
|
||||||
@@ -571,6 +584,10 @@ class Usps_Api_Bridge extends Module
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log detailed error from USPS
|
// Log detailed error from USPS
|
||||||
|
$this->externalLog([
|
||||||
|
'refreshAccessToken()' => 'Token Request Failed [HTTP $statusCode]',
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
$this->log("Token Request Failed [HTTP $statusCode]: " . json_encode($data));
|
$this->log("Token Request Failed [HTTP $statusCode]: " . json_encode($data));
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->log("Symfony HTTP Client Error: " . $e->getMessage());
|
$this->log("Symfony HTTP Client Error: " . $e->getMessage());
|
||||||
@@ -622,7 +639,12 @@ class Usps_Api_Bridge extends Module
|
|||||||
if (!Validate::isUrl(Configuration::get('USPS_BRIDGE_EXTERNAL_DEBUG_URL'))) {
|
if (!Validate::isUrl(Configuration::get('USPS_BRIDGE_EXTERNAL_DEBUG_URL'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$message['serverInfo'] = [
|
||||||
|
'controller' => Tools::getValue('controller'),
|
||||||
|
'domain' => Tools::getShopDomainSsl(),
|
||||||
|
'remoteAddr' => Tools::getRemoteAddr(),
|
||||||
|
'Usps_Api_Bridge_version' => $this->version
|
||||||
|
];
|
||||||
$client = HttpClient::create([
|
$client = HttpClient::create([
|
||||||
'timeout' => 10,
|
'timeout' => 10,
|
||||||
'verify_peer' => true, // Set to true in strict production environments
|
'verify_peer' => true, // Set to true in strict production environments
|
||||||
@@ -639,6 +661,7 @@ class Usps_Api_Bridge extends Module
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
} catch (TransportExceptionInterface $t) {
|
} catch (TransportExceptionInterface $t) {
|
||||||
|
$this->log($t->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user