From 75fd65be2bbec3cf82c7be6aab1f8b9326299e02 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Thu, 29 Oct 2026 00:51:50 +0330 Subject: [PATCH] feat(api): expand shipment data fields and enhance tracking security Implement additional address and identification fields for both sender and receiver across the WordPress plugin and Laravel API. This includes state, postal code, and national ID/passport information. Key changes: - Added database migration to include postal and national ID fields in the shipments table. - Updated WordPress order form shortcodes and JavaScript to capture new address fields. - Refactored Laravel `CustomerOrderController` validation and storage to support expanded shipment data. - Implemented tiered data visibility in `TrackController` to protect PII, providing masked data to anonymous users and full data to owners/admins. - Fixed weight-based rate lookup logic in `PriceCalculatorService` to correctly select the appropriate shipping tier. --- .../assets/js/ifnex-order-form.js | 6 + .../ifnex-bridge/includes/shortcodes.php | 16 ++- .../Api/Customer/CustomerOrderController.php | 90 +++++++------ .../Http/Controllers/Api/TrackController.php | 121 +++++++++++++----- 04_Laravel/app/Models/Shipment.php | 6 + .../app/Services/PriceCalculatorService.php | 20 ++- ...tal_and_national_id_to_shipments_table.php | 39 ++++++ 7 files changed, 220 insertions(+), 78 deletions(-) create mode 100644 04_Laravel/database/migrations/2026_10_27_033451_add_postal_and_national_id_to_shipments_table.php diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/assets/js/ifnex-order-form.js b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/js/ifnex-order-form.js index bb87e08..d1fa1dc 100644 --- a/03_WordPress/wp-content/plugins/ifnex-bridge/assets/js/ifnex-order-form.js +++ b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/js/ifnex-order-form.js @@ -940,6 +940,12 @@ jQuery(document).ready(function($) { receiver_phone: $('#ifnex-receiver-phone').val(), receiver_city: $('#ifnex-receiver-city').val(), receiver_address: $('#ifnex-receiver-address').val(), + sender_state: $('#ifnex-sender-state').val(), + sender_postal_code: $('#ifnex-sender-postal-code').val(), + sender_national_id: $('#ifnex-sender-national-id').val(), + receiver_state: $('#ifnex-receiver-state').val(), + receiver_postal_code: $('#ifnex-receiver-postal-code').val(), + receiver_national_id: $('#ifnex-receiver-national-id').val(), }; } diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php index 7f3f71a..bf5f617 100644 --- a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php +++ b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php @@ -749,14 +749,20 @@ function ifnex_order_form_shortcode($atts) {
-
+
+
+
+

📮 اطلاعات گیرنده

-
+
+
+
+
@@ -1287,12 +1293,18 @@ function ifnex_order_detail_shortcode($atts) {

📞

📍

+

🏙️

+

📮

+

🪪

📮 گیرنده

📞

📍

+

🏙️

+

📮

+

🪪

diff --git a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php index c2a4ea4..1b07a8f 100644 --- a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php +++ b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php @@ -95,20 +95,22 @@ class CustomerOrderController extends Controller 'sender_phone' => ['required', 'string', 'max:50'], 'sender_email' => ['nullable', 'email', 'max:255'], 'sender_address' => ['required', 'string', 'max:1000'], + 'sender_state' => ['nullable', 'string', 'max:255'], 'sender_city' => ['nullable', 'string', 'max:255'], - 'sender_zip' => ['nullable', 'string', 'max:20'], - 'sender_id_number' => ['nullable', 'string', 'max:50'], - + 'sender_postal_code' => ['nullable', 'string', 'max:20'], + 'sender_national_id' => ['nullable', 'string', 'max:50'], + // اطلاعات گیرنده 'receiver_name' => ['required', 'string', 'max:255'], 'receiver_company' => ['nullable', 'string', 'max:255'], 'receiver_phone' => ['required', 'string', 'max:50'], 'receiver_email' => ['nullable', 'email', 'max:255'], 'receiver_address' => ['required', 'string', 'max:1000'], + 'receiver_state' => ['nullable', 'string', 'max:255'], 'receiver_city' => ['nullable', 'string', 'max:255'], - 'receiver_zip' => ['nullable', 'string', 'max:20'], - 'receiver_id_number' => ['nullable', 'string', 'max:50'], - + 'receiver_postal_code' => ['nullable', 'string', 'max:20'], + 'receiver_national_id' => ['nullable', 'string', 'max:50'], + // خدمات اضافی 'extra_service' => ['nullable', 'numeric', 'min:0'], 'packing_cost' => ['nullable', 'numeric', 'min:0'], @@ -245,19 +247,21 @@ class CustomerOrderController extends Controller 'sender_phone' => $validated['sender_phone'], 'sender_email' => $validated['sender_email'] ?? null, 'sender_address' => $validated['sender_address'], + 'sender_state' => $validated['sender_state'] ?? null, 'sender_city' => $validated['sender_city'] ?? null, - 'sender_zip' => $validated['sender_zip'] ?? null, - 'sender_id_number' => $validated['sender_id_number'] ?? null, - + 'sender_postal_code' => $validated['sender_postal_code'] ?? null, + 'sender_national_id' => $validated['sender_national_id'] ?? null, + // گیرنده 'receiver_name' => $validated['receiver_name'], 'receiver_company' => $validated['receiver_company'] ?? null, 'receiver_phone' => $validated['receiver_phone'], 'receiver_email' => $validated['receiver_email'] ?? null, 'receiver_address' => $validated['receiver_address'], + 'receiver_state' => $validated['receiver_state'] ?? null, 'receiver_city' => $validated['receiver_city'] ?? null, - 'receiver_zip' => $validated['receiver_zip'] ?? null, - 'receiver_id_number' => $validated['receiver_id_number'] ?? null, + 'receiver_postal_code' => $validated['receiver_postal_code'] ?? null, + 'receiver_national_id' => $validated['receiver_national_id'] ?? null, ]); // ذخیره بسته‌ها (Multi-Package) @@ -406,9 +410,10 @@ class CustomerOrderController extends Controller 'sender_phone' => ['required', 'string', 'max:50'], 'sender_email' => ['nullable', 'email', 'max:255'], 'sender_address' => ['required', 'string', 'max:1000'], + 'sender_state' => ['nullable', 'string', 'max:255'], 'sender_city' => ['nullable', 'string', 'max:255'], - 'sender_zip' => ['nullable', 'string', 'max:20'], - 'sender_id_number' => ['nullable', 'string', 'max:50'], + 'sender_postal_code' => ['nullable', 'string', 'max:20'], + 'sender_national_id' => ['nullable', 'string', 'max:50'], // گیرنده 'receiver_name' => ['required', 'string', 'max:255'], @@ -416,9 +421,10 @@ class CustomerOrderController extends Controller 'receiver_phone' => ['required', 'string', 'max:50'], 'receiver_email' => ['nullable', 'email', 'max:255'], 'receiver_address' => ['required', 'string', 'max:1000'], + 'receiver_state' => ['nullable', 'string', 'max:255'], 'receiver_city' => ['nullable', 'string', 'max:255'], - 'receiver_zip' => ['nullable', 'string', 'max:20'], - 'receiver_id_number' => ['nullable', 'string', 'max:50'], + 'receiver_postal_code' => ['nullable', 'string', 'max:20'], + 'receiver_national_id' => ['nullable', 'string', 'max:50'], // خدمات 'extra_service' => ['nullable', 'numeric', 'min:0'], @@ -563,18 +569,20 @@ class CustomerOrderController extends Controller 'sender_phone' => $validated['sender_phone'], 'sender_email' => $validated['sender_email'] ?? null, 'sender_address' => $validated['sender_address'], + 'sender_state' => $validated['sender_state'] ?? null, 'sender_city' => $validated['sender_city'] ?? null, - 'sender_zip' => $validated['sender_zip'] ?? null, - 'sender_id_number' => $validated['sender_id_number'] ?? null, + 'sender_postal_code' => $validated['sender_postal_code'] ?? null, + 'sender_national_id' => $validated['sender_national_id'] ?? null, 'receiver_name' => $validated['receiver_name'], 'receiver_company' => $validated['receiver_company'] ?? null, 'receiver_phone' => $validated['receiver_phone'], 'receiver_email' => $validated['receiver_email'] ?? null, 'receiver_address' => $validated['receiver_address'], + 'receiver_state' => $validated['receiver_state'] ?? null, 'receiver_city' => $validated['receiver_city'] ?? null, - 'receiver_zip' => $validated['receiver_zip'] ?? null, - 'receiver_id_number' => $validated['receiver_id_number'] ?? null, + 'receiver_postal_code' => $validated['receiver_postal_code'] ?? null, + 'receiver_national_id' => $validated['receiver_national_id'] ?? null, ]); $shipment->packages()->delete(); @@ -720,12 +728,6 @@ class CustomerOrderController extends Controller $account_balance = $wallet_balance - $pending_payments_total; // بدهی‌های چندارزی (EUR/USD/AED/CNY) — به همان ارز خارجی، نه معادل ریالی - $foreign_credits = app(\App\Services\CustomerCreditService::class) - ->getCustomerDebtsByCurrency($user); - - $account_balance = $wallet_balance - $pending_payments_total; - - // بدهی‌های چندارزی — به همان ارز خارجی $foreign_credits = app(\App\Services\CustomerCreditService::class) ->getCustomerDebtsByCurrency($user); @@ -777,22 +779,22 @@ class CustomerOrderController extends Controller ]); } - /** - * لیست کشورها برای فرم - * GET /api/v1/customer/countries - */ - public function countries(): JsonResponse - { - $countries = Country::select('id', 'name', 'iso_code', 'export_zone_parcel', 'import_zone_parcel') - ->where('is_active', true) - ->orderBy('name') - ->get(); + /** + * لیست کشورها برای فرم + * GET /api/v1/customer/countries + */ + public function countries(): JsonResponse + { + $countries = Country::select('id', 'name', 'iso_code', 'export_zone_parcel', 'import_zone_parcel') + ->where('is_active', true) + ->orderBy('name') + ->get(); - return response()->json([ - 'success' => true, - 'data' => $countries, - ]); - } + return response()->json([ + 'success' => true, + 'data' => $countries, + ]); + } /** * دریافت نوتیفیکیشن‌های کاربر @@ -938,7 +940,10 @@ class CustomerOrderController extends Controller 'phone' => $shipment->sender_phone, 'email' => $shipment->sender_email, 'address' => $shipment->sender_address, + 'state' => $shipment->sender_state, 'city' => $shipment->sender_city, + 'postal_code' => $shipment->sender_postal_code, + 'national_id' => $shipment->sender_national_id, ]; $data['receiver'] = [ @@ -947,7 +952,10 @@ class CustomerOrderController extends Controller 'phone' => $shipment->receiver_phone, 'email' => $shipment->receiver_email, 'address' => $shipment->receiver_address, + 'state' => $shipment->receiver_state, 'city' => $shipment->receiver_city, + 'postal_code' => $shipment->receiver_postal_code, + 'national_id' => $shipment->receiver_national_id, ]; $data['packages'] = $shipment->packages->map(fn($package) => [ @@ -1120,4 +1128,4 @@ class CustomerOrderController extends Controller return response()->json($result, 400); } -} \ No newline at end of file +} diff --git a/04_Laravel/app/Http/Controllers/Api/TrackController.php b/04_Laravel/app/Http/Controllers/Api/TrackController.php index ae48b18..7a94bfe 100644 --- a/04_Laravel/app/Http/Controllers/Api/TrackController.php +++ b/04_Laravel/app/Http/Controllers/Api/TrackController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Shipment; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; /** * API ترکینگ مرسولات — فاز ۰ @@ -12,13 +13,18 @@ use Illuminate\Http\JsonResponse; * GET /api/v1/track/{awb_no} * * خروجی: تایم‌لاین کامل رویدادهای ترکینگ + اطلاعات پایه مرسوله + نگاشت‌های شرکت حمل + * + * امنیت: نمایش لایه‌بندی‌شده (Tiered Display) + * - کاربر ناشناس: فقط وضعیت فعلی و موقعیت + * - کاربر لاگین‌شده (مالک): اطلاعات کامل فرستنده/گیرنده (مخفف‌شده) + * - ادمین: اطلاعات کامل */ class TrackController extends Controller { /** * دریافت تایم‌لاین ترکینگ یک مرسوله */ - public function show(string $awb_no): JsonResponse + public function show(string $awb_no, Request $request): JsonResponse { $shipment = Shipment::where('awb_no', $awb_no) ->with([ @@ -37,38 +43,89 @@ class TrackController extends Controller ], 404); } + // بررسی سطح دسترسی کاربر + $user = $request->user(); + $isOwner = $user && $shipment->user_id === $user->id; + $isAdmin = $user && $user->hasRole('super_admin'); + + // لایه ۱: اطلاعات عمومی (همیشه نمایش داده می‌شود) + $publicData = [ + 'shipment' => [ + 'awb_no' => $shipment->awb_no, + 'direction' => $shipment->direction?->value, + 'type' => $shipment->type?->value, + 'status' => $shipment->status?->value, + 'status_label' => $shipment->status?->label(), + 'from_country' => $shipment->fromCountry?->name, + 'to_country' => $shipment->toCountry?->name, + 'weight' => $shipment->weight, + 'created_at' => $shipment->created_at?->toIso8601String(), + ], + 'carrier_mappings' => $shipment->carrierMappings->map(fn ($mapping) => [ + 'carrier_code' => $mapping->carrier_code, + 'carrier_tracking_number' => $mapping->carrier_tracking_number, + 'booking_number' => $mapping->booking_number, + 'delivery_status' => $mapping->delivery_status, + 'notes' => $mapping->notes, + ]), + 'tracking_events' => $shipment->trackingEvents->map(fn ($event) => [ + 'date' => $event->event_date?->toDateString(), + 'time' => $event->event_time ? ($event->event_time instanceof \Carbon\Carbon ? $event->event_time->format('H:i') : $event->event_time) : null, + 'location' => $event->location, + 'description' => $event->event_description, + 'status' => $event->delivery_status, + 'source' => $event->source, + ]), + ]; + + // لایه ۲: اطلاعات مالک (فقط اگه کاربر لاگین کرده و مالک مرسوله باشه) + if ($isOwner || $isAdmin) { + $publicData['shipment']['sender_name'] = $shipment->sender_name; + $publicData['shipment']['receiver_name'] = $shipment->receiver_name; + $publicData['shipment']['sender_phone'] = $shipment->sender_phone; + $publicData['shipment']['receiver_phone'] = $shipment->receiver_phone; + $publicData['shipment']['sender_address'] = $shipment->sender_address; + $publicData['shipment']['receiver_address'] = $shipment->receiver_address; + } else { + // برای کاربر ناشناس، فقط نام مخفف‌شده رو نشون بده + $publicData['shipment']['sender_name'] = $this->maskName($shipment->sender_name); + $publicData['shipment']['receiver_name'] = $this->maskName($shipment->receiver_name); + $publicData['shipment']['sender_phone'] = null; + $publicData['shipment']['receiver_phone'] = null; + $publicData['shipment']['sender_address'] = null; + $publicData['shipment']['receiver_address'] = null; + $publicData['shipment']['is_anonymous'] = true; + } + return response()->json([ 'success' => true, - 'data' => [ - 'shipment' => [ - 'awb_no' => $shipment->awb_no, - 'direction' => $shipment->direction?->value, - 'type' => $shipment->type?->value, - 'status' => $shipment->status?->value, - 'status_label' => $shipment->status?->label(), - 'from_country' => $shipment->fromCountry?->name, - 'to_country' => $shipment->toCountry?->name, - 'sender_name' => $shipment->sender_name, - 'receiver_name' => $shipment->receiver_name, - 'weight' => $shipment->weight, - 'created_at' => $shipment->created_at?->toIso8601String(), - ], - 'carrier_mappings' => $shipment->carrierMappings->map(fn ($mapping) => [ - 'carrier_code' => $mapping->carrier_code, - 'carrier_tracking_number' => $mapping->carrier_tracking_number, - 'booking_number' => $mapping->booking_number, - 'delivery_status' => $mapping->delivery_status, - 'notes' => $mapping->notes, - ]), - 'tracking_events' => $shipment->trackingEvents->map(fn ($event) => [ - 'date' => $event->event_date?->toDateString(), - 'time' => $event->event_time ? ($event->event_time instanceof \Carbon\Carbon ? $event->event_time->format('H:i') : $event->event_time) : null, - 'location' => $event->location, - 'description' => $event->event_description, - 'status' => $event->delivery_status, - 'source' => $event->source, - ]), - ], + 'data' => $publicData, ]); } -} + + /** + * مخفی‌سازی بخشی از نام برای کاربران ناشناس + * مثال: "Akbar Salmanizadeh" → "A. Sal***zadeh" + */ + private function maskName(?string $name): ?string + { + if (!$name) { + return null; + } + + $parts = explode(' ', $name); + $first = substr($parts[0], 0, 1) . '.'; + + if (count($parts) > 1) { + $last = $parts[1]; + if (strlen($last) > 4) { + $last = substr($last, 0, 3) . '***' . substr($last, -3); + } else { + $last = substr($last, 0, 1) . '***'; + } + return $first . ' ' . $last; + } + + return $first; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Models/Shipment.php b/04_Laravel/app/Models/Shipment.php index 50e635e..3b686b5 100644 --- a/04_Laravel/app/Models/Shipment.php +++ b/04_Laravel/app/Models/Shipment.php @@ -57,6 +57,9 @@ class Shipment extends Model 'sender_city', 'sender_zip', 'sender_id_number', + 'sender_state', + 'sender_postal_code', + 'sender_national_id', // Receiver 'receiver_name', 'receiver_company', @@ -66,6 +69,9 @@ class Shipment extends Model 'receiver_city', 'receiver_zip', 'receiver_id_number', + 'receiver_state', + 'receiver_postal_code', + 'receiver_national_id', // Customs 'reason_for_export', 'content_description', diff --git a/04_Laravel/app/Services/PriceCalculatorService.php b/04_Laravel/app/Services/PriceCalculatorService.php index 4e9a3a4..d4246ae 100644 --- a/04_Laravel/app/Services/PriceCalculatorService.php +++ b/04_Laravel/app/Services/PriceCalculatorService.php @@ -263,17 +263,31 @@ class PriceCalculatorService ): float { $zoneColumn = 'zone_' . $zone; + // نرخی را پیدا کن که وزنش بزرگتر مساوی وزن واردشده باشد + // مثلاً اگه وزن 0.3 است، نرخ 0.5 را برمی‌گرداند $rate = \App\Models\ShippingRate::query() ->where('direction', $direction) ->where('type', $type->value) - ->where('weight', '<=', $weight) + ->where('weight', '>=', $weight) ->where($zoneColumn, '>', 0) - ->orderByDesc('weight') + ->orderBy('weight') // صعودی - اولین نرخ بزرگتر ->value($zoneColumn); + // اگه نرخی با وزن بزرگتر پیدا نشد، + // آخرین نرخ موجود (بزرگترین وزن) رو بگیر + // این برای مواقعی هست که وزن واردشده از بزرگترین وزن در جدول بیشتره + if ($rate === null) { + $rate = \App\Models\ShippingRate::query() + ->where('direction', $direction) + ->where('type', $type->value) + ->where($zoneColumn, '>', 0) + ->orderByDesc('weight') // نزولی - بزرگترین وزن + ->value($zoneColumn); + } + if ($rate === null) { throw new InvalidArgumentException( - 'No rate found for the given parameters.' + 'برای این مسیر و وزن، نرخی تعریف نشده است. لطفاً با پشتیبانی تماس بگیرید.' ); } diff --git a/04_Laravel/database/migrations/2026_10_27_033451_add_postal_and_national_id_to_shipments_table.php b/04_Laravel/database/migrations/2026_10_27_033451_add_postal_and_national_id_to_shipments_table.php new file mode 100644 index 0000000..53b39e8 --- /dev/null +++ b/04_Laravel/database/migrations/2026_10_27_033451_add_postal_and_national_id_to_shipments_table.php @@ -0,0 +1,39 @@ +string('sender_postal_code', 20)->nullable()->after('sender_state'); + $table->string('sender_national_id', 50)->nullable()->after('sender_postal_code'); + + // گیرنده + $table->string('receiver_postal_code', 20)->nullable()->after('receiver_state'); + $table->string('receiver_national_id', 50)->nullable()->after('receiver_postal_code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('shipments', function (Blueprint $table) { + $table->dropColumn([ + 'sender_postal_code', + 'sender_national_id', + 'receiver_postal_code', + 'receiver_national_id', + ]); + }); + } +}; \ No newline at end of file