From ea1cd1dc26625532b499b79eb44a53e440a63e41 Mon Sep 17 00:00:00 2001 From: dava Date: Thu, 30 Apr 2026 15:14:30 +0800 Subject: [PATCH 01/59] INI-1495 : Added Transport Company CTT on Aiku --- .../ApiCalls/CallApiCttEsShipping.php | 341 ++++++++++++++++++ .../Dispatching/Shipment/StoreShipment.php | 2 + config/app.php | 16 +- config/services.php | 10 + 4 files changed, 363 insertions(+), 6 deletions(-) create mode 100644 app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php diff --git a/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php b/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php new file mode 100644 index 00000000000..a527f31c653 --- /dev/null +++ b/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php @@ -0,0 +1,341 @@ +environment('production')) { + throw new \RuntimeException('CTT base URL is missing in production. Set services.ctt.base_url.'); + } + + return 'https://api-test.cttexpress.com/integrations'; + } + + public function getAccessToken(bool $forceRefresh = false): string + { + $tokenCacheKey = $this->getTokenCacheKey(); + + if (! $forceRefresh) { + $cachedToken = Cache::get($tokenCacheKey); + + if (is_string($cachedToken) && $cachedToken !== '') { + return $cachedToken; + } + } + + [$accessToken, $ttlSeconds] = $this->fetchAccessToken(); + + Cache::put( + $tokenCacheKey, + $accessToken, + now()->addSeconds($ttlSeconds) + ); + + return $accessToken; + } + + private function getTokenCacheKey(): string + { + $signature = hash('sha256', $this->getBaseUrl().'|'.(string) config('services.ctt.client_id', '')); + + return self::TOKEN_CACHE_KEY_PREFIX.$signature; + } + + private function getManifestLockKey(DeliveryNote $parent, Shipper $shipper): string + { + return self::MANIFEST_LOCK_KEY_PREFIX.$shipper->id.':'.$parent->id; + } + + private function getClientCenterCode(): string + { + $clientCenterCode = (string) config('services.ctt.client_center_code', ''); + + if ($clientCenterCode === '') { + $clientCenterCode = (string) config('app.sandbox.shipper_ctt_es_client_center_number', ''); + } + + if ($clientCenterCode === '') { + throw new \RuntimeException('CTT client center code is missing. Set services.ctt.client_center_code.'); + } + + return $clientCenterCode; + } + + private function fetchAccessToken(): array + { + $clientId = (string) config('services.ctt.client_id'); + $clientSecret = (string) config('services.ctt.client_secret'); + $grantType = (string) config('services.ctt.grant_type', 'client_credentials'); + $scope = (string) config('services.ctt.scope', ''); + + if ($clientId === '' || $clientSecret === '') { + throw new \RuntimeException('CTT credentials are missing in config (services.ctt.client_id / client_secret).'); + } + + $response = Http::asForm() + ->connectTimeout(10) + ->timeout(30) + ->post($this->getBaseUrl().'/oauth2/token', [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'grant_type' => $grantType, + 'scope' => $scope, + ]); + + if ($response->failed()) { + throw new \RuntimeException( + sprintf( + 'Failed to get CTT ES access token. Status: %d Body: %s', + $response->status(), + $response->body() + ) + ); + } + + $accessToken = (string) $response->json('access_token', ''); + if ($accessToken === '') { + throw new \RuntimeException('CTT token response does not contain access_token.'); + } + + $expiresIn = (int) $response->json('expires_in', self::TOKEN_FALLBACK_TTL_SECONDS); + $ttlSeconds = max($expiresIn - self::TOKEN_TTL_BUFFER_SECONDS, 60); + + return [$accessToken, $ttlSeconds]; + } + + private function getHeaders(bool $forceRefreshToken = false, array $extraHeaders = []): array + { + return array_merge([ + 'Authorization' => 'Bearer '.$this->getAccessToken($forceRefreshToken), + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], $extraHeaders); + } + + private function requestWithTokenRefresh(callable $request): Response + { + $response = $request(false); + + if ($response->status() === 401) { + Cache::forget($this->getTokenCacheKey()); + $response = $request(true); + } + + return $response; + } + + /** + * @throws \Illuminate\Http\Client\ConnectionException + */ + public function handle(DeliveryNote $parent, Shipper $shipper): array + { + $manifestLockKey = $this->getManifestLockKey($parent, $shipper); + + if (! Cache::add($manifestLockKey, 1, self::MANIFEST_LOCK_TTL_SECONDS)) { + return [ + 'status' => 'fail', + 'modelData' => [], + 'errorData' => [ + 'message' => ['A shipment manifest request is already in progress for this delivery note.'], + 'others' => [], + ], + ]; + } + + try { + $url = '/manifest/v2.0/shippings'; + $parentResource = GetShippingDeliveryNoteData::run($parent); + $parcels = $parent->parcels ?? []; + + $items = []; + foreach ($parcels as $parcel) { + $dimensions = Arr::get($parcel, 'dimensions', []); + + $items[] = [ + 'item_weight_declared' => (float) Arr::get($parcel, 'weight', 1), + 'item_length_declared' => (float) Arr::get($dimensions, 0, 10), + 'item_width_declared' => (float) Arr::get($dimensions, 1, 10), + 'item_height_declared' => (float) Arr::get($dimensions, 2, 10), + 'item_comments' => 'Item from AW Artisan', + ]; + } + + $fromAddress = Arr::get($parentResource, 'from_address', []); + $toAddress = Arr::get($parentResource, 'to_address', []); + + $customerReference = (string) Arr::get($parentResource, 'customer_reference', $parent->reference); + $idempotencyKey = hash('sha256', implode('|', [ + 'ctt-es', + $shipper->id, + $parent->id, + $customerReference, + ])); + + $params = [ + 'client_center_code' => $this->getClientCenterCode(), + 'shipping_type_code' => 'C24', + 'department_code' => '1', + 'client_references' => [$customerReference], + 'shipping_weight_declared' => collect($parcels)->sum('weight') ?: 1, + 'item_count' => count($items), + + 'sender_name' => Str::limit( + Arr::get($parentResource, 'from_company_name') + ?: Arr::get($parentResource, 'from_contact_name', 'AW Artisan'), + 60 + ), + 'sender_country_code' => Arr::get($fromAddress, 'country_code', 'ES'), + 'sender_postal_code' => Arr::get($fromAddress, 'postal_code', '28821'), + 'sender_address' => trim(Arr::get($fromAddress, 'address_line_1', '').' '.Arr::get($fromAddress, 'address_line_2', '')), + 'sender_town' => Arr::get($fromAddress, 'locality', 'Coslada'), + 'sender_email_notify_address' => Arr::get($parentResource, 'from_email'), + 'sender_phones' => array_values(array_filter( + [Arr::get($parentResource, 'from_phone')], + fn ($phone) => filled($phone) + )), + + 'recipient_name' => Str::limit( + Arr::get($parentResource, 'to_company_name') + ?: Arr::get($parentResource, 'to_contact_name', 'Customer'), + 60 + ), + 'recipient_country_code' => Arr::get($toAddress, 'country_code', 'ES'), + 'recipient_postal_code' => Arr::get($toAddress, 'postal_code', '08850'), + 'recipient_address' => trim(Arr::get($toAddress, 'address_line_1', '').' '.Arr::get($toAddress, 'address_line_2', '')), + 'recipient_town' => Arr::get($toAddress, 'locality', 'Madrid'), + 'recipient_email_notify_address' => Arr::get($parentResource, 'to_email'), + 'recipient_phones' => array_values(array_filter( + [Arr::get($parentResource, 'to_phone')], + fn ($phone) => filled($phone) + )), + + 'shipping_date' => now()->format('Y-m-d'), + 'delivery' => [ + 'contact_name' => Str::limit((string) Arr::get($parentResource, 'to_contact_name', ''), 60), + 'comments' => Str::limit(strip_tags((string) ($parent->shipping_notes ?? '')), 100), + ], + 'items' => $items, + ]; + + $params['custom_origin_name'] = $params['sender_name']; + $params['custom_origin_country_code'] = $params['sender_country_code']; + $params['custom_origin_postal_code'] = $params['sender_postal_code']; + $params['custom_origin_address'] = $params['sender_address']; + $params['custom_origin_town'] = $params['sender_town']; + + $codData = Arr::get($parentResource, 'cash_on_delivery'); + if (! empty($codData)) { + $params['additionals'] = [[ + 'additional_code' => 'REE', + 'additional_value' => (float) Arr::get($codData, 'amount', 0), + 'additional_flag' => true, + 'additional_text' => 'Cash on delivery payment', + 'additional_sub_code' => '', + ]]; + } + + // Do not auto-retry manifest POST to avoid accidental duplicate shipments on ambiguous failures. + $response = $this->requestWithTokenRefresh(function (bool $forceRefreshToken) use ($url, $params, $idempotencyKey) { + return Http::withHeaders($this->getHeaders($forceRefreshToken, [ + 'Idempotency-Key' => $idempotencyKey, + ])) + ->connectTimeout(10) + ->timeout(45) + ->post($this->getBaseUrl().$url, $params); + }); + + $apiResponse = $response->json(); + $statusCode = $response->status(); + + $modelData = ['api_response' => $apiResponse]; + $errorData = []; + + if ($statusCode === 201 && Arr::has($apiResponse, 'shipping_data.shipping_code')) { + $status = 'success'; + $shippingCode = Arr::get($apiResponse, 'shipping_data.shipping_code'); + + $modelData['trackings'] = [$shippingCode]; + $modelData['tracking'] = $shippingCode; + $modelData['label_type'] = ShipmentLabelTypeEnum::PDF; + $modelData['number_parcels'] = count($parcels); + $modelData['label'] = $this->getLabel($shippingCode); + } else { + $status = 'fail'; + $errorData['message'][] = Arr::get($apiResponse, 'error.error_description', 'Failed to manifest'); + $errorData['others'][] = Arr::get($apiResponse, 'error.error_extended_info.message', ''); + } + + return [ + 'status' => $status, + 'modelData' => $modelData, + 'errorData' => $errorData, + ]; + } finally { + Cache::forget($manifestLockKey); + } + } + + public function getLabel(string $shippingCode): string + { + $content = ''; + $url = "/trf/labelling/v1.0/shippings/{$shippingCode}/shipping-labels?label_type_code=PDF&model_type_code=SINGLE&label_offset=1"; + + for ($attempt = 1; $attempt <= 3; $attempt++) { + try { + $response = $this->requestWithTokenRefresh(function (bool $forceRefreshToken) use ($url) { + return Http::withHeaders($this->getHeaders($forceRefreshToken)) + ->connectTimeout(10) + ->timeout(45) + ->get($this->getBaseUrl().$url); + }); + + if ($response->successful()) { + $apiResponse = $response->json(); + $content = (string) Arr::get($apiResponse, 'data.0.label', ''); + + if ($content !== '') { + return $content; + } + } + } catch (Throwable $e) { + Sentry::captureException($e); + } + + usleep($attempt * 300000); // 300ms, 600ms, 900ms + } + + return ''; + } +} diff --git a/app/Actions/Dispatching/Shipment/StoreShipment.php b/app/Actions/Dispatching/Shipment/StoreShipment.php index e1e0fb495b6..8c0f7cccc2e 100644 --- a/app/Actions/Dispatching/Shipment/StoreShipment.php +++ b/app/Actions/Dispatching/Shipment/StoreShipment.php @@ -17,6 +17,7 @@ use App\Actions\Dispatching\Shipment\ApiCalls\CallApiGlsSkShipping; use App\Actions\Dispatching\Shipment\ApiCalls\CallApiItdShipping; use App\Actions\Dispatching\Shipment\ApiCalls\CallApiPacketaShipping; +use App\Actions\Dispatching\Shipment\ApiCalls\CallApiCttEsShipping; use App\Actions\Ordering\Order\Hydrators\OrderHydrateShipments; use App\Actions\OrgAction; use App\Enums\Catalogue\Shop\ShopTypeEnum; @@ -65,6 +66,7 @@ public function handle(DeliveryNote|PalletReturn $parent, Shipper $shipper, arra 'dpd-gb' => CallApiDpdGbShipping::run($parent, $shipper), 'dpd-sk' => CallApiDpdSkShipping::run($parent, $shipper), 'itd' => CallApiItdShipping::run($parent, $shipper), + 'ctt-es' => CallApiCttEsShipping::run($parent, $shipper), default => [ 'status' => 'error', 'errorData' => [ diff --git a/config/app.php b/config/app.php index ccc670d1bfc..b54e763a322 100644 --- a/config/app.php +++ b/config/app.php @@ -289,12 +289,16 @@ 'sandbox' => [ 'translate' => env('TRANSLATE_ON_LOCAL', false), - 'shipper_itd_token' => env('ITD_TOKEN'), - 'shipper_apc_token' => env('APC_TOKEN'), - 'shipper_dpd_gb_token' => env('DPD_GB_TOKEN'), - 'shipper_gls_sk_token' => env('GLS_SK_TOKEN'), - 'shipper_gls_es_token' => env('GLS_ES_TOKEN'), - 'shipper_packeta_access_token' => env('PACKETA_ACCESS_TOKEN'), + 'shipper_itd_token' => env('ITD_TOKEN'), + 'shipper_apc_token' => env('APC_TOKEN'), + 'shipper_dpd_gb_token' => env('DPD_GB_TOKEN'), + 'shipper_gls_sk_token' => env('GLS_SK_TOKEN'), + 'shipper_gls_es_token' => env('GLS_ES_TOKEN'), + 'shipper_packeta_access_token' => env('PACKETA_ACCESS_TOKEN'), + 'shipper_ctt_es_token' => env('CTT_ES_TOKEN'), + 'shipper_ctt_es_username' => env('CTT_ES_USERNAME'), + 'shipper_ctt_es_password' => env('CTT_ES_PASSWORD'), + 'shipper_ctt_es_client_center_number' => env('CTT_ES_CLIENT_CENTER_NUMBER'), 'local_share_url' => env('SANDBOX_SHARE_URL'), diff --git a/config/services.php b/config/services.php index 067e714515f..649507cb367 100644 --- a/config/services.php +++ b/config/services.php @@ -91,6 +91,16 @@ 'google_maps' => [ 'api_key' => env('GOOGLE_MAPS_API_KEY'), ], + + 'ctt' => [ + 'base_url' => env('CTT_BASE_URL'), + 'client_id' => env('CTT_CLIENT_ID'), + 'client_secret' => env('CTT_CLIENT_SECRET'), + 'grant_type' => env('CTT_GRANT_TYPE', 'client_credentials'), + 'scope' => env('CTT_SCOPE', ''), + 'client_center_code' => env('CTT_ES_CLIENT_CENTER_NUMBER'), + ], + 'openai' => [ 'api_key' => env('CHATGPT_TRANSLATIONS_API_KEY') ] From 286f2e84db124e6921d350ec3dd7199abdd56a6c Mon Sep 17 00:00:00 2001 From: Raul Perusquia Date: Tue, 5 May 2026 14:48:00 +0800 Subject: [PATCH 02/59] Update CTT configuration: move credentials to `app.sandbox` and refactor `CallApiCttEsShipping` for dynamic client ID and center code handling --- .../ApiCalls/CallApiCttEsShipping.php | 43 ++++++++++--------- config/app.php | 24 ++++++----- config/services.php | 9 ---- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php b/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php index a527f31c653..d689204bc02 100644 --- a/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php +++ b/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php @@ -22,25 +22,21 @@ class CallApiCttEsShipping extends OrgAction use AsAction; use WithAttributes; - private const TOKEN_CACHE_KEY_PREFIX = 'ctt_access_token:'; - private const TOKEN_FALLBACK_TTL_SECONDS = 3600; - private const TOKEN_TTL_BUFFER_SECONDS = 120; - private const MANIFEST_LOCK_KEY_PREFIX = 'ctt_manifest_lock:'; - private const MANIFEST_LOCK_TTL_SECONDS = 180; + private const string TOKEN_CACHE_KEY_PREFIX = 'ctt_access_token:'; + private const int TOKEN_FALLBACK_TTL_SECONDS = 3600; + private const int TOKEN_TTL_BUFFER_SECONDS = 120; + private const string MANIFEST_LOCK_KEY_PREFIX = 'ctt_manifest_lock:'; + private const int MANIFEST_LOCK_TTL_SECONDS = 180; public function getBaseUrl(): string { - $configuredBaseUrl = (string) config('services.ctt.base_url', ''); - - if ($configuredBaseUrl !== '') { - return rtrim($configuredBaseUrl, '/'); - } if (app()->environment('production')) { - throw new \RuntimeException('CTT base URL is missing in production. Set services.ctt.base_url.'); + //todo: return here production url } - return 'https://api-test.cttexpress.com/integrations'; + + } public function getAccessToken(bool $forceRefresh = false): string @@ -68,7 +64,15 @@ public function getAccessToken(bool $forceRefresh = false): string private function getTokenCacheKey(): string { - $signature = hash('sha256', $this->getBaseUrl().'|'.(string) config('services.ctt.client_id', '')); + + if (app()->environment('production')) { + //todo: get this data from the DB + $cttClientId=''; + }else{ + $cttClientId= config('app.sandbox.shipper_ctt_es_client_id', ''); + } + + $signature = hash('sha256', $this->getBaseUrl().'|'.$cttClientId); return self::TOKEN_CACHE_KEY_PREFIX.$signature; } @@ -80,14 +84,11 @@ private function getManifestLockKey(DeliveryNote $parent, Shipper $shipper): str private function getClientCenterCode(): string { - $clientCenterCode = (string) config('services.ctt.client_center_code', ''); - - if ($clientCenterCode === '') { - $clientCenterCode = (string) config('app.sandbox.shipper_ctt_es_client_center_number', ''); - } - - if ($clientCenterCode === '') { - throw new \RuntimeException('CTT client center code is missing. Set services.ctt.client_center_code.'); + if (app()->environment('production')) { + //todo: get this data from the DB + $clientCenterCode=''; + }else{ + $clientCenterCode= config('app.sandbox.shipper_ctt_es_client_center_number', ''); } return $clientCenterCode; diff --git a/config/app.php b/config/app.php index b54e763a322..5b530049253 100644 --- a/config/app.php +++ b/config/app.php @@ -289,16 +289,20 @@ 'sandbox' => [ 'translate' => env('TRANSLATE_ON_LOCAL', false), - 'shipper_itd_token' => env('ITD_TOKEN'), - 'shipper_apc_token' => env('APC_TOKEN'), - 'shipper_dpd_gb_token' => env('DPD_GB_TOKEN'), - 'shipper_gls_sk_token' => env('GLS_SK_TOKEN'), - 'shipper_gls_es_token' => env('GLS_ES_TOKEN'), - 'shipper_packeta_access_token' => env('PACKETA_ACCESS_TOKEN'), - 'shipper_ctt_es_token' => env('CTT_ES_TOKEN'), - 'shipper_ctt_es_username' => env('CTT_ES_USERNAME'), - 'shipper_ctt_es_password' => env('CTT_ES_PASSWORD'), - 'shipper_ctt_es_client_center_number' => env('CTT_ES_CLIENT_CENTER_NUMBER'), + 'shipper_itd_token' => env('ITD_TOKEN'), + 'shipper_apc_token' => env('APC_TOKEN'), + 'shipper_dpd_gb_token' => env('DPD_GB_TOKEN'), + 'shipper_gls_sk_token' => env('GLS_SK_TOKEN'), + 'shipper_gls_es_token' => env('GLS_ES_TOKEN'), + 'shipper_packeta_access_token' => env('PACKETA_ACCESS_TOKEN'), + + + 'shipper_ctt_es_client_id' => env('CTT_CLIENT_ID'), + 'shipper_ctt_es_client_center_number' => env('CTT_ES_CLIENT_CENTER_NUMBER'), + + + 'shipper_ctt_es_username' => env('CTT_ES_USERNAME'), + 'shipper_ctt_es_password' => env('CTT_ES_PASSWORD'), 'local_share_url' => env('SANDBOX_SHARE_URL'), diff --git a/config/services.php b/config/services.php index 649507cb367..2de9f6063f0 100644 --- a/config/services.php +++ b/config/services.php @@ -92,15 +92,6 @@ 'api_key' => env('GOOGLE_MAPS_API_KEY'), ], - 'ctt' => [ - 'base_url' => env('CTT_BASE_URL'), - 'client_id' => env('CTT_CLIENT_ID'), - 'client_secret' => env('CTT_CLIENT_SECRET'), - 'grant_type' => env('CTT_GRANT_TYPE', 'client_credentials'), - 'scope' => env('CTT_SCOPE', ''), - 'client_center_code' => env('CTT_ES_CLIENT_CENTER_NUMBER'), - ], - 'openai' => [ 'api_key' => env('CHATGPT_TRANSLATIONS_API_KEY') ] From 15446460ac51260eefe090bef21293e30b998279 Mon Sep 17 00:00:00 2001 From: Raul Perusquia Date: Tue, 5 May 2026 14:52:08 +0800 Subject: [PATCH 03/59] Add `GetRelatedProductsFromAurora` action and refactor `CallApiCttEsShipping` for Shipper-specific dynamic URLs and token handling. --- .../ApiCalls/CallApiCttEsShipping.php | 199 +++++++++--------- .../Web/GetRelatedProductsFromAurora.php | 85 ++++++++ 2 files changed, 186 insertions(+), 98 deletions(-) create mode 100644 app/Actions/Maintenance/Web/GetRelatedProductsFromAurora.php diff --git a/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php b/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php index d689204bc02..7863ae0ef15 100644 --- a/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php +++ b/app/Actions/Dispatching/Shipment/ApiCalls/CallApiCttEsShipping.php @@ -28,22 +28,20 @@ class CallApiCttEsShipping extends OrgAction private const string MANIFEST_LOCK_KEY_PREFIX = 'ctt_manifest_lock:'; private const int MANIFEST_LOCK_TTL_SECONDS = 180; - public function getBaseUrl(): string + public function getBaseUrl(Shipper $shipper): string { - if (app()->environment('production')) { - //todo: return here production url + return Arr::get($shipper->settings, 'base_url'); } - return 'https://api-test.cttexpress.com/integrations'; - + return 'https://api-test.cttexpress.com/integrations'; } - public function getAccessToken(bool $forceRefresh = false): string + public function getAccessToken(Shipper $shipper,bool $forceRefresh = false): string { - $tokenCacheKey = $this->getTokenCacheKey(); + $tokenCacheKey = $this->getTokenCacheKey($shipper); - if (! $forceRefresh) { + if (!$forceRefresh) { $cachedToken = Cache::get($tokenCacheKey); if (is_string($cachedToken) && $cachedToken !== '') { @@ -51,7 +49,7 @@ public function getAccessToken(bool $forceRefresh = false): string } } - [$accessToken, $ttlSeconds] = $this->fetchAccessToken(); + [$accessToken, $ttlSeconds] = $this->fetchAccessToken($shipper); Cache::put( $tokenCacheKey, @@ -62,17 +60,16 @@ public function getAccessToken(bool $forceRefresh = false): string return $accessToken; } - private function getTokenCacheKey(): string + private function getTokenCacheKey(Shipper $shipper): string { - if (app()->environment('production')) { //todo: get this data from the DB - $cttClientId=''; - }else{ - $cttClientId= config('app.sandbox.shipper_ctt_es_client_id', ''); + $cttClientId = ''; + } else { + $cttClientId = config('app.sandbox.shipper_ctt_es_client_id', ''); } - $signature = hash('sha256', $this->getBaseUrl().'|'.$cttClientId); + $signature = hash('sha256', $this->getBaseUrl($shipper).'|'.$cttClientId); return self::TOKEN_CACHE_KEY_PREFIX.$signature; } @@ -86,20 +83,20 @@ private function getClientCenterCode(): string { if (app()->environment('production')) { //todo: get this data from the DB - $clientCenterCode=''; - }else{ - $clientCenterCode= config('app.sandbox.shipper_ctt_es_client_center_number', ''); + $clientCenterCode = ''; + } else { + $clientCenterCode = config('app.sandbox.shipper_ctt_es_client_center_number', ''); } return $clientCenterCode; } - private function fetchAccessToken(): array + private function fetchAccessToken(Shipper $shipper): array { - $clientId = (string) config('services.ctt.client_id'); - $clientSecret = (string) config('services.ctt.client_secret'); - $grantType = (string) config('services.ctt.grant_type', 'client_credentials'); - $scope = (string) config('services.ctt.scope', ''); + $clientId = (string)config('services.ctt.client_id'); + $clientSecret = (string)config('services.ctt.client_secret'); + $grantType = (string)config('services.ctt.grant_type', 'client_credentials'); + $scope = (string)config('services.ctt.scope', ''); if ($clientId === '' || $clientSecret === '') { throw new \RuntimeException('CTT credentials are missing in config (services.ctt.client_id / client_secret).'); @@ -108,11 +105,11 @@ private function fetchAccessToken(): array $response = Http::asForm() ->connectTimeout(10) ->timeout(30) - ->post($this->getBaseUrl().'/oauth2/token', [ - 'client_id' => $clientId, + ->post($this->getBaseUrl($shipper).'/oauth2/token', [ + 'client_id' => $clientId, 'client_secret' => $clientSecret, - 'grant_type' => $grantType, - 'scope' => $scope, + 'grant_type' => $grantType, + 'scope' => $scope, ]); if ($response->failed()) { @@ -125,12 +122,12 @@ private function fetchAccessToken(): array ); } - $accessToken = (string) $response->json('access_token', ''); + $accessToken = (string)$response->json('access_token', ''); if ($accessToken === '') { throw new \RuntimeException('CTT token response does not contain access_token.'); } - $expiresIn = (int) $response->json('expires_in', self::TOKEN_FALLBACK_TTL_SECONDS); + $expiresIn = (int)$response->json('expires_in', self::TOKEN_FALLBACK_TTL_SECONDS); $ttlSeconds = max($expiresIn - self::TOKEN_TTL_BUFFER_SECONDS, 60); return [$accessToken, $ttlSeconds]; @@ -140,8 +137,8 @@ private function getHeaders(bool $forceRefreshToken = false, array $extraHeaders { return array_merge([ 'Authorization' => 'Bearer '.$this->getAccessToken($forceRefreshToken), - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', ], $extraHeaders); } @@ -164,40 +161,40 @@ public function handle(DeliveryNote $parent, Shipper $shipper): array { $manifestLockKey = $this->getManifestLockKey($parent, $shipper); - if (! Cache::add($manifestLockKey, 1, self::MANIFEST_LOCK_TTL_SECONDS)) { + if (!Cache::add($manifestLockKey, 1, self::MANIFEST_LOCK_TTL_SECONDS)) { return [ - 'status' => 'fail', + 'status' => 'fail', 'modelData' => [], 'errorData' => [ 'message' => ['A shipment manifest request is already in progress for this delivery note.'], - 'others' => [], + 'others' => [], ], ]; } try { - $url = '/manifest/v2.0/shippings'; + $url = '/manifest/v2.0/shippings'; $parentResource = GetShippingDeliveryNoteData::run($parent); - $parcels = $parent->parcels ?? []; + $parcels = $parent->parcels ?? []; $items = []; foreach ($parcels as $parcel) { $dimensions = Arr::get($parcel, 'dimensions', []); $items[] = [ - 'item_weight_declared' => (float) Arr::get($parcel, 'weight', 1), - 'item_length_declared' => (float) Arr::get($dimensions, 0, 10), - 'item_width_declared' => (float) Arr::get($dimensions, 1, 10), - 'item_height_declared' => (float) Arr::get($dimensions, 2, 10), - 'item_comments' => 'Item from AW Artisan', + 'item_weight_declared' => (float)Arr::get($parcel, 'weight', 1), + 'item_length_declared' => (float)Arr::get($dimensions, 0, 10), + 'item_width_declared' => (float)Arr::get($dimensions, 1, 10), + 'item_height_declared' => (float)Arr::get($dimensions, 2, 10), + 'item_comments' => 'Item from AW Artisan', ]; } $fromAddress = Arr::get($parentResource, 'from_address', []); - $toAddress = Arr::get($parentResource, 'to_address', []); + $toAddress = Arr::get($parentResource, 'to_address', []); - $customerReference = (string) Arr::get($parentResource, 'customer_reference', $parent->reference); - $idempotencyKey = hash('sha256', implode('|', [ + $customerReference = (string)Arr::get($parentResource, 'customer_reference', $parent->reference); + $idempotencyKey = hash('sha256', implode('|', [ 'ctt-es', $shipper->id, $parent->id, @@ -205,101 +202,107 @@ public function handle(DeliveryNote $parent, Shipper $shipper): array ])); $params = [ - 'client_center_code' => $this->getClientCenterCode(), - 'shipping_type_code' => 'C24', - 'department_code' => '1', - 'client_references' => [$customerReference], + 'client_center_code' => $this->getClientCenterCode(), + 'shipping_type_code' => 'C24', + 'department_code' => '1', + 'client_references' => [$customerReference], 'shipping_weight_declared' => collect($parcels)->sum('weight') ?: 1, - 'item_count' => count($items), + 'item_count' => count($items), - 'sender_name' => Str::limit( + 'sender_name' => Str::limit( Arr::get($parentResource, 'from_company_name') - ?: Arr::get($parentResource, 'from_contact_name', 'AW Artisan'), + ?: Arr::get($parentResource, 'from_contact_name', 'AW Artisan'), 60 ), - 'sender_country_code' => Arr::get($fromAddress, 'country_code', 'ES'), - 'sender_postal_code' => Arr::get($fromAddress, 'postal_code', '28821'), - 'sender_address' => trim(Arr::get($fromAddress, 'address_line_1', '').' '.Arr::get($fromAddress, 'address_line_2', '')), - 'sender_town' => Arr::get($fromAddress, 'locality', 'Coslada'), + 'sender_country_code' => Arr::get($fromAddress, 'country_code', 'ES'), + 'sender_postal_code' => Arr::get($fromAddress, 'postal_code', '28821'), + 'sender_address' => trim(Arr::get($fromAddress, 'address_line_1', '').' '.Arr::get($fromAddress, 'address_line_2', '')), + 'sender_town' => Arr::get($fromAddress, 'locality', 'Coslada'), 'sender_email_notify_address' => Arr::get($parentResource, 'from_email'), - 'sender_phones' => array_values(array_filter( - [Arr::get($parentResource, 'from_phone')], - fn ($phone) => filled($phone) - )), + 'sender_phones' => array_values( + array_filter( + [Arr::get($parentResource, 'from_phone')], + fn($phone) => filled($phone) + ) + ), - 'recipient_name' => Str::limit( + 'recipient_name' => Str::limit( Arr::get($parentResource, 'to_company_name') - ?: Arr::get($parentResource, 'to_contact_name', 'Customer'), + ?: Arr::get($parentResource, 'to_contact_name', 'Customer'), 60 ), - 'recipient_country_code' => Arr::get($toAddress, 'country_code', 'ES'), - 'recipient_postal_code' => Arr::get($toAddress, 'postal_code', '08850'), - 'recipient_address' => trim(Arr::get($toAddress, 'address_line_1', '').' '.Arr::get($toAddress, 'address_line_2', '')), - 'recipient_town' => Arr::get($toAddress, 'locality', 'Madrid'), + 'recipient_country_code' => Arr::get($toAddress, 'country_code', 'ES'), + 'recipient_postal_code' => Arr::get($toAddress, 'postal_code', '08850'), + 'recipient_address' => trim(Arr::get($toAddress, 'address_line_1', '').' '.Arr::get($toAddress, 'address_line_2', '')), + 'recipient_town' => Arr::get($toAddress, 'locality', 'Madrid'), 'recipient_email_notify_address' => Arr::get($parentResource, 'to_email'), - 'recipient_phones' => array_values(array_filter( - [Arr::get($parentResource, 'to_phone')], - fn ($phone) => filled($phone) - )), + 'recipient_phones' => array_values( + array_filter( + [Arr::get($parentResource, 'to_phone')], + fn($phone) => filled($phone) + ) + ), 'shipping_date' => now()->format('Y-m-d'), - 'delivery' => [ - 'contact_name' => Str::limit((string) Arr::get($parentResource, 'to_contact_name', ''), 60), - 'comments' => Str::limit(strip_tags((string) ($parent->shipping_notes ?? '')), 100), + 'delivery' => [ + 'contact_name' => Str::limit((string)Arr::get($parentResource, 'to_contact_name', ''), 60), + 'comments' => Str::limit(strip_tags((string)($parent->shipping_notes ?? '')), 100), ], - 'items' => $items, + 'items' => $items, ]; - $params['custom_origin_name'] = $params['sender_name']; + $params['custom_origin_name'] = $params['sender_name']; $params['custom_origin_country_code'] = $params['sender_country_code']; - $params['custom_origin_postal_code'] = $params['sender_postal_code']; - $params['custom_origin_address'] = $params['sender_address']; - $params['custom_origin_town'] = $params['sender_town']; + $params['custom_origin_postal_code'] = $params['sender_postal_code']; + $params['custom_origin_address'] = $params['sender_address']; + $params['custom_origin_town'] = $params['sender_town']; $codData = Arr::get($parentResource, 'cash_on_delivery'); - if (! empty($codData)) { - $params['additionals'] = [[ - 'additional_code' => 'REE', - 'additional_value' => (float) Arr::get($codData, 'amount', 0), - 'additional_flag' => true, - 'additional_text' => 'Cash on delivery payment', - 'additional_sub_code' => '', - ]]; + if (!empty($codData)) { + $params['additionals'] = [ + [ + 'additional_code' => 'REE', + 'additional_value' => (float)Arr::get($codData, 'amount', 0), + 'additional_flag' => true, + 'additional_text' => 'Cash on delivery payment', + 'additional_sub_code' => '', + ] + ]; } // Do not auto-retry manifest POST to avoid accidental duplicate shipments on ambiguous failures. - $response = $this->requestWithTokenRefresh(function (bool $forceRefreshToken) use ($url, $params, $idempotencyKey) { + $response = $this->requestWithTokenRefresh(function (bool $forceRefreshToken) use ($url, $params, $idempotencyKey, $shipper) { return Http::withHeaders($this->getHeaders($forceRefreshToken, [ 'Idempotency-Key' => $idempotencyKey, ])) ->connectTimeout(10) ->timeout(45) - ->post($this->getBaseUrl().$url, $params); + ->post($this->getBaseUrl($shipper).$url, $params); }); $apiResponse = $response->json(); - $statusCode = $response->status(); + $statusCode = $response->status(); $modelData = ['api_response' => $apiResponse]; $errorData = []; if ($statusCode === 201 && Arr::has($apiResponse, 'shipping_data.shipping_code')) { - $status = 'success'; + $status = 'success'; $shippingCode = Arr::get($apiResponse, 'shipping_data.shipping_code'); - $modelData['trackings'] = [$shippingCode]; - $modelData['tracking'] = $shippingCode; - $modelData['label_type'] = ShipmentLabelTypeEnum::PDF; + $modelData['trackings'] = [$shippingCode]; + $modelData['tracking'] = $shippingCode; + $modelData['label_type'] = ShipmentLabelTypeEnum::PDF; $modelData['number_parcels'] = count($parcels); - $modelData['label'] = $this->getLabel($shippingCode); + $modelData['label'] = $this->getLabel($shippingCode); } else { - $status = 'fail'; + $status = 'fail'; $errorData['message'][] = Arr::get($apiResponse, 'error.error_description', 'Failed to manifest'); - $errorData['others'][] = Arr::get($apiResponse, 'error.error_extended_info.message', ''); + $errorData['others'][] = Arr::get($apiResponse, 'error.error_extended_info.message', ''); } return [ - 'status' => $status, + 'status' => $status, 'modelData' => $modelData, 'errorData' => $errorData, ]; @@ -311,7 +314,7 @@ public function handle(DeliveryNote $parent, Shipper $shipper): array public function getLabel(string $shippingCode): string { $content = ''; - $url = "/trf/labelling/v1.0/shippings/{$shippingCode}/shipping-labels?label_type_code=PDF&model_type_code=SINGLE&label_offset=1"; + $url = "/trf/labelling/v1.0/shippings/{$shippingCode}/shipping-labels?label_type_code=PDF&model_type_code=SINGLE&label_offset=1"; for ($attempt = 1; $attempt <= 3; $attempt++) { try { @@ -324,7 +327,7 @@ public function getLabel(string $shippingCode): string if ($response->successful()) { $apiResponse = $response->json(); - $content = (string) Arr::get($apiResponse, 'data.0.label', ''); + $content = (string)Arr::get($apiResponse, 'data.0.label', ''); if ($content !== '') { return $content; diff --git a/app/Actions/Maintenance/Web/GetRelatedProductsFromAurora.php b/app/Actions/Maintenance/Web/GetRelatedProductsFromAurora.php new file mode 100644 index 00000000000..675c6169f05 --- /dev/null +++ b/app/Actions/Maintenance/Web/GetRelatedProductsFromAurora.php @@ -0,0 +1,85 @@ + + * Created: Tue, 05 May 2026 14:32:22 Malaysia Time, Kuala Lumpur, Malaysia + * Copyright (c) 2026, Raul A Perusquia Flores + */ + + + + +/** @noinspection DuplicatedCode */ + +namespace App\Actions\Maintenance\Web; + +use App\Actions\Traits\WithOrganisationSource; +use App\Models\Catalogue\Shop; +use App\Models\Web\Webpage; +use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; +use Lorisleiva\Actions\Concerns\AsAction; + +class GetRelatedProductsFromAurora +{ + use AsAction; + use WithOrganisationSource; + + public function handle(Shop $shop, ?Command $command = null): void + { + $organisation = $shop->organisation; + $this->organisationSource = $this->getOrganisationSource($organisation); + $this->organisationSource->initialisation($organisation); + + + Webpage::query() + ->where( + 'website_id', + $shop->website->id + ) + ->whereNotNull('source_id') + + // ->where('id',31890) + ->orderBy('id') + ->chunkById(1000, function ($webpages) use ($command) { + foreach ($webpages as $webpage) { + $sourceData = explode(':', $webpage->source_id); + + $auData = DB::connection('aurora')->table('Page Store Dimension') + ->where('Page Key', $sourceData[1]) + ->first(); + + if ($auData) { + if ($auData->{'browser_title'} != '') { + $webpage->update( + [ + 'title' => $auData->{'browser_title'} + ] + ); + } + + if ($auData->{'Webpage Meta Description'} != '') { + $webpage->update( + [ + 'description' => $auData->{'Webpage Meta Description'} + ] + ); + } + } + } + }, 'id'); + } + + + public function getCommandSignature(): string + { + return 'maintenance:get_aurora_related_products'; + } + + public function asCommand(Command $command): int + { + $this->handle($shop, $command); + + return 0; + } + +} From b2c9c62df5cafa2dde429f703f4a633b9d2bd0b3 Mon Sep 17 00:00:00 2001 From: aqordeon Date: Tue, 5 May 2026 16:08:28 +0800 Subject: [PATCH 04/59] feat: Pages: BlankLayout --- resources/js/Layouts/BlankLayout.vue | 32 ++++++++++ .../js/Pages/Grp/EmailFallbackOnError.vue | 64 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 resources/js/Layouts/BlankLayout.vue create mode 100644 resources/js/Pages/Grp/EmailFallbackOnError.vue diff --git a/resources/js/Layouts/BlankLayout.vue b/resources/js/Layouts/BlankLayout.vue new file mode 100644 index 00000000000..683e6eb22f3 --- /dev/null +++ b/resources/js/Layouts/BlankLayout.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/resources/js/Pages/Grp/EmailFallbackOnError.vue b/resources/js/Pages/Grp/EmailFallbackOnError.vue new file mode 100644 index 00000000000..3aaf5d296c7 --- /dev/null +++ b/resources/js/Pages/Grp/EmailFallbackOnError.vue @@ -0,0 +1,64 @@ + + + + + From 901185761d6a0a164f2366e96a3c355ffcde317b Mon Sep 17 00:00:00 2001 From: YudhistiraA Date: Tue, 5 May 2026 16:12:05 +0800 Subject: [PATCH 05/59] refactor: clean up commented code in family3Iris and family3Workshop components --- .../CMS/Webpage/Family3/family3Iris.vue | 22 ++++++++++++------- .../CMS/Webpage/Family3/family3Workshop.vue | 13 +++++------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/resources/js/Components/CMS/Webpage/Family3/family3Iris.vue b/resources/js/Components/CMS/Webpage/Family3/family3Iris.vue index b07503402cf..7c783b2ac80 100644 --- a/resources/js/Components/CMS/Webpage/Family3/family3Iris.vue +++ b/resources/js/Components/CMS/Webpage/Family3/family3Iris.vue @@ -82,13 +82,13 @@ console.log("Family2 Workshop Props:", props)
- -
--> +
@@ -137,17 +137,23 @@ console.log("Family2 Workshop Props:", props)
-

+
+ +
-->

diff --git a/resources/js/Components/CMS/Webpage/Family3/family3Workshop.vue b/resources/js/Components/CMS/Webpage/Family3/family3Workshop.vue index c7247c35cef..d4c9d375be5 100644 --- a/resources/js/Components/CMS/Webpage/Family3/family3Workshop.vue +++ b/resources/js/Components/CMS/Webpage/Family3/family3Workshop.vue @@ -143,7 +143,7 @@ console.log("Family2 Workshop Props:", props)
-
- --> +
@@ -203,13 +203,13 @@ console.log("Family2 Workshop Props:", props)
-

+

-