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.
This commit is contained in:
Kazem Alghasi 2026-10-29 00:51:50 +03:30
parent 35fde95f5e
commit 75fd65be2b
7 changed files with 220 additions and 78 deletions

View File

@ -940,6 +940,12 @@ jQuery(document).ready(function($) {
receiver_phone: $('#ifnex-receiver-phone').val(), receiver_phone: $('#ifnex-receiver-phone').val(),
receiver_city: $('#ifnex-receiver-city').val(), receiver_city: $('#ifnex-receiver-city').val(),
receiver_address: $('#ifnex-receiver-address').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(),
}; };
} }

View File

@ -749,14 +749,20 @@ function ifnex_order_form_shortcode($atts) {
<div class="ifnex-form-grid"> <div class="ifnex-form-grid">
<div class="ifnex-field"><label for="ifnex-sender-name">نام و نام خانوادگی *</label><input type="text" id="ifnex-sender-name" name="ifnex_sender_name"></div> <div class="ifnex-field"><label for="ifnex-sender-name">نام و نام خانوادگی *</label><input type="text" id="ifnex-sender-name" name="ifnex_sender_name"></div>
<div class="ifnex-field"><label for="ifnex-sender-phone">شماره تماس *</label><input type="tel" id="ifnex-sender-phone" name="ifnex_sender_phone"></div> <div class="ifnex-field"><label for="ifnex-sender-phone">شماره تماس *</label><input type="tel" id="ifnex-sender-phone" name="ifnex_sender_phone"></div>
<div class="ifnex-field"><label for="ifnex-sender-city">شهر</label><input type="text" id="ifnex-sender-city" name="ifnex_sender_city"></div> <div class="ifnex-field"><label for="ifnex-sender-state">استان / ایالت *</label><input type="text" id="ifnex-sender-state" name="ifnex_sender_state"></div>
<div class="ifnex-field"><label for="ifnex-sender-city">شهر *</label><input type="text" id="ifnex-sender-city" name="ifnex_sender_city"></div>
<div class="ifnex-field"><label for="ifnex-sender-postal-code">کد پستی *</label><input type="text" id="ifnex-sender-postal-code" name="ifnex_sender_postal_code" pattern="[0-9]{5,15}" maxlength="15"></div>
<div class="ifnex-field"><label for="ifnex-sender-national-id">کد ملی / شماره گذرنامه *</label><input type="text" id="ifnex-sender-national-id" name="ifnex_sender_national_id"></div>
<div class="ifnex-field full"><label for="ifnex-sender-address">آدرس کامل *</label><textarea id="ifnex-sender-address" name="ifnex_sender_address" rows="2"></textarea></div> <div class="ifnex-field full"><label for="ifnex-sender-address">آدرس کامل *</label><textarea id="ifnex-sender-address" name="ifnex_sender_address" rows="2"></textarea></div>
</div> </div>
<h3 style="margin-top:24px;">📮 اطلاعات گیرنده</h3> <h3 style="margin-top:24px;">📮 اطلاعات گیرنده</h3>
<div class="ifnex-form-grid"> <div class="ifnex-form-grid">
<div class="ifnex-field"><label for="ifnex-receiver-name">نام و نام خانوادگی *</label><input type="text" id="ifnex-receiver-name" name="ifnex_receiver_name"></div> <div class="ifnex-field"><label for="ifnex-receiver-name">نام و نام خانوادگی *</label><input type="text" id="ifnex-receiver-name" name="ifnex_receiver_name"></div>
<div class="ifnex-field"><label for="ifnex-receiver-phone">شماره تماس *</label><input type="tel" id="ifnex-receiver-phone" name="ifnex_receiver_phone"></div> <div class="ifnex-field"><label for="ifnex-receiver-phone">شماره تماس *</label><input type="tel" id="ifnex-receiver-phone" name="ifnex_receiver_phone"></div>
<div class="ifnex-field"><label for="ifnex-receiver-city">شهر</label><input type="text" id="ifnex-receiver-city" name="ifnex_receiver_city"></div> <div class="ifnex-field"><label for="ifnex-receiver-state">استان / ایالت *</label><input type="text" id="ifnex-receiver-state" name="ifnex_receiver_state"></div>
<div class="ifnex-field"><label for="ifnex-receiver-city">شهر *</label><input type="text" id="ifnex-receiver-city" name="ifnex_receiver_city"></div>
<div class="ifnex-field"><label for="ifnex-receiver-postal-code">کد پستی *</label><input type="text" id="ifnex-receiver-postal-code" name="ifnex_receiver_postal_code" pattern="[0-9]{5,15}" maxlength="15"></div>
<div class="ifnex-field"><label for="ifnex-receiver-national-id">کد ملی / شماره گذرنامه *</label><input type="text" id="ifnex-receiver-national-id" name="ifnex_receiver_national_id"></div>
<div class="ifnex-field full"><label for="ifnex-receiver-address">آدرس کامل *</label><textarea id="ifnex-receiver-address" name="ifnex_receiver_address" rows="2"></textarea></div> <div class="ifnex-field full"><label for="ifnex-receiver-address">آدرس کامل *</label><textarea id="ifnex-receiver-address" name="ifnex_receiver_address" rows="2"></textarea></div>
</div> </div>
<div class="ifnex-form-nav"> <div class="ifnex-form-nav">
@ -1287,12 +1293,18 @@ function ifnex_order_detail_shortcode($atts) {
<p><?php echo esc_html($data['sender']['name'] ?? '—'); ?></p> <p><?php echo esc_html($data['sender']['name'] ?? '—'); ?></p>
<p>📞 <?php echo esc_html($data['sender']['phone'] ?? '—'); ?></p> <p>📞 <?php echo esc_html($data['sender']['phone'] ?? '—'); ?></p>
<p>📍 <?php echo esc_html($data['sender']['address'] ?? '—'); ?></p> <p>📍 <?php echo esc_html($data['sender']['address'] ?? '—'); ?></p>
<p>🏙️ <?php echo esc_html(($data['sender']['state'] ?? '') . ' / ' . ($data['sender']['city'] ?? '')); ?></p>
<p>📮 <?php echo esc_html($data['sender']['postal_code'] ?? '—'); ?></p>
<p>🪪 <?php echo esc_html($data['sender']['national_id'] ?? '—'); ?></p>
</div> </div>
<div class="ifnex-od-person"> <div class="ifnex-od-person">
<h4>📮 گیرنده</h4> <h4>📮 گیرنده</h4>
<p><?php echo esc_html($data['receiver']['name'] ?? '—'); ?></p> <p><?php echo esc_html($data['receiver']['name'] ?? '—'); ?></p>
<p>📞 <?php echo esc_html($data['receiver']['phone'] ?? '—'); ?></p> <p>📞 <?php echo esc_html($data['receiver']['phone'] ?? '—'); ?></p>
<p>📍 <?php echo esc_html($data['receiver']['address'] ?? '—'); ?></p> <p>📍 <?php echo esc_html($data['receiver']['address'] ?? '—'); ?></p>
<p>🏙️ <?php echo esc_html(($data['receiver']['state'] ?? '') . ' / ' . ($data['receiver']['city'] ?? '')); ?></p>
<p>📮 <?php echo esc_html($data['receiver']['postal_code'] ?? '—'); ?></p>
<p>🪪 <?php echo esc_html($data['receiver']['national_id'] ?? '—'); ?></p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -95,20 +95,22 @@ class CustomerOrderController extends Controller
'sender_phone' => ['required', 'string', 'max:50'], 'sender_phone' => ['required', 'string', 'max:50'],
'sender_email' => ['nullable', 'email', 'max:255'], 'sender_email' => ['nullable', 'email', 'max:255'],
'sender_address' => ['required', 'string', 'max:1000'], 'sender_address' => ['required', 'string', 'max:1000'],
'sender_state' => ['nullable', 'string', 'max:255'],
'sender_city' => ['nullable', 'string', 'max:255'], 'sender_city' => ['nullable', 'string', 'max:255'],
'sender_zip' => ['nullable', 'string', 'max:20'], 'sender_postal_code' => ['nullable', 'string', 'max:20'],
'sender_id_number' => ['nullable', 'string', 'max:50'], 'sender_national_id' => ['nullable', 'string', 'max:50'],
// اطلاعات گیرنده // اطلاعات گیرنده
'receiver_name' => ['required', 'string', 'max:255'], 'receiver_name' => ['required', 'string', 'max:255'],
'receiver_company' => ['nullable', 'string', 'max:255'], 'receiver_company' => ['nullable', 'string', 'max:255'],
'receiver_phone' => ['required', 'string', 'max:50'], 'receiver_phone' => ['required', 'string', 'max:50'],
'receiver_email' => ['nullable', 'email', 'max:255'], 'receiver_email' => ['nullable', 'email', 'max:255'],
'receiver_address' => ['required', 'string', 'max:1000'], 'receiver_address' => ['required', 'string', 'max:1000'],
'receiver_state' => ['nullable', 'string', 'max:255'],
'receiver_city' => ['nullable', 'string', 'max:255'], 'receiver_city' => ['nullable', 'string', 'max:255'],
'receiver_zip' => ['nullable', 'string', 'max:20'], 'receiver_postal_code' => ['nullable', 'string', 'max:20'],
'receiver_id_number' => ['nullable', 'string', 'max:50'], 'receiver_national_id' => ['nullable', 'string', 'max:50'],
// خدمات اضافی // خدمات اضافی
'extra_service' => ['nullable', 'numeric', 'min:0'], 'extra_service' => ['nullable', 'numeric', 'min:0'],
'packing_cost' => ['nullable', 'numeric', 'min:0'], 'packing_cost' => ['nullable', 'numeric', 'min:0'],
@ -245,19 +247,21 @@ class CustomerOrderController extends Controller
'sender_phone' => $validated['sender_phone'], 'sender_phone' => $validated['sender_phone'],
'sender_email' => $validated['sender_email'] ?? null, 'sender_email' => $validated['sender_email'] ?? null,
'sender_address' => $validated['sender_address'], 'sender_address' => $validated['sender_address'],
'sender_state' => $validated['sender_state'] ?? null,
'sender_city' => $validated['sender_city'] ?? null, 'sender_city' => $validated['sender_city'] ?? null,
'sender_zip' => $validated['sender_zip'] ?? null, 'sender_postal_code' => $validated['sender_postal_code'] ?? null,
'sender_id_number' => $validated['sender_id_number'] ?? null, 'sender_national_id' => $validated['sender_national_id'] ?? null,
// گیرنده // گیرنده
'receiver_name' => $validated['receiver_name'], 'receiver_name' => $validated['receiver_name'],
'receiver_company' => $validated['receiver_company'] ?? null, 'receiver_company' => $validated['receiver_company'] ?? null,
'receiver_phone' => $validated['receiver_phone'], 'receiver_phone' => $validated['receiver_phone'],
'receiver_email' => $validated['receiver_email'] ?? null, 'receiver_email' => $validated['receiver_email'] ?? null,
'receiver_address' => $validated['receiver_address'], 'receiver_address' => $validated['receiver_address'],
'receiver_state' => $validated['receiver_state'] ?? null,
'receiver_city' => $validated['receiver_city'] ?? null, 'receiver_city' => $validated['receiver_city'] ?? null,
'receiver_zip' => $validated['receiver_zip'] ?? null, 'receiver_postal_code' => $validated['receiver_postal_code'] ?? null,
'receiver_id_number' => $validated['receiver_id_number'] ?? null, 'receiver_national_id' => $validated['receiver_national_id'] ?? null,
]); ]);
// ذخیره بسته‌ها (Multi-Package) // ذخیره بسته‌ها (Multi-Package)
@ -406,9 +410,10 @@ class CustomerOrderController extends Controller
'sender_phone' => ['required', 'string', 'max:50'], 'sender_phone' => ['required', 'string', 'max:50'],
'sender_email' => ['nullable', 'email', 'max:255'], 'sender_email' => ['nullable', 'email', 'max:255'],
'sender_address' => ['required', 'string', 'max:1000'], 'sender_address' => ['required', 'string', 'max:1000'],
'sender_state' => ['nullable', 'string', 'max:255'],
'sender_city' => ['nullable', 'string', 'max:255'], 'sender_city' => ['nullable', 'string', 'max:255'],
'sender_zip' => ['nullable', 'string', 'max:20'], 'sender_postal_code' => ['nullable', 'string', 'max:20'],
'sender_id_number' => ['nullable', 'string', 'max:50'], 'sender_national_id' => ['nullable', 'string', 'max:50'],
// گیرنده // گیرنده
'receiver_name' => ['required', 'string', 'max:255'], 'receiver_name' => ['required', 'string', 'max:255'],
@ -416,9 +421,10 @@ class CustomerOrderController extends Controller
'receiver_phone' => ['required', 'string', 'max:50'], 'receiver_phone' => ['required', 'string', 'max:50'],
'receiver_email' => ['nullable', 'email', 'max:255'], 'receiver_email' => ['nullable', 'email', 'max:255'],
'receiver_address' => ['required', 'string', 'max:1000'], 'receiver_address' => ['required', 'string', 'max:1000'],
'receiver_state' => ['nullable', 'string', 'max:255'],
'receiver_city' => ['nullable', 'string', 'max:255'], 'receiver_city' => ['nullable', 'string', 'max:255'],
'receiver_zip' => ['nullable', 'string', 'max:20'], 'receiver_postal_code' => ['nullable', 'string', 'max:20'],
'receiver_id_number' => ['nullable', 'string', 'max:50'], 'receiver_national_id' => ['nullable', 'string', 'max:50'],
// خدمات // خدمات
'extra_service' => ['nullable', 'numeric', 'min:0'], 'extra_service' => ['nullable', 'numeric', 'min:0'],
@ -563,18 +569,20 @@ class CustomerOrderController extends Controller
'sender_phone' => $validated['sender_phone'], 'sender_phone' => $validated['sender_phone'],
'sender_email' => $validated['sender_email'] ?? null, 'sender_email' => $validated['sender_email'] ?? null,
'sender_address' => $validated['sender_address'], 'sender_address' => $validated['sender_address'],
'sender_state' => $validated['sender_state'] ?? null,
'sender_city' => $validated['sender_city'] ?? null, 'sender_city' => $validated['sender_city'] ?? null,
'sender_zip' => $validated['sender_zip'] ?? null, 'sender_postal_code' => $validated['sender_postal_code'] ?? null,
'sender_id_number' => $validated['sender_id_number'] ?? null, 'sender_national_id' => $validated['sender_national_id'] ?? null,
'receiver_name' => $validated['receiver_name'], 'receiver_name' => $validated['receiver_name'],
'receiver_company' => $validated['receiver_company'] ?? null, 'receiver_company' => $validated['receiver_company'] ?? null,
'receiver_phone' => $validated['receiver_phone'], 'receiver_phone' => $validated['receiver_phone'],
'receiver_email' => $validated['receiver_email'] ?? null, 'receiver_email' => $validated['receiver_email'] ?? null,
'receiver_address' => $validated['receiver_address'], 'receiver_address' => $validated['receiver_address'],
'receiver_state' => $validated['receiver_state'] ?? null,
'receiver_city' => $validated['receiver_city'] ?? null, 'receiver_city' => $validated['receiver_city'] ?? null,
'receiver_zip' => $validated['receiver_zip'] ?? null, 'receiver_postal_code' => $validated['receiver_postal_code'] ?? null,
'receiver_id_number' => $validated['receiver_id_number'] ?? null, 'receiver_national_id' => $validated['receiver_national_id'] ?? null,
]); ]);
$shipment->packages()->delete(); $shipment->packages()->delete();
@ -720,12 +728,6 @@ class CustomerOrderController extends Controller
$account_balance = $wallet_balance - $pending_payments_total; $account_balance = $wallet_balance - $pending_payments_total;
// بدهی‌های چندارزی (EUR/USD/AED/CNY) — به همان ارز خارجی، نه معادل ریالی // بدهی‌های چندارزی (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) $foreign_credits = app(\App\Services\CustomerCreditService::class)
->getCustomerDebtsByCurrency($user); ->getCustomerDebtsByCurrency($user);
@ -777,22 +779,22 @@ class CustomerOrderController extends Controller
]); ]);
} }
/** /**
* لیست کشورها برای فرم * لیست کشورها برای فرم
* GET /api/v1/customer/countries * GET /api/v1/customer/countries
*/ */
public function countries(): JsonResponse public function countries(): JsonResponse
{ {
$countries = Country::select('id', 'name', 'iso_code', 'export_zone_parcel', 'import_zone_parcel') $countries = Country::select('id', 'name', 'iso_code', 'export_zone_parcel', 'import_zone_parcel')
->where('is_active', true) ->where('is_active', true)
->orderBy('name') ->orderBy('name')
->get(); ->get();
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => $countries, 'data' => $countries,
]); ]);
} }
/** /**
* دریافت نوتیفیکیشن‌های کاربر * دریافت نوتیفیکیشن‌های کاربر
@ -938,7 +940,10 @@ class CustomerOrderController extends Controller
'phone' => $shipment->sender_phone, 'phone' => $shipment->sender_phone,
'email' => $shipment->sender_email, 'email' => $shipment->sender_email,
'address' => $shipment->sender_address, 'address' => $shipment->sender_address,
'state' => $shipment->sender_state,
'city' => $shipment->sender_city, 'city' => $shipment->sender_city,
'postal_code' => $shipment->sender_postal_code,
'national_id' => $shipment->sender_national_id,
]; ];
$data['receiver'] = [ $data['receiver'] = [
@ -947,7 +952,10 @@ class CustomerOrderController extends Controller
'phone' => $shipment->receiver_phone, 'phone' => $shipment->receiver_phone,
'email' => $shipment->receiver_email, 'email' => $shipment->receiver_email,
'address' => $shipment->receiver_address, 'address' => $shipment->receiver_address,
'state' => $shipment->receiver_state,
'city' => $shipment->receiver_city, 'city' => $shipment->receiver_city,
'postal_code' => $shipment->receiver_postal_code,
'national_id' => $shipment->receiver_national_id,
]; ];
$data['packages'] = $shipment->packages->map(fn($package) => [ $data['packages'] = $shipment->packages->map(fn($package) => [
@ -1120,4 +1128,4 @@ class CustomerOrderController extends Controller
return response()->json($result, 400); return response()->json($result, 400);
} }
} }

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Shipment; use App\Models\Shipment;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/** /**
* API ترکینگ مرسولات — فاز ۰ * API ترکینگ مرسولات — فاز ۰
@ -12,13 +13,18 @@ use Illuminate\Http\JsonResponse;
* GET /api/v1/track/{awb_no} * GET /api/v1/track/{awb_no}
* *
* خروجی: تایم‌لاین کامل رویدادهای ترکینگ + اطلاعات پایه مرسوله + نگاشت‌های شرکت حمل * خروجی: تایم‌لاین کامل رویدادهای ترکینگ + اطلاعات پایه مرسوله + نگاشت‌های شرکت حمل
*
* امنیت: نمایش لایه‌بندی‌شده (Tiered Display)
* - کاربر ناشناس: فقط وضعیت فعلی و موقعیت
* - کاربر لاگین‌شده (مالک): اطلاعات کامل فرستنده/گیرنده (مخفف‌شده)
* - ادمین: اطلاعات کامل
*/ */
class TrackController extends Controller 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) $shipment = Shipment::where('awb_no', $awb_no)
->with([ ->with([
@ -37,38 +43,89 @@ class TrackController extends Controller
], 404); ], 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([ return response()->json([
'success' => true, 'success' => true,
'data' => [ 'data' => $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,
'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,
]),
],
]); ]);
} }
}
/**
* مخفی‌سازی بخشی از نام برای کاربران ناشناس
* مثال: "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;
}
}

View File

@ -57,6 +57,9 @@ class Shipment extends Model
'sender_city', 'sender_city',
'sender_zip', 'sender_zip',
'sender_id_number', 'sender_id_number',
'sender_state',
'sender_postal_code',
'sender_national_id',
// Receiver // Receiver
'receiver_name', 'receiver_name',
'receiver_company', 'receiver_company',
@ -66,6 +69,9 @@ class Shipment extends Model
'receiver_city', 'receiver_city',
'receiver_zip', 'receiver_zip',
'receiver_id_number', 'receiver_id_number',
'receiver_state',
'receiver_postal_code',
'receiver_national_id',
// Customs // Customs
'reason_for_export', 'reason_for_export',
'content_description', 'content_description',

View File

@ -263,17 +263,31 @@ class PriceCalculatorService
): float { ): float {
$zoneColumn = 'zone_' . $zone; $zoneColumn = 'zone_' . $zone;
// نرخی را پیدا کن که وزنش بزرگتر مساوی وزن واردشده باشد
// مثلاً اگه وزن 0.3 است، نرخ 0.5 را برمی‌گرداند
$rate = \App\Models\ShippingRate::query() $rate = \App\Models\ShippingRate::query()
->where('direction', $direction) ->where('direction', $direction)
->where('type', $type->value) ->where('type', $type->value)
->where('weight', '<=', $weight) ->where('weight', '>=', $weight)
->where($zoneColumn, '>', 0) ->where($zoneColumn, '>', 0)
->orderByDesc('weight') ->orderBy('weight') // صعودی - اولین نرخ بزرگتر
->value($zoneColumn); ->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) { if ($rate === null) {
throw new InvalidArgumentException( throw new InvalidArgumentException(
'No rate found for the given parameters.' 'برای این مسیر و وزن، نرخی تعریف نشده است. لطفاً با پشتیبانی تماس بگیرید.'
); );
} }

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('shipments', function (Blueprint $table) {
// فرستنده
$table->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',
]);
});
}
};