diff --git a/Makefile b/Makefile index 88df650..3f2a1ff 100644 --- a/Makefile +++ b/Makefile @@ -126,7 +126,8 @@ zip: rsync -a $(RSYNC_EXCLUDES) ./ $(STAGE_DIR)/ cp composer.json $(STAGE_DIR)/ @if [ -f composer.lock ]; then cp composer.lock $(STAGE_DIR)/; fi - cd $(STAGE_DIR) && composer install --no-dev --optimize-autoloader --no-interaction 2>/dev/null; rm -f $(STAGE_DIR)/composer.json $(STAGE_DIR)/composer.lock + (cd $(STAGE_DIR) && composer install --no-dev --optimize-autoloader --no-interaction 2>/dev/null) + rm -f $(STAGE_DIR)/composer.lock $(STAGE_DIR)/vendor/bin/.phpunit.result.cache @if [ "$(KIRIOF_ENV)" != "prd" ] && [ -f .env ]; then \ API_URL=$$(grep '^$(ENV_VAR_NAME)=' .env | head -1 | cut -d= -f2- | xargs); \ if [ -n "$$API_URL" ]; then \ diff --git a/inc/Controllers/CheckoutController.php b/inc/Controllers/CheckoutController.php index 196eb70..f1e5c24 100644 --- a/inc/Controllers/CheckoutController.php +++ b/inc/Controllers/CheckoutController.php @@ -635,14 +635,13 @@ function add_custom_select_options_field_and_script_shipping() } private function kiriof_get_posted_text_field( string $key ): string { - // phpcs:disable WordPress.Security.NonceVerification.Missing -- Read-only checkout POST normalization; WooCommerce verifies checkout/update-order-review requests. + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only checkout POST normalization; WooCommerce verifies checkout/update-order-review requests. if ( ! isset( $_POST[ $key ] ) || is_array( $_POST[ $key ] ) ) { - // phpcs:enable WordPress.Security.NonceVerification.Missing return ''; } + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only checkout POST normalization; WooCommerce verifies checkout/update-order-review requests. $value = sanitize_text_field( wp_unslash( $_POST[ $key ] ) ); - // phpcs:enable WordPress.Security.NonceVerification.Missing return $value; } diff --git a/inc/Controllers/SettingController.php b/inc/Controllers/SettingController.php index a28695f..c7da36c 100644 --- a/inc/Controllers/SettingController.php +++ b/inc/Controllers/SettingController.php @@ -364,9 +364,6 @@ function storeCourierWhitelist() { 'origin_whitelist_expedition_name'=> $whitelist_names, )); - // Bust the couriers API cache so the updated whitelist is reflected immediately. - ( new \KiriminAjaOfficial\Services\KiriminajaApiService() )->invalidateCouriersCache(); - wp_send_json_success(['status' => 200, 'message' => 'Saved']); }catch (Throwable $e){ wp_send_json_error(['status'=>400,'message'=>$e->getMessage()]); diff --git a/inc/Controllers/ShippingDiscountCouponController.php b/inc/Controllers/ShippingDiscountCouponController.php index 57143d2..dcdfa71 100644 --- a/inc/Controllers/ShippingDiscountCouponController.php +++ b/inc/Controllers/ShippingDiscountCouponController.php @@ -947,11 +947,11 @@ public function flushCouriersCache() { } $service = new KiriminajaApiService(); - $service->invalidateCouriersCache(); + $service->invalidateCouriersCache( false ); $fresh = $service->get_couriers(); - if ( 200 !== $fresh->status ) { - wp_send_json_error( array( 'message' => __( 'Flushed, but could not re-fetch from API. Will retry on next page load.', 'kiriminaja-official' ) ) ); + if ( 200 !== $fresh->status || 'courier_cache_fallback' === $fresh->customCode ) { + wp_send_json_error( array( 'message' => __( 'Could not refresh couriers from the API. The last successful courier list remains available.', 'kiriminaja-official' ) ) ); } $count = is_array( $fresh->data ) ? count( $fresh->data ) : 0; diff --git a/inc/Controllers/ShippingProcessController.php b/inc/Controllers/ShippingProcessController.php index 7d924aa..aeb522f 100644 --- a/inc/Controllers/ShippingProcessController.php +++ b/inc/Controllers/ShippingProcessController.php @@ -119,6 +119,7 @@ private function markTransactionsPrinted( array $orderIds ) // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- Placeholder list is generated from sanitized order IDs above. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Print status is updated immediately after successful label fetch. $wpdb->query( + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic IN placeholders match the sanitized order IDs in $query_args. $wpdb->prepare( "UPDATE {$wpdb->prefix}kiriminaja_transactions SET is_printed = %d, printed_at = %s diff --git a/inc/Services/KiriminajaApiService.php b/inc/Services/KiriminajaApiService.php index bf692fb..b6f444a 100644 --- a/inc/Services/KiriminajaApiService.php +++ b/inc/Services/KiriminajaApiService.php @@ -42,6 +42,7 @@ public function getTracking($order_id) return self::success($repo['data']); } private const KIRIOF_COURIERS_CACHE_KEY = 'kiriof_couriers_list_v2'; + private const KIRIOF_COURIERS_LAST_SUCCESS_CACHE_KEY = 'kiriof_couriers_last_success_cache'; private const KIRIOF_COURIERS_CACHE_TTL = DAY_IN_SECONDS; public function get_couriers(){ @@ -52,6 +53,17 @@ public function get_couriers(){ $repo = (new \KiriminAjaOfficial\Repositories\KiriminajaApiRepository())->get_couriers(); if ( empty( $repo['status'] ) || ! is_object( $repo['data'] ?? null ) || empty( $repo['data']->status ) ) { + $last_success = get_transient( self::KIRIOF_COURIERS_LAST_SUCCESS_CACHE_KEY ); + if ( false !== $last_success ) { + kiriof_log( + 'warning', + 'Courier lookup used the cached fallback because the live API request failed.', + array( 'source' => 'kiriminaja_api' ) + ); + set_transient( self::KIRIOF_COURIERS_CACHE_KEY, $last_success, self::KIRIOF_COURIERS_CACHE_TTL ); + return self::success( $last_success, 'Using cached courier data.', 'courier_cache_fallback' ); + } + return self::error( array(), $this->extractErrorMessage( $repo, 'Something is wrong' ) ); } @@ -66,11 +78,15 @@ function ( $courier ) use ( $excluded_types ) { ) ); set_transient( self::KIRIOF_COURIERS_CACHE_KEY, $data, self::KIRIOF_COURIERS_CACHE_TTL ); + set_transient( self::KIRIOF_COURIERS_LAST_SUCCESS_CACHE_KEY, $data, WEEK_IN_SECONDS ); return self::success( $data ); } - public function invalidateCouriersCache(): void { + public function invalidateCouriersCache( bool $include_last_success = true ): void { delete_transient( self::KIRIOF_COURIERS_CACHE_KEY ); + if ( $include_last_success ) { + delete_transient( self::KIRIOF_COURIERS_LAST_SUCCESS_CACHE_KEY ); + } } public function invalidateProfileCache(): void { @@ -216,4 +232,4 @@ private function extractErrorMessage($repo, string $fallback): string { return $fallback; } -} \ No newline at end of file +} diff --git a/inc/Services/SettingService.php b/inc/Services/SettingService.php index ce8b923..3ba6e7a 100644 --- a/inc/Services/SettingService.php +++ b/inc/Services/SettingService.php @@ -50,6 +50,7 @@ public function processingSetupKey($setupKey) /** Storing result to DB*/ $arrayRepoDataData = (array) $arrayRepoData['result']; (new \KiriminAjaOfficial\Services\KiriminajaApiService())->invalidateProfileCache(); + (new \KiriminAjaOfficial\Services\KiriminajaApiService())->invalidateCouriersCache(); (new \KiriminAjaOfficial\Repositories\SettingRepository())->storeIntegrationData([ 'api_key' => sanitize_text_field($arrayRepoDataData['api_key']), 'oid_prefix' => sanitize_text_field($arrayRepoDataData['oid_prefix']), @@ -82,6 +83,7 @@ public function disconnectIntegration() { try { (new \KiriminAjaOfficial\Services\KiriminajaApiService())->invalidateProfileCache(); + (new \KiriminAjaOfficial\Services\KiriminajaApiService())->invalidateCouriersCache(); $repo = (new \KiriminAjaOfficial\Repositories\SettingRepository())->disconnectIntegration(); kiriof_log( 'notice', diff --git a/lang/kiriminaja-official-id_ID.po b/lang/kiriminaja-official-id_ID.po index 0841662..12ddca0 100644 --- a/lang/kiriminaja-official-id_ID.po +++ b/lang/kiriminaja-official-id_ID.po @@ -13,6 +13,22 @@ msgstr "" "Plural-Forms: nplurals=1; plural=0;\n" "X-Domain: kiriminaja-official\n" +#: templates/setting/setuped/section-couriers.php +msgid "Could not load couriers. Reload this page and try again." +msgstr "Tidak dapat memuat kurir. Muat ulang halaman ini dan coba lagi." + +#: inc/Controllers/ShippingDiscountCouponController.php +msgid "Could not refresh couriers from the API. The last successful courier list remains available." +msgstr "Tidak dapat memperbarui kurir dari API. Daftar kurir terakhir yang berhasil masih tersedia." + +#: templates/setting/setuped/section-couriers.php +msgid "Could not save courier settings." +msgstr "Tidak dapat menyimpan pengaturan kurir." + +#: templates/setting/setuped/section-couriers.php +msgid "No couriers are available for this account." +msgstr "Tidak ada kurir yang tersedia untuk akun ini." + #. translators: %s: total number of items #: templates/transaction-process/view/index.php:86 #: templates/transaction-process/view/index.php:283 diff --git a/templates/request-pickup/index.php b/templates/request-pickup/index.php index 0256c82..ff8d19c 100644 --- a/templates/request-pickup/index.php +++ b/templates/request-pickup/index.php @@ -56,8 +56,7 @@ private function pageQuery(){ $items_per_page = 20; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $page = isset($_GET['cpage']) ? absint($_GET['cpage']) : 1; - $offset = ( $page * $items_per_page ) - $items_per_page; + $page = isset($_GET['cpage']) ? max( 1, absint($_GET['cpage']) ) : 1; $whereConditions = []; @@ -72,18 +71,36 @@ private function pageQuery(){ $whereConditions[] = $wpdb->prepare("kiriminaja_payments.created_at LIKE %s", '%' . $wpdb->esc_like($month) . '%'); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if (!empty(sanitize_text_field(wp_unslash($_GET['status'] ?? '')))) { - $status = sanitize_text_field(wp_unslash($_GET['status'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $status = sanitize_text_field(wp_unslash($_GET['status'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( in_array( $status, array( 'unpaid', 'paid' ), true ) ) { $whereConditions[] = $wpdb->prepare("kiriminaja_payments.status = %s", $status); } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Each condition is individually prepared above $whereCondition = !empty($whereConditions) ? 'WHERE ' . implode(' AND ', $whereConditions) : ''; + /** Pagination Query*/ + $count_sql = "SELECT + kiriminaja_payments.id, kiriminaja_payments.pickup_number + FROM {$wpdb->prefix}kiriminaja_payments as kiriminaja_payments + INNER JOIN {$wpdb->prefix}kiriminaja_transactions as kiriminaja_transactions + ON kiriminaja_payments.pickup_number = kiriminaja_transactions.pickup_number + " . $whereCondition . " + GROUP BY kiriminaja_payments.pickup_number"; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query safe: whereCondition contains pre-prepared statements, no additional params needed + $totalQuery = $wpdb->get_results($count_sql); + $total = count($totalQuery); + $total_pages = (int) ceil($total/$items_per_page); + if ( $page > $total_pages && $total_pages > 0 ) { + $page = $total_pages; + } + $offset = ( $page - 1 ) * $items_per_page; + /** Main Query*/ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names use prefix, whereCondition pre-prepared above - $baseQuery = "SELECT - kiriminaja_payments.*, + $baseQuery = "SELECT + kiriminaja_payments.*, SUM(CASE WHEN kiriminaja_transactions.cod_fee = 0 THEN kiriminaja_transactions.shipping_cost - COALESCE(kiriminaja_transactions.discount_amount, 0) + kiriminaja_transactions.insurance_cost ELSE 0 END) AS cost FROM {$wpdb->prefix}kiriminaja_payments as kiriminaja_payments INNER JOIN {$wpdb->prefix}kiriminaja_transactions as kiriminaja_transactions @@ -94,28 +111,14 @@ private function pageQuery(){ LIMIT %d, %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter - $results = $wpdb->get_results( + $results = $wpdb->get_results( $wpdb->prepare($baseQuery, $offset, $items_per_page) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared ); - + if (strlen(@$wpdb->last_error ?? '') > 0){ (new \KiriminAjaOfficial\Base\BaseInit())->logThis('last_error',@$wpdb->last_error); } - /** Pagination Query*/ - $count_sql = "SELECT - kiriminaja_payments.id, kiriminaja_payments.pickup_number - FROM {$wpdb->prefix}kiriminaja_payments as kiriminaja_payments - INNER JOIN {$wpdb->prefix}kiriminaja_transactions as kiriminaja_transactions - ON kiriminaja_payments.pickup_number = kiriminaja_transactions.pickup_number - " . $whereCondition . " - GROUP BY kiriminaja_payments.pickup_number"; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query safe: whereCondition contains pre-prepared statements, no additional params needed - $totalQuery = $wpdb->get_results($count_sql); - $total = count($totalQuery); - $total_pages = ceil($total/$items_per_page); - /** Paginate*/ $next_page_link = admin_url( 'admin.php?' ); $prev_page_link = admin_url( 'admin.php?' ); diff --git a/templates/setting/setuped/section-couriers.php b/templates/setting/setuped/section-couriers.php index a7dc2ef..252c56a 100644 --- a/templates/setting/setuped/section-couriers.php +++ b/templates/setting/setuped/section-couriers.php @@ -1,5 +1,5 @@
' + escHtml(text) + '