From 4ee1d27d44509606d67624a62850433a140f5153 Mon Sep 17 00:00:00 2001
From: Kazem Alghasi
Date: Mon, 10 Aug 2026 06:09:56 +0330
Subject: [PATCH] feat: Complete customer ordering system (Phase 3)
Laravel Backend:
- Add AuthController for Sanctum token login/logout
- Add CustomerOrderController with 6 endpoints
(profile, countries, orders CRUD, cancel)
- Extend ShipmentStatus enum with pending_payment/cancelled
- Add SampleShippingRatesSeeder (10 zones, 3 service types)
- Zone-assign 232 countries (Gulf=1, Asia=2, EU=3, East=4, US=5)
- Add migration to extend shipments.status enum
- Add test-api.php automated test suite (5 tests)
WordPress Frontend:
- Add [ifnex_order_form] multi-step wizard (4 steps)
- Add [ifnex_orders_list] with status filter & cancel action
- Add [ifnex_user_profile] with wallet & stats
- Add ifnex-orders.css (RTL-ready, IFNEX theme)
- Add ifnex-order-form.js (step navigation + AJAX)
- Add AJAX handlers (countries, calculate, submit, cancel)
- Fix plugin asset paths with dirname(__FILE__)
---
.../ifnex-bridge/assets/css/ifnex-orders.css | 214 ++++++++
.../assets/js/ifnex-order-form.js | 200 +++++++
.../ifnex-bridge/includes/shortcodes.php | 416 +++++++++++++++
.../ifnex-bridge/includes/user-bridge.php | 245 ++++++++-
04_Laravel/app/Enums/ShipmentStatus.php | 83 ++-
.../Filament/Resources/ShipmentResource.php | 17 +
.../Http/Controllers/Api/AuthController.php | 68 +++
.../Api/Customer/CustomerOrderController.php | 492 ++++++++++++++++++
.../app/Http/Controllers/Controller.php | 5 +
...10_020410_update_shipments_status_enum.php | 47 ++
.../seeders/SampleShippingRatesSeeder.php | 214 ++++++++
04_Laravel/routes/api.php | 55 +-
04_Laravel/test-api.php | 136 +++++
13 files changed, 2171 insertions(+), 21 deletions(-)
create mode 100644 03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css
create mode 100644 03_WordPress/wp-content/plugins/ifnex-bridge/assets/js/ifnex-order-form.js
create mode 100644 04_Laravel/app/Http/Controllers/Api/AuthController.php
create mode 100644 04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php
create mode 100644 04_Laravel/database/migrations/2026_08_10_020410_update_shipments_status_enum.php
create mode 100644 04_Laravel/database/seeders/SampleShippingRatesSeeder.php
create mode 100644 04_Laravel/test-api.php
diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css
new file mode 100644
index 0000000..52af5ff
--- /dev/null
+++ b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css
@@ -0,0 +1,214 @@
+/* ═══════════════════════════════════════════
+ IFNEX Orders & Order Form Styles
+ ═══════════════════════════════════════════ */
+
+:root {
+ --ifnex-primary: #f59e0b;
+ --ifnex-primary-dark: #d97706;
+ --ifnex-dark: #1a1a2e;
+ --ifnex-gray: #6b7280;
+ --ifnex-light: #f9fafb;
+ --ifnex-border: #e5e7eb;
+ --ifnex-success: #10b981;
+ --ifnex-danger: #ef4444;
+ --ifnex-info: #3b82f6;
+ --ifnex-warning: #f59e0b;
+ --ifnex-radius: 12px;
+}
+
+/* ─── General Buttons ─── */
+.ifnex-btn {
+ display: inline-block;
+ padding: 10px 20px;
+ border-radius: 8px;
+ font-weight: 600;
+ text-decoration: none !important;
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s;
+ font-size: 14px;
+}
+.ifnex-btn:hover { opacity: 0.9; transform: translateY(-1px); }
+.ifnex-btn-primary { background: var(--ifnex-primary); color: #fff; }
+.ifnex-btn-success { background: var(--ifnex-success); color: #fff; }
+.ifnex-btn-danger { background: var(--ifnex-danger); color: #fff; }
+.ifnex-btn-info { background: var(--ifnex-info); color: #fff; }
+.ifnex-btn-sm { padding: 6px 12px; font-size: 12px; }
+
+/* ─── Badges ─── */
+.ifnex-badge {
+ display: inline-block;
+ padding: 4px 12px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 600;
+}
+.ifnex-badge-success { background: #d1fae5; color: #065f46; }
+.ifnex-badge-danger { background: #fee2e2; color: #991b1b; }
+.ifnex-badge-info { background: #dbeafe; color: #1e40af; }
+.ifnex-badge-warning { background: #fef3c7; color: #92400e; }
+.ifnex-badge-gray { background: #f3f4f6; color: #374151; }
+.ifnex-badge-primary { background: #fef3c7; color: #92400e; }
+.ifnex-badge-purple { background: #ede9fe; color: #5b21b6; }
+
+/* ═══ Orders List ═══ */
+.ifnex-orders-list { max-width: 1200px; margin: 0 auto; padding: 20px 0; }
+.ifnex-orders-header {
+ display: flex; justify-content: space-between; align-items: center;
+ margin-bottom: 20px; flex-wrap: wrap; gap: 12px;
+}
+.ifnex-orders-header h2 { margin: 0; color: var(--ifnex-dark); }
+
+.ifnex-filter-bar {
+ display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px;
+ padding: 12px; background: #fff; border-radius: var(--ifnex-radius);
+ border: 1px solid var(--ifnex-border);
+}
+.ifnex-filter-bar a {
+ padding: 6px 14px; border-radius: 999px; font-size: 13px;
+ text-decoration: none !important; color: var(--ifnex-gray);
+ border: 1px solid var(--ifnex-border); transition: all 0.2s;
+}
+.ifnex-filter-bar a:hover { border-color: var(--ifnex-primary); color: var(--ifnex-primary); }
+.ifnex-filter-bar a.active {
+ background: var(--ifnex-primary); color: #fff; border-color: var(--ifnex-primary);
+}
+
+.ifnex-orders-grid {
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
+ gap: 20px;
+}
+.ifnex-order-card {
+ background: #fff; border-radius: var(--ifnex-radius);
+ border: 1px solid var(--ifnex-border); padding: 20px;
+ transition: box-shadow 0.2s;
+}
+.ifnex-order-card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
+
+.ifnex-order-header {
+ display: flex; justify-content: space-between; align-items: center;
+ margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--ifnex-border);
+}
+.ifnex-awb code {
+ background: var(--ifnex-light); padding: 4px 8px; border-radius: 6px;
+ font-size: 13px; color: var(--ifnex-dark); font-weight: 700;
+}
+
+.ifnex-order-route {
+ display: flex; align-items: center; gap: 12px; margin-bottom: 16px;
+}
+.ifnex-route-from, .ifnex-route-to { flex: 1; }
+.ifnex-route-arrow { color: var(--ifnex-primary); font-size: 18px; }
+.ifnex-label { display: block; font-size: 11px; color: var(--ifnex-gray); margin-bottom: 2px; }
+.ifnex-country { font-weight: 600; color: var(--ifnex-dark); }
+
+.ifnex-order-details {
+ display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
+ margin-bottom: 16px; padding: 12px; background: var(--ifnex-light);
+ border-radius: 8px;
+}
+.ifnex-detail-item { font-size: 13px; }
+.ifnex-price { font-weight: 700; color: var(--ifnex-primary-dark); }
+
+.ifnex-order-actions { display: flex; gap: 8px; flex-wrap: wrap; }
+
+.ifnex-empty-state {
+ text-align: center; padding: 60px 20px; background: #fff;
+ border-radius: var(--ifnex-radius); border: 1px dashed var(--ifnex-border);
+}
+.ifnex-empty-state p { font-size: 18px; color: var(--ifnex-gray); margin-bottom: 20px; }
+
+.ifnex-pagination { display: flex; gap: 6px; justify-content: center; margin-top: 24px; }
+.ifnex-pagination a {
+ padding: 6px 12px; border-radius: 6px; text-decoration: none !important;
+ border: 1px solid var(--ifnex-border); color: var(--ifnex-gray); font-size: 13px;
+}
+.ifnex-pagination a.active { background: var(--ifnex-primary); color: #fff; border-color: var(--ifnex-primary); }
+
+/* ═══ Order Form (Multi-step) ═══ */
+.ifnex-order-form-container {
+ max-width: 900px; margin: 0 auto; padding: 20px 0;
+}
+
+.ifnex-steps {
+ display: flex; justify-content: space-between; margin-bottom: 32px;
+ position: relative;
+}
+.ifnex-steps::before {
+ content: ''; position: absolute; top: 20px; left: 10%; right: 10%;
+ height: 2px; background: var(--ifnex-border); z-index: 0;
+}
+.ifnex-step {
+ position: relative; z-index: 1; text-align: center; flex: 1;
+ font-size: 13px; color: var(--ifnex-gray);
+}
+.ifnex-step span {
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 40px; height: 40px; border-radius: 50%; background: #fff;
+ border: 2px solid var(--ifnex-border); margin-bottom: 6px;
+ font-weight: 700; transition: all 0.3s;
+}
+.ifnex-step.active span, .ifnex-step.done span {
+ background: var(--ifnex-primary); border-color: var(--ifnex-primary); color: #fff;
+}
+.ifnex-step.active { color: var(--ifnex-primary-dark); font-weight: 700; }
+
+.ifnex-form-step { display: none; }
+.ifnex-form-step.active { display: block; animation: ifnexFadeIn 0.3s; }
+@keyframes ifnexFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; } }
+
+.ifnex-form-card {
+ background: #fff; border-radius: var(--ifnex-radius);
+ border: 1px solid var(--ifnex-border); padding: 28px;
+}
+.ifnex-form-card h3 { margin-top: 0; color: var(--ifnex-dark); }
+
+.ifnex-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
+.ifnex-form-grid .full { grid-column: 1 / -1; }
+@media (max-width: 640px) { .ifnex-form-grid { grid-template-columns: 1fr; } }
+
+.ifnex-field label {
+ display: block; font-size: 13px; font-weight: 600;
+ color: var(--ifnex-dark); margin-bottom: 6px;
+}
+.ifnex-field input, .ifnex-field select, .ifnex-field textarea {
+ width: 100%; padding: 10px 12px; border: 1px solid var(--ifnex-border);
+ border-radius: 8px; font-size: 14px; background: #fff;
+ transition: border-color 0.2s;
+}
+.ifnex-field input:focus, .ifnex-field select:focus, .ifnex-field textarea:focus {
+ outline: none; border-color: var(--ifnex-primary);
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15);
+}
+
+.ifnex-form-nav {
+ display: flex; justify-content: space-between; margin-top: 24px; gap: 12px;
+}
+
+/* ─── Price Summary ─── */
+.ifnex-price-summary {
+ background: var(--ifnex-light); border-radius: 8px; padding: 20px;
+}
+.ifnex-price-row {
+ display: flex; justify-content: space-between; padding: 8px 0;
+ font-size: 14px; border-bottom: 1px dashed var(--ifnex-border);
+}
+.ifnex-price-row.total {
+ border-bottom: none; font-size: 18px; font-weight: 800;
+ color: var(--ifnex-primary-dark); padding-top: 12px;
+}
+
+.ifnex-error-box {
+ background: #fee2e2; color: #991b1b; padding: 12px 16px;
+ border-radius: 8px; margin-bottom: 16px; font-size: 14px;
+}
+.ifnex-success-box {
+ background: #d1fae5; color: #065f46; padding: 20px;
+ border-radius: 8px; text-align: center; font-size: 16px;
+}
+
+.ifnex-login-prompt {
+ max-width: 500px; margin: 40px auto; text-align: center;
+ background: #fff; padding: 40px; border-radius: var(--ifnex-radius);
+ border: 1px solid var(--ifnex-border);
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..824c429
--- /dev/null
+++ b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/js/ifnex-order-form.js
@@ -0,0 +1,200 @@
+jQuery(document).ready(function($) {
+ 'use strict';
+
+ var currentStep = 1;
+ var countries = [];
+ var priceData = null;
+
+ // ─── Load Countries ───
+ function loadCountries() {
+ $.ajax({
+ url: ifnex_ajax.ajax_url,
+ method: 'POST',
+ data: { action: 'ifnex_get_countries', nonce: ifnex_ajax.nonce },
+ success: function(res) {
+ if (res.success) {
+ countries = res.data;
+ populateCountrySelects();
+ }
+ }
+ });
+ }
+
+ function populateCountrySelects() {
+ var $from = $('#ifnex-from-country');
+ var $to = $('#ifnex-to-country');
+ $from.empty().append('');
+ $to.empty().append('');
+ $.each(countries, function(i, c) {
+ var opt = '';
+ $from.append(opt);
+ $to.append(opt);
+ });
+ }
+
+ // ─── Step Navigation ───
+ function goToStep(step) {
+ currentStep = step;
+ $('.ifnex-form-step').removeClass('active');
+ $('.ifnex-form-step[data-step="' + step + '"]').addClass('active');
+ $('.ifnex-step').each(function() {
+ var s = parseInt($(this).data('step'));
+ $(this).toggleClass('active', s === step);
+ $(this).toggleClass('done', s < step);
+ });
+ $('.ifnex-order-form-container')[0].scrollIntoView({ behavior: 'smooth' });
+ }
+
+ $('.ifnex-next-step').on('click', function() {
+ var next = currentStep + 1;
+ if (currentStep === 1 && !validateStep1()) return;
+ if (currentStep === 2 && !validateStep2()) return;
+ if (currentStep === 3) { calculatePrice(); return; }
+ goToStep(next);
+ });
+
+ $('.ifnex-prev-step').on('click', function() {
+ goToStep(currentStep - 1);
+ });
+
+ function validateStep1() {
+ var ok = true;
+ ['#ifnex-direction', '#ifnex-type', '#ifnex-from-country', '#ifnex-to-country', '#ifnex-weight'].forEach(function(sel) {
+ if (!$(sel).val()) { $(sel).css('border-color', '#ef4444'); ok = false; }
+ else { $(sel).css('border-color', ''); }
+ });
+ if (!ok) showFormError('لطفاً همه فیلدهای مرحله ۱ را تکمیل کنید.');
+ return ok;
+ }
+
+ function validateStep2() {
+ var ok = true;
+ ['#ifnex-sender-name', '#ifnex-sender-phone', '#ifnex-sender-address',
+ '#ifnex-receiver-name', '#ifnex-receiver-phone', '#ifnex-receiver-address'].forEach(function(sel) {
+ if (!$(sel).val()) { $(sel).css('border-color', '#ef4444'); ok = false; }
+ else { $(sel).css('border-color', ''); }
+ });
+ if (!ok) showFormError('لطفاً اطلاعات فرستنده و گیرنده را کامل کنید.');
+ return ok;
+ }
+
+ // ─── Calculate Price (Step 3 → 4) ───
+ function calculatePrice() {
+ $('#ifnex-calc-loading').show();
+ $('#ifnex-calc-error').hide();
+
+ var data = buildOrderData();
+
+ $.ajax({
+ url: ifnex_ajax.ajax_url,
+ method: 'POST',
+ data: { action: 'ifnex_calculate_price', nonce: ifnex_ajax.nonce, data: data },
+ success: function(res) {
+ $('#ifnex-calc-loading').hide();
+ if (res.success) {
+ priceData = res.data;
+ renderPriceSummary(res.data);
+ goToStep(4);
+ } else {
+ showCalcError(res.data || 'خطا در محاسبه قیمت');
+ }
+ },
+ error: function() {
+ $('#ifnex-calc-loading').hide();
+ showCalcError('خطا در ارتباط با سرور');
+ }
+ });
+ }
+
+ function buildOrderData() {
+ var direction = $('#ifnex-direction').val();
+ var toIso = $('#ifnex-to-country option:selected').data('iso');
+ var fromIso = $('#ifnex-from-country option:selected').data('iso');
+ return {
+ direction: direction,
+ type: $('#ifnex-type').val(),
+ from_country_id: parseInt($('#ifnex-from-country').val()),
+ to_country_id: parseInt($('#ifnex-to-country').val()),
+ country_iso: (direction === 'export') ? toIso : fromIso,
+ weight: parseFloat($('#ifnex-weight').val()) || 0,
+ volumetric_weight: parseFloat($('#ifnex-volumetric-weight').val()) || 0,
+ sender_name: $('#ifnex-sender-name').val(),
+ sender_phone: $('#ifnex-sender-phone').val(),
+ sender_address: $('#ifnex-sender-address').val(),
+ sender_city: $('#ifnex-sender-city').val(),
+ receiver_name: $('#ifnex-receiver-name').val(),
+ receiver_phone: $('#ifnex-receiver-phone').val(),
+ receiver_address: $('#ifnex-receiver-address').val(),
+ receiver_city: $('#ifnex-receiver-city').val(),
+ discount_code: $('#ifnex-discount-code').val() || null
+ };
+ }
+
+ function renderPriceSummary(p) {
+ var html = '';
+ html += priceRow('قیمت پایه (درهم)', p.base_price);
+ html += priceRow('خالص (ریال)', p.net_rial);
+ html += priceRow('بستهبندی', p.packing_cost);
+ if (p.discount_amount > 0) html += priceRow('تخفیف', -p.discount_amount);
+ html += priceRow('مالیات (VAT)', p.vat_amount);
+ html += 'مبلغ قابل پرداخت' + formatNum(p.total_fee) + ' ریال
';
+ $('#ifnex-price-summary').html(html);
+ }
+
+ function priceRow(label, val) {
+ return '' + label + '' + formatNum(val) + '
';
+ }
+
+ function formatNum(n) {
+ return Number(n).toLocaleString('fa-IR');
+ }
+
+ function showFormError(msg) {
+ $('#ifnex-form-error').text(msg).show();
+ setTimeout(function() { $('#ifnex-form-error').fadeOut(); }, 4000);
+ }
+ function showCalcError(msg) {
+ $('#ifnex-calc-error').text(msg).show();
+ }
+
+ // ─── Submit Order (Step 4) ───
+ $('#ifnex-submit-order').on('click', function() {
+ var btn = $(this);
+ btn.prop('disabled', true).text('در حال ثبت سفارش...');
+ $('#ifnex-submit-error').hide();
+
+ var data = buildOrderData();
+
+ $.ajax({
+ url: ifnex_ajax.ajax_url,
+ method: 'POST',
+ data: { action: 'ifnex_submit_order', nonce: ifnex_ajax.nonce, data: data },
+ success: function(res) {
+ btn.prop('disabled', false).text('✅ تأیید و ثبت سفارش');
+ if (res.success) {
+ var d = res.data.data;
+ $('#ifnex-order-form').html(
+ '' +
+ '
🎉 سفارش شما با موفقیت ثبت شد!
' +
+ '
شماره پیگیری: ' + d.awb_no + '
' +
+ '
مبلغ قابل پرداخت: ' + formatNum(d.total_fee) + ' ریال
' +
+ '
وضعیت: ' + d.status.label + '
' +
+ '
مشاهده سفارشات من' +
+ '
'
+ );
+ } else {
+ $('#ifnex-submit-error').text(res.data || 'خطا در ثبت سفارش').show();
+ }
+ },
+ error: function() {
+ btn.prop('disabled', false).text('✅ تأیید و ثبت سفارش');
+ $('#ifnex-submit-error').text('خطا در ارتباط با سرور').show();
+ }
+ });
+ });
+
+ // ─── Init ──
+ if ($('#ifnex-order-form').length) {
+ loadCountries();
+ }
+});
\ No newline at end of file
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 09ca608..38ed335 100644
--- a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php
+++ b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php
@@ -189,3 +189,419 @@ function ifnex_transactions_shortcode($atts) {
برای مشاهده پروفایل، باید وارد شوید.
';
+ }
+
+ $atts = shortcode_atts(array(
+ 'show_stats' => 'yes',
+ 'show_wallet' => 'yes',
+ 'show_recent_orders' => 'yes',
+ ), $atts);
+
+ $bridge = new IFNEX_User_Bridge();
+ $user_id = get_current_user_id();
+
+ $profile = $bridge->get_customer_profile($user_id);
+
+ if (is_wp_error($profile)) {
+ return '' . esc_html($profile->get_error_message()) . '
';
+ }
+
+ ob_start();
+ ?>
+
+
+
+
+
+
+
+
💰 کیف پول
+
+
+ ریال
+
+
+ مسدود
+
+ فعال
+
+
+
+
+
+
+
+
+
📊 آمار سفارشات
+
+ 0): ?>
+
+ مجموع خرید:
+ ریال
+
+
+
+
+
+
+
+
+ برای مشاهده سفارشات، باید وارد شوید.';
+ }
+
+ $atts = shortcode_atts(array(
+ 'per_page' => 10,
+ 'status' => '',
+ ), $atts);
+
+ $bridge = new IFNEX_User_Bridge();
+ $user_id = get_current_user_id();
+
+ $result = $bridge->get_customer_orders($user_id, intval($atts['per_page']), $atts['status']);
+
+ if (is_wp_error($result)) {
+ return '' . esc_html($result->get_error_message()) . '
';
+ }
+
+ $orders = $result['data'] ?? [];
+ $pagination = $result['pagination'] ?? [];
+
+ ob_start();
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ از:
+
+
+
+
+
➜
+
+ به:
+
+
+
+
+
+
+
+
+ وزن:
+ kg
+
+
+ مبلغ:
+
+ ریال
+
+
+
+ تاریخ:
+
+
+
+
+
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
+
+
+
🔐 ورود لازم است
+
برای ثبت سفارش، ابتدا باید وارد حساب کاربری خود شوید.
+
ورود به حساب
+
+
+
+ api_url . '/sanctum/token';
+ //$url = $this->api_url . '/sanctum/token';
+ $url = $this->api_url . '/auth/login';
+
$args = array(
'headers' => array(
'Content-Type' => 'application/json',
@@ -218,4 +220,245 @@ class IFNEX_User_Bridge {
return $data;
}
+
+ // ══════════════════════════════════════════════════════════════
+ // Customer Orders API (جدید)
+ // ══════════════════════════════════════════════════════════════
+
+ /**
+ * دریافت پروفایل و آمار کاربر
+ */
+ public function get_customer_profile($user_id) {
+ $result = $this->authenticated_request($user_id, '/customer/profile');
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ return $result;
+ }
+
+ /**
+ * دریافت لیست کشورها
+ */
+ public function get_countries($user_id) {
+ $result = $this->authenticated_request($user_id, '/customer/countries');
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ return $result['data'] ?? [];
+ }
+
+ /**
+ * دریافت لیست سفارشات کاربر
+ */
+ public function get_customer_orders($user_id, $per_page = 15, $status = null) {
+ $params = ['per_page' => $per_page];
+
+ if ($status) {
+ $params['status'] = $status;
+ }
+
+ $result = $this->authenticated_request($user_id, '/customer/orders', 'GET', $params);
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ return $result;
+ }
+
+ /**
+ * دریافت جزئیات یک سفارش
+ */
+ public function get_customer_order($user_id, $shipment_id) {
+ $result = $this->authenticated_request($user_id, "/customer/orders/{$shipment_id}");
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ return $result;
+ }
+
+ /**
+ * ثبت سفارش جدید
+ */
+ public function create_customer_order($user_id, $order_data) {
+ $result = $this->authenticated_request($user_id, '/customer/orders', 'POST', $order_data);
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ return $result;
+ }
+
+ /**
+ * لغو سفارش
+ */
+ public function cancel_customer_order($user_id, $shipment_id) {
+ $result = $this->authenticated_request($user_id, "/customer/orders/{$shipment_id}/cancel", 'POST');
+
+ if (is_wp_error($result)) {
+ return $result;
+ }
+
+ return $result;
+ }
}
+
+// ══════════════════════════════════════════════════════════════
+// AJAX Handler برای لغو سفارش
+// ══════════════════════════════════════════════════════════════
+
+add_action('wp_ajax_ifnex_cancel_order', 'ifnex_cancel_order_ajax');
+
+function ifnex_cancel_order_ajax() {
+ check_ajax_referer('ifnex_ajax_nonce', 'nonce');
+
+ if (!is_user_logged_in()) {
+ wp_send_json_error('باید وارد شوید.');
+ }
+
+ $order_id = intval($_POST['order_id'] ?? 0);
+ if (!$order_id) {
+ wp_send_json_error('شناسه سفارش نامعتبر است.');
+ }
+
+ $bridge = new IFNEX_User_Bridge();
+ $result = $bridge->cancel_customer_order(get_current_user_id(), $order_id);
+
+ if (is_wp_error($result)) {
+ wp_send_json_error($result->get_error_message());
+ }
+
+ wp_send_json_success('سفارش لغو شد.');
+}
+
+// Enqueue AJAX script
+add_action('wp_enqueue_scripts', 'ifnex_enqueue_ajax_script');
+
+function ifnex_enqueue_ajax_script() {
+ global $post;
+
+ if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'ifnex_orders_list')) {
+ wp_enqueue_script('jquery');
+ wp_localize_script('jquery', 'ifnex_ajax', array(
+ 'ajax_url' => admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce('ifnex_ajax_nonce'),
+ ));
+ }
+}
+
+// ══════════════════════════════════════════════════════════════
+// AJAX Handlers برای فرم سفارش
+// ══════════════════════════════════════════════════════════════
+
+add_action('wp_ajax_ifnex_get_countries', 'ifnex_get_countries_ajax');
+function ifnex_get_countries_ajax() {
+ check_ajax_referer('ifnex_ajax_nonce', 'nonce');
+ if (!is_user_logged_in()) wp_send_json_error('باید وارد شوید.');
+
+ $bridge = new IFNEX_User_Bridge();
+ $result = $bridge->get_countries(get_current_user_id());
+
+ if (is_wp_error($result)) wp_send_json_error($result->get_error_message());
+ wp_send_json_success($result);
+}
+
+add_action('wp_ajax_ifnex_calculate_price', 'ifnex_calculate_price_ajax');
+function ifnex_calculate_price_ajax() {
+ check_ajax_referer('ifnex_ajax_nonce', 'nonce');
+ if (!is_user_logged_in()) wp_send_json_error('باید وارد شوید.');
+
+ $data = $_POST['data'] ?? [];
+ if (empty($data)) wp_send_json_error('دادهای ارسال نشده.');
+
+ // تبدیل direction برای PriceCalculator
+ $payload = array(
+ 'direction' => ($data['direction'] === 'export') ? 'Outbound' : 'Inbound',
+ 'type' => $data['type'],
+ 'country_iso' => $data['country_iso'],
+ 'weight' => floatval($data['weight']),
+ 'volumetric_weight' => floatval($data['volumetric_weight'] ?? 0),
+ );
+ if (!empty($data['discount_code'])) $payload['discount_code'] = $data['discount_code'];
+
+ $api_url = rtrim(get_option('ifnex_api_url', 'http://localhost:8000/api/v1'), '/');
+ $response = wp_remote_post($api_url . '/calculate', array(
+ 'headers' => array('Content-Type' => 'application/json', 'Accept' => 'application/json'),
+ 'body' => json_encode($payload),
+ 'timeout' => 30,
+ ));
+
+ if (is_wp_error($response)) wp_send_json_error($response->get_error_message());
+
+ $body = json_decode(wp_remote_retrieve_body($response), true);
+ if ($response['response']['code'] !== 200) {
+ wp_send_json_error($body['message'] ?? 'خطا در محاسبه قیمت');
+ }
+ wp_send_json_success($body);
+}
+
+add_action('wp_ajax_ifnex_submit_order', 'ifnex_submit_order_ajax');
+function ifnex_submit_order_ajax() {
+ check_ajax_referer('ifnex_ajax_nonce', 'nonce');
+ if (!is_user_logged_in()) wp_send_json_error('باید وارد شوید.');
+
+ $data = $_POST['data'] ?? [];
+ if (empty($data)) wp_send_json_error('دادهای ارسال نشده.');
+
+ $bridge = new IFNEX_User_Bridge();
+ $result = $bridge->create_customer_order(get_current_user_id(), $data);
+
+ if (is_wp_error($result)) wp_send_json_error($result->get_error_message());
+ if (isset($result['success']) && !$result['success']) {
+ wp_send_json_error($result['message'] ?? 'خطا در ثبت سفارش');
+ }
+ wp_send_json_success($result);
+}
+
+// Enqueue scripts & styles برای شورتکدهای جدید
+add_action('wp_enqueue_scripts', 'ifnex_enqueue_order_assets');
+function ifnex_enqueue_order_assets() {
+ global $post;
+ if (!is_a($post, 'WP_Post')) return;
+
+ $has_order_shortcodes =
+ has_shortcode($post->post_content, 'ifnex_orders_list') ||
+ has_shortcode($post->post_content, 'ifnex_order_form') ||
+ has_shortcode($post->post_content, 'ifnex_user_profile');
+
+ if ($has_order_shortcodes) {
+ // ✅ اصلاح: dirname(__FILE__) یک سطح بالاتر میرود
+ $plugin_url = plugin_dir_url(dirname(__FILE__));
+
+ // Load CSS
+ wp_enqueue_style(
+ 'ifnex-orders-css',
+ $plugin_url . 'assets/css/ifnex-orders.css',
+ array(),
+ '1.2.0' //版本号 changed برای شکستن cache
+ );
+
+ // Load jQuery & JS
+ wp_enqueue_script('jquery');
+ wp_enqueue_script(
+ 'ifnex-order-form-js',
+ $plugin_url . 'assets/js/ifnex-order-form.js',
+ array('jquery'),
+ '1.2.0', //版本号 changed
+ true
+ );
+
+ // Pass data to JS
+ wp_localize_script('ifnex-order-form-js', 'ifnex_ajax', array(
+ 'ajax_url' => admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce('ifnex_ajax_nonce'),
+ 'orders_url' => home_url('/my-orders/'),
+ ));
+ }
+}
\ No newline at end of file
diff --git a/04_Laravel/app/Enums/ShipmentStatus.php b/04_Laravel/app/Enums/ShipmentStatus.php
index 76a837d..a567aa9 100644
--- a/04_Laravel/app/Enums/ShipmentStatus.php
+++ b/04_Laravel/app/Enums/ShipmentStatus.php
@@ -4,11 +4,78 @@ namespace App\Enums;
enum ShipmentStatus: string
{
- case Processed = 'processed';
- case PickedUp = 'picked_up';
- case InTransit = 'in_transit';
- case OutForDelivery = 'out_for_delivery';
- case Failed = 'failed';
- case Delivered = 'delivered';
- case Returned = 'returned';
-}
+ // ─── وضعیتهای مشتری (قبل از پرداخت) ───
+ case PendingPayment = 'pending_payment'; // در انتظار پرداخت
+ case Cancelled = 'cancelled'; // لغو شده توسط مشتری
+
+ // ─── وضعیتهای عملیاتی (بعد از پرداخت) ───
+ case Processed = 'processed'; // پردازش شده (ادمین تأیید کرده)
+ case PickedUp = 'picked_up'; // تحویل گرفته شده
+ case InTransit = 'in_transit'; // در حال حمل
+ case OutForDelivery = 'out_for_delivery'; // در مسیر تحویل
+ case Delivered = 'delivered'; // تحویل داده شده
+ case Failed = 'failed'; // ناموفق
+ case Returned = 'returned'; // برگشتی
+
+ /**
+ * لیبل فارسی
+ */
+ public function label(): string
+ {
+ return match($this) {
+ self::PendingPayment => 'در انتظار پرداخت',
+ self::Cancelled => 'لغو شده',
+ self::Processed => 'پردازش شده',
+ self::PickedUp => 'تحویل گرفته شده',
+ self::InTransit => 'در حال حمل',
+ self::OutForDelivery => 'در مسیر تحویل',
+ self::Delivered => 'تحویل داده شده',
+ self::Failed => 'ناموفق',
+ self::Returned => 'برگشتی',
+ };
+ }
+
+ /**
+ * رنگ badge در Filament
+ */
+ public function color(): string
+ {
+ return match($this) {
+ self::PendingPayment => 'warning',
+ self::Cancelled => 'gray',
+ self::Processed => 'info',
+ self::PickedUp => 'info',
+ self::InTransit => 'primary',
+ self::OutForDelivery => 'purple',
+ self::Delivered => 'success',
+ self::Failed => 'danger',
+ self::Returned => 'warning',
+ };
+ }
+
+ /**
+ * آیا این وضعیت به معنای "پرداخت شده" است؟
+ */
+ public function isPaid(): bool
+ {
+ return in_array($this, [
+ self::Processed,
+ self::PickedUp,
+ self::InTransit,
+ self::OutForDelivery,
+ self::Delivered,
+ self::Failed,
+ self::Returned,
+ ]);
+ }
+
+ /**
+ * آیا این وضعیت قابل لغو توسط مشتری است؟
+ */
+ public function canBeCancelledByCustomer(): bool
+ {
+ return in_array($this, [
+ self::PendingPayment,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/04_Laravel/app/Filament/Resources/ShipmentResource.php b/04_Laravel/app/Filament/Resources/ShipmentResource.php
index 47820b0..fa15419 100644
--- a/04_Laravel/app/Filament/Resources/ShipmentResource.php
+++ b/04_Laravel/app/Filament/Resources/ShipmentResource.php
@@ -45,6 +45,21 @@ class ShipmentResource extends Resource
'PARCEL' => 'Parcel',
])
->required(),
+
+ Forms\Components\Select::make('status')
+ ->options([
+ 'pending_payment' => 'در انتظار پرداخت',
+ 'cancelled' => 'لغو شده',
+ 'processed' => 'Processed',
+ 'picked_up' => 'Picked Up',
+ 'in_transit' => 'In Transit',
+ 'out_for_delivery' => 'Out For Delivery',
+ 'failed' => 'Failed',
+ 'delivered' => 'Delivered',
+ 'returned' => 'Returned',
+ ])
+ ->default('processed')
+ ->required(),
Forms\Components\Select::make('status')
->options([
'processed' => 'Processed',
@@ -301,6 +316,8 @@ class ShipmentResource extends Resource
->filters([
Tables\Filters\SelectFilter::make('status')
->options([
+ 'pending_payment' => 'در انتظار پرداخت',
+ 'cancelled' => 'لغو شده',
'processed' => 'Processed',
'picked_up' => 'Picked Up',
'in_transit' => 'In Transit',
diff --git a/04_Laravel/app/Http/Controllers/Api/AuthController.php b/04_Laravel/app/Http/Controllers/Api/AuthController.php
new file mode 100644
index 0000000..23816ab
--- /dev/null
+++ b/04_Laravel/app/Http/Controllers/Api/AuthController.php
@@ -0,0 +1,68 @@
+validate([
+ 'email' => ['required', 'email'],
+ 'password' => ['required', 'string'],
+ 'token_name' => ['nullable', 'string', 'max:100'],
+ ]);
+
+ $user = User::where('email', $validated['email'])->first();
+
+ if (!$user || !Hash::check($validated['password'], $user->password)) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'ایمیل یا رمز عبور اشتباه است.',
+ ], 401);
+ }
+
+ // بررسی فعال بودن کاربر
+ if (isset($user->is_active) && !$user->is_active) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'حساب کاربری شما غیرفعال است.',
+ ], 403);
+ }
+
+ $tokenName = $validated['token_name'] ?? 'api-token';
+ $token = $user->createToken($tokenName)->plainTextToken;
+
+ return response()->json([
+ 'success' => true,
+ 'token' => $token,
+ 'user' => [
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
+ ],
+ ]);
+ }
+
+ /**
+ * خروج و حذف توکن فعلی
+ * POST /api/v1/auth/logout
+ */
+ public function logout(Request $request)
+ {
+ $request->user()->currentAccessToken()->delete();
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'خروج با موفقیت انجام شد.',
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php
new file mode 100644
index 0000000..ff7b7f8
--- /dev/null
+++ b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php
@@ -0,0 +1,492 @@
+validate([
+ 'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
+ 'status' => ['nullable', 'string'],
+ 'page' => ['nullable', 'integer', 'min:1'],
+ ]);
+
+ $user = Auth::user();
+ $query = Shipment::query()
+ ->where('user_id', $user->id)
+ ->with(['fromCountry', 'toCountry', 'items'])
+ ->orderBy('created_at', 'desc');
+
+ if (!empty($validated['status'])) {
+ $query->where('status', $validated['status']);
+ }
+
+ $shipments = $query->paginate($validated['per_page'] ?? 15);
+
+ return response()->json([
+ 'success' => true,
+ 'data' => $shipments->map(function ($shipment) {
+ return $this->formatShipment($shipment);
+ }),
+ 'pagination' => [
+ 'current_page' => $shipments->currentPage(),
+ 'last_page' => $shipments->lastPage(),
+ 'per_page' => $shipments->perPage(),
+ 'total' => $shipments->total(),
+ ],
+ ]);
+ }
+
+ /**
+ * ثبت سفارش جدید
+ * POST /api/v1/customer/orders
+ */
+ public function store(Request $request): JsonResponse
+ {
+ try {
+ $validated = $request->validate([
+ // مسیر و نوع
+ 'direction' => ['required', 'in:export,import'],
+ 'type' => ['required', 'in:DOC_NORMAL,DOC_ECONOMY,PARCEL'],
+ 'from_country_id' => ['required', 'exists:countries,id'],
+ 'to_country_id' => ['required', 'exists:countries,id'],
+
+ // وزن و ابعاد
+ 'weight' => ['required', 'numeric', 'min:0.1'],
+ 'volumetric_weight' => ['nullable', 'numeric', 'min:0'],
+ 'dimensions' => ['nullable', 'string', 'max:100'],
+
+ // اطلاعات فرستنده
+ 'sender_name' => ['required', 'string', 'max:255'],
+ 'sender_company' => ['nullable', 'string', 'max:255'],
+ 'sender_phone' => ['required', 'string', 'max:50'],
+ 'sender_email' => ['nullable', 'email', 'max:255'],
+ 'sender_address' => ['required', 'string', 'max:1000'],
+ 'sender_city' => ['nullable', 'string', 'max:255'],
+ 'sender_zip' => ['nullable', 'string', 'max:20'],
+ 'sender_id_number' => ['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_city' => ['nullable', 'string', 'max:255'],
+ 'receiver_zip' => ['nullable', 'string', 'max:20'],
+ 'receiver_id_number' => ['nullable', 'string', 'max:50'],
+
+ // خدمات اضافی
+ 'extra_service' => ['nullable', 'numeric', 'min:0'],
+ 'packing_cost' => ['nullable', 'numeric', 'min:0'],
+ 'domestic_pickup' => ['nullable', 'numeric', 'min:0'],
+ 'domestic_delivery' => ['nullable', 'numeric', 'min:0'],
+ 'warehousing_cost' => ['nullable', 'numeric', 'min:0'],
+
+ // تخفیف
+ 'discount_code' => ['nullable', 'string', 'max:50'],
+
+ // اقلام گمرکی
+ 'items' => ['nullable', 'array', 'max:9'],
+ 'items.*.description' => ['required_with:items', 'string', 'max:500'],
+ 'items.*.hs_code' => ['required_with:items', 'string', 'max:20'],
+ 'items.*.quantity' => ['required_with:items', 'integer', 'min:1'],
+ 'items.*.unit_price' => ['required_with:items', 'numeric', 'min:0'],
+ ]);
+ } catch (ValidationException $e) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'خطا در اعتبارسنجی اطلاعات',
+ 'errors' => $e->errors(),
+ ], 422);
+ }
+
+ $user = Auth::user();
+
+ // بررسی اینکه کاربر کیف پول فعال دارد
+ if (!$user->wallet || $user->wallet->is_frozen) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'کیف پول شما غیرفعال است. لطفاً با پشتیبانی تماس بگیرید.',
+ ], 403);
+ }
+
+ // دریافت کشور مقصد برای محاسبه قیمت
+ $destinationCountry = Country::findOrFail(
+ $validated['direction'] === 'export'
+ ? $validated['to_country_id']
+ : $validated['from_country_id']
+ );
+
+ // محاسبه قیمت
+ try {
+ $pricingData = [
+ 'direction' => $validated['direction'] === 'export' ? 'Outbound' : 'Inbound',
+ 'type' => $validated['type'],
+ 'country_iso' => $destinationCountry->iso_code,
+ 'weight' => (float) $validated['weight'],
+ 'volumetric_weight' => (float) ($validated['volumetric_weight'] ?? 0),
+ 'extra_service' => (float) ($validated['extra_service'] ?? 0),
+ 'packing_cost' => (float) ($validated['packing_cost'] ?? 0),
+ 'domestic_pickup' => (float) ($validated['domestic_pickup'] ?? 0),
+ 'domestic_delivery' => (float) ($validated['domestic_delivery'] ?? 0),
+ 'warehousing_cost' => (float) ($validated['warehousing_cost'] ?? 0),
+ 'discount_code' => $validated['discount_code'] ?? null,
+ ];
+
+ $priceResult = $this->calculator->calculate($pricingData);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'خطا در محاسبه قیمت: ' . $e->getMessage(),
+ ], 400);
+ }
+
+ // ساخت شماره AWB (فرمت: IFN-YYYY-XXXXX)
+ $awbNo = $this->generateAwbNumber();
+
+ // ذخیره سفارش
+ try {
+ $shipment = DB::transaction(function () use ($validated, $user, $priceResult, $awbNo) {
+ $chargeableWeight = max(
+ (float) $validated['weight'],
+ (float) ($validated['volumetric_weight'] ?? 0)
+ );
+
+ $shipment = Shipment::create([
+ 'user_id' => $user->id,
+ 'awb_no' => $awbNo,
+ 'direction' => $validated['direction'],
+ 'type' => $validated['type'],
+ 'status' => ShipmentStatus::PendingPayment,
+
+ // وزن
+ 'weight' => $validated['weight'],
+ 'volumetric_weight' => $validated['volumetric_weight'] ?? null,
+ 'chargeable_weight' => $chargeableWeight,
+ 'dimensions' => $validated['dimensions'] ?? null,
+
+ // کشورها
+ 'from_country_id' => $validated['from_country_id'],
+ 'to_country_id' => $validated['to_country_id'],
+
+ // اطلاعات مالی (از محاسبه)
+ 'shipping_price' => $priceResult['net_dirham'],
+ 'extra_service' => $priceResult['extra_service'],
+ 'packing_cost' => $priceResult['packing_cost'],
+ 'domestic_pickup' => $priceResult['domestic_pickup'],
+ 'domestic_delivery' => $priceResult['domestic_delivery'],
+ 'warehousing_cost' => $priceResult['warehousing_cost'],
+ 'vat_amount' => $priceResult['vat_amount'],
+ 'discount' => $priceResult['discount_amount'],
+ 'total_fee' => $priceResult['total_fee'],
+ 'net_dirham' => $priceResult['net_dirham'],
+ 'net_rial' => $priceResult['net_rial'],
+
+ // فرستنده
+ 'sender_name' => $validated['sender_name'],
+ 'sender_company' => $validated['sender_company'] ?? null,
+ 'sender_phone' => $validated['sender_phone'],
+ 'sender_email' => $validated['sender_email'] ?? null,
+ 'sender_address' => $validated['sender_address'],
+ 'sender_city' => $validated['sender_city'] ?? null,
+ 'sender_zip' => $validated['sender_zip'] ?? null,
+ 'sender_id_number' => $validated['sender_id_number'] ?? 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_city' => $validated['receiver_city'] ?? null,
+ 'receiver_zip' => $validated['receiver_zip'] ?? null,
+ 'receiver_id_number' => $validated['receiver_id_number'] ?? null,
+ ]);
+
+ // ذخیره اقلام گمرکی
+ if (!empty($validated['items'])) {
+ foreach ($validated['items'] as $index => $item) {
+ ShipmentItem::create([
+ 'shipment_id' => $shipment->id,
+ 'row_number' => $index + 1,
+ 'description' => $item['description'],
+ 'hs_code' => $item['hs_code'],
+ 'quantity' => $item['quantity'],
+ 'unit_price' => $item['unit_price'],
+ 'total_usd' => $item['quantity'] * $item['unit_price'],
+ ]);
+ }
+ }
+
+ return $shipment;
+ });
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'سفارش شما با موفقیت ثبت شد. لطفاً برای تکمیل، پرداخت را انجام دهید.',
+ 'data' => $this->formatShipment($shipment->load(['fromCountry', 'toCountry', 'items'])),
+ 'pricing' => $priceResult,
+ ], 201);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'خطا در ثبت سفارش: ' . $e->getMessage(),
+ ], 500);
+ }
+ }
+
+ /**
+ * جزئیات یک سفارش
+ * GET /api/v1/customer/orders/{shipment}
+ */
+ public function show(Shipment $shipment): JsonResponse
+ {
+ $user = Auth::user();
+
+ // بررسی مالکیت
+ if ($shipment->user_id !== $user->id) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'شما به این سفارش دسترسی ندارید.',
+ ], 403);
+ }
+
+ $shipment->load(['fromCountry', 'toCountry', 'items', 'trackingEvents']);
+
+ return response()->json([
+ 'success' => true,
+ 'data' => $this->formatShipment($shipment, detailed: true),
+ ]);
+ }
+
+ /**
+ * لغو سفارش (فقط در وضعیت pending_payment)
+ * POST /api/v1/customer/orders/{shipment}/cancel
+ */
+ public function cancel(Shipment $shipment): JsonResponse
+ {
+ $user = Auth::user();
+
+ if ($shipment->user_id !== $user->id) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'شما به این سفارش دسترسی ندارید.',
+ ], 403);
+ }
+
+ if (!$shipment->status->canBeCancelledByCustomer()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'این سفارش در وضعیتی نیست که بتوان آن را لغو کرد.',
+ ], 400);
+ }
+
+ $shipment->update(['status' => ShipmentStatus::Cancelled]);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'سفارش با موفقیت لغو شد.',
+ 'data' => $this->formatShipment($shipment->fresh()),
+ ]);
+ }
+
+ /**
+ * پروفایل و آمار کاربر
+ * GET /api/v1/customer/profile
+ */
+ public function profile(Request $request): JsonResponse
+ {
+ $user = Auth::user();
+ $user->load(['wallet']);
+
+ // آمار سفارشات
+ $stats = [
+ 'total_orders' => Shipment::where('user_id', $user->id)->count(),
+ 'pending_orders' => Shipment::where('user_id', $user->id)
+ ->where('status', ShipmentStatus::PendingPayment)
+ ->count(),
+ 'active_orders' => Shipment::where('user_id', $user->id)
+ ->whereIn('status', [
+ ShipmentStatus::Processed,
+ ShipmentStatus::PickedUp,
+ ShipmentStatus::InTransit,
+ ShipmentStatus::OutForDelivery,
+ ])
+ ->count(),
+ 'delivered_orders' => Shipment::where('user_id', $user->id)
+ ->where('status', ShipmentStatus::Delivered)
+ ->count(),
+ 'total_spent' => Shipment::where('user_id', $user->id)
+ ->where('status', '!=', ShipmentStatus::PendingPayment)
+ ->where('status', '!=', ShipmentStatus::Cancelled)
+ ->sum('total_fee'),
+ ];
+
+ return response()->json([
+ 'success' => true,
+ 'user' => [
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
+ 'phone' => $user->phone,
+ 'member_since' => $user->created_at->format('Y-m-d'),
+ ],
+ 'wallet' => $user->wallet ? [
+ 'balance' => $user->wallet->balance,
+ 'is_frozen' => $user->wallet->is_frozen,
+ ] : null,
+ 'stats' => $stats,
+ ]);
+ }
+
+ /**
+ * لیست کشورها برای فرم
+ * 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,
+ ]);
+ }
+
+ /**
+ * فرمت کردن Shipment برای خروجی API
+ */
+ private function formatShipment(Shipment $shipment, bool $detailed = false): array
+ {
+ $data = [
+ 'id' => $shipment->id,
+ 'awb_no' => $shipment->awb_no,
+ 'direction' => $shipment->direction?->value,
+ 'type' => $shipment->type?->value,
+ 'status' => [
+ 'value' => $shipment->status?->value,
+ 'label' => $shipment->status?->label(),
+ 'color' => $shipment->status?->color(),
+ 'is_paid' => $shipment->status?->isPaid(),
+ 'can_cancel' => $shipment->status?->canBeCancelledByCustomer(),
+ ],
+ 'from_country' => $shipment->fromCountry ? [
+ 'id' => $shipment->fromCountry->id,
+ 'name' => $shipment->fromCountry->name,
+ 'iso_code' => $shipment->fromCountry->iso_code,
+ ] : null,
+ 'to_country' => $shipment->toCountry ? [
+ 'id' => $shipment->toCountry->id,
+ 'name' => $shipment->toCountry->name,
+ 'iso_code' => $shipment->toCountry->iso_code,
+ ] : null,
+ 'weight' => $shipment->weight,
+ 'chargeable_weight' => $shipment->chargeable_weight,
+ 'total_fee' => $shipment->total_fee,
+ 'net_rial' => $shipment->net_rial,
+ 'net_dirham' => $shipment->net_dirham,
+ 'created_at' => $shipment->created_at->toIso8601String(),
+ 'created_at_jalali' => $this->toJalali($shipment->created_at),
+ ];
+
+ if ($detailed) {
+ $data['sender'] = [
+ 'name' => $shipment->sender_name,
+ 'company' => $shipment->sender_company,
+ 'phone' => $shipment->sender_phone,
+ 'email' => $shipment->sender_email,
+ 'address' => $shipment->sender_address,
+ 'city' => $shipment->sender_city,
+ ];
+ $data['receiver'] = [
+ 'name' => $shipment->receiver_name,
+ 'company' => $shipment->receiver_company,
+ 'phone' => $shipment->receiver_phone,
+ 'email' => $shipment->receiver_email,
+ 'address' => $shipment->receiver_address,
+ 'city' => $shipment->receiver_city,
+ ];
+ $data['financial'] = [
+ 'shipping_price' => $shipment->shipping_price,
+ 'extra_service' => $shipment->extra_service,
+ 'packing_cost' => $shipment->packing_cost,
+ 'domestic_pickup' => $shipment->domestic_pickup,
+ 'domestic_delivery' => $shipment->domestic_delivery,
+ 'warehousing_cost' => $shipment->warehousing_cost,
+ 'vat_amount' => $shipment->vat_amount,
+ 'discount' => $shipment->discount,
+ 'total_fee' => $shipment->total_fee,
+ ];
+ $data['items'] = $shipment->items->map(fn($item) => [
+ 'row' => $item->row_number,
+ 'description' => $item->description,
+ 'hs_code' => $item->hs_code,
+ 'quantity' => $item->quantity,
+ 'unit_price' => $item->unit_price,
+ 'total' => $item->total_usd,
+ ]);
+ $data['tracking_events'] = $shipment->trackingEvents->map(fn($event) => [
+ 'date' => $event->event_date,
+ 'description' => $event->event_description,
+ 'location' => $event->location,
+ ]);
+ }
+
+ return $data;
+ }
+
+ /**
+ * تولید شماره AWB
+ */
+ private function generateAwbNumber(): string
+ {
+ do {
+ $number = 'IFN-' . date('Y') . '-' . str_pad(random_int(10000, 99999), 5, '0', STR_PAD_LEFT);
+ } while (Shipment::where('awb_no', $number)->exists());
+
+ return $number;
+ }
+
+ /**
+ * تبدیل تاریخ به شمسی
+ */
+ private function toJalali($date): string
+ {
+ if (!$date) return '—';
+
+ try {
+ return \Morilog\Jalali\Jalalian::fromCarbon($date)->format('Y/m/d H:i');
+ } catch (\Exception $e) {
+ return $date->format('Y-m-d H:i');
+ }
+ }
+}
\ No newline at end of file
diff --git a/04_Laravel/app/Http/Controllers/Controller.php b/04_Laravel/app/Http/Controllers/Controller.php
index 8677cd5..62b4969 100644
--- a/04_Laravel/app/Http/Controllers/Controller.php
+++ b/04_Laravel/app/Http/Controllers/Controller.php
@@ -6,3 +6,8 @@ abstract class Controller
{
//
}
+
+class CustomerOrderController
+{
+ // ...
+}
\ No newline at end of file
diff --git a/04_Laravel/database/migrations/2026_08_10_020410_update_shipments_status_enum.php b/04_Laravel/database/migrations/2026_08_10_020410_update_shipments_status_enum.php
new file mode 100644
index 0000000..cff78fa
--- /dev/null
+++ b/04_Laravel/database/migrations/2026_08_10_020410_update_shipments_status_enum.php
@@ -0,0 +1,47 @@
+whereIn('status', ['pending_payment', 'cancelled'])
+ ->update(['status' => 'processed']);
+
+ DB::statement("
+ ALTER TABLE `shipments`
+ MODIFY COLUMN `status` ENUM(
+ 'processed',
+ 'picked_up',
+ 'in_transit',
+ 'out_for_delivery',
+ 'delivered',
+ 'failed',
+ 'returned'
+ ) DEFAULT 'processed'
+ ");
+ }
+};
\ No newline at end of file
diff --git a/04_Laravel/database/seeders/SampleShippingRatesSeeder.php b/04_Laravel/database/seeders/SampleShippingRatesSeeder.php
new file mode 100644
index 0000000..d76c115
--- /dev/null
+++ b/04_Laravel/database/seeders/SampleShippingRatesSeeder.php
@@ -0,0 +1,214 @@
+ str_starts_with($col, 'zone_'));
+ $zoneCount = count($zoneColumns);
+
+ $this->command->info("📊 Found {$zoneCount} zones in shipping_rates table");
+ $this->command->info(" Zones: " . implode(', ', $zoneColumns));
+ $this->command->info('');
+
+ // ─── نرخ پایه برای هر zone (PARCEL Export) ──────────
+ // قیمتها به درهم برای وزنهای مختلف
+ // Zone 1: نزدیکترین (Gulf)
+ // Zone 2-5: متوسط تا دور
+ // Zone 6+: خیلی دور (با ۲۰٪ افزایش نسبت به zone قبلی)
+
+ $baseParcelRates = [
+ 0.5 => [25, 35, 50, 65, 85],
+ 1.0 => [40, 55, 80, 100, 130],
+ 2.0 => [65, 90, 130, 165, 210],
+ 5.0 => [120, 170, 240, 310, 400],
+ 10.0 => [200, 290, 410, 530, 680],
+ 20.0 => [350, 510, 720, 930, 1200],
+ 30.0 => [480, 700, 990, 1280, 1650],
+ ];
+
+ $this->command->info('📦 Creating Export Parcel rates...');
+ foreach ($baseParcelRates as $weight => $zones) {
+ $rateData = [
+ 'direction' => 'export',
+ 'type' => 'PARCEL',
+ 'weight' => $weight,
+ ];
+
+ // اضافه کردن zoneهای موجود
+ for ($i = 1; $i <= $zoneCount; $i++) {
+ if (isset($zones[$i - 1])) {
+ $rateData["zone_{$i}"] = $zones[$i - 1];
+ } else {
+ // Zone بیشتر از ۵ = افزایش ۲۰٪ نسبت به قبلی
+ $prevZone = $rateData["zone_" . ($i - 1)] ?? end($zones);
+ $rateData["zone_{$i}"] = round($prevZone * 1.2);
+ }
+ }
+
+ ShippingRate::firstOrCreate(
+ [
+ 'direction' => 'export',
+ 'type' => 'PARCEL',
+ 'weight' => $weight,
+ ],
+ $rateData
+ );
+ }
+ $this->command->info(' ✅ ' . count($baseParcelRates) . ' Parcel rates created');
+
+ // ─── DOC_NORMAL Export ────────────────────────
+ $baseDocNormalRates = [
+ 0.5 => [30, 42, 60, 78, 100],
+ 1.0 => [50, 70, 100, 130, 165],
+ 2.0 => [80, 110, 160, 205, 260],
+ 5.0 => [150, 210, 300, 390, 500],
+ 10.0 => [250, 360, 510, 660, 850],
+ ];
+
+ $this->command->info('📄 Creating Export DOC_NORMAL rates...');
+ foreach ($baseDocNormalRates as $weight => $zones) {
+ $rateData = [
+ 'direction' => 'export',
+ 'type' => 'DOC_NORMAL',
+ 'weight' => $weight,
+ ];
+
+ for ($i = 1; $i <= $zoneCount; $i++) {
+ if (isset($zones[$i - 1])) {
+ $rateData["zone_{$i}"] = $zones[$i - 1];
+ } else {
+ $prevZone = $rateData["zone_" . ($i - 1)] ?? end($zones);
+ $rateData["zone_{$i}"] = round($prevZone * 1.2);
+ }
+ }
+
+ ShippingRate::firstOrCreate(
+ [
+ 'direction' => 'export',
+ 'type' => 'DOC_NORMAL',
+ 'weight' => $weight,
+ ],
+ $rateData
+ );
+ }
+ $this->command->info(' ✅ ' . count($baseDocNormalRates) . ' DOC_NORMAL rates created');
+
+ // ─── DOC_ECONOMY Export ───────────────────────
+ $baseDocEconomyRates = [
+ 0.5 => [20, 28, 40, 52, 67],
+ 1.0 => [35, 48, 68, 88, 112],
+ 2.0 => [55, 76, 108, 140, 178],
+ 5.0 => [105, 145, 208, 270, 345],
+ ];
+
+ $this->command->info('📃 Creating Export DOC_ECONOMY rates...');
+ foreach ($baseDocEconomyRates as $weight => $zones) {
+ $rateData = [
+ 'direction' => 'export',
+ 'type' => 'DOC_ECONOMY',
+ 'weight' => $weight,
+ ];
+
+ for ($i = 1; $i <= $zoneCount; $i++) {
+ if (isset($zones[$i - 1])) {
+ $rateData["zone_{$i}"] = $zones[$i - 1];
+ } else {
+ $prevZone = $rateData["zone_" . ($i - 1)] ?? end($zones);
+ $rateData["zone_{$i}"] = round($prevZone * 1.2);
+ }
+ }
+
+ ShippingRate::firstOrCreate(
+ [
+ 'direction' => 'export',
+ 'type' => 'DOC_ECONOMY',
+ 'weight' => $weight,
+ ],
+ $rateData
+ );
+ }
+ $this->command->info(' ✅ ' . count($baseDocEconomyRates) . ' DOC_ECONOMY rates created');
+
+ // ─── Update Country Zones ─────────────────────
+ $this->command->info('');
+ $this->command->info('🌍 Updating country zones...');
+
+ // امارات و کشورهای حاشیه خلیج فارس = Zone 1
+ $zone1IsoCodes = ['AE', 'QA', 'KW', 'BH', 'OM', 'SA', 'IQ', 'TR'];
+ Country::whereIn('iso_code', $zone1IsoCodes)->update([
+ 'export_zone_parcel' => 1,
+ 'export_zone_doc' => 1,
+ 'import_zone_parcel' => 1,
+ 'import_zone_doc' => 1,
+ ]);
+ $this->command->info(' ✅ Zone 1 (Gulf): ' . count($zone1IsoCodes) . ' countries');
+
+ // خاورمیانه و آسیای مرکزی = Zone 2
+ $zone2IsoCodes = ['PK', 'IN', 'AF', 'AZ', 'AM', 'GE', 'KZ', 'UZ', 'TM', 'TJ', 'KG'];
+ Country::whereIn('iso_code', $zone2IsoCodes)->update([
+ 'export_zone_parcel' => 2,
+ 'export_zone_doc' => 2,
+ 'import_zone_parcel' => 2,
+ 'import_zone_doc' => 2,
+ ]);
+ $this->command->info(' ✅ Zone 2 (Central Asia): ' . count($zone2IsoCodes) . ' countries');
+
+ // اروپا = Zone 3
+ $europeanCountries = Country::whereIn('iso_code', [
+ 'DE', 'FR', 'IT', 'ES', 'GB', 'NL', 'BE', 'AT', 'CH', 'SE', 'NO', 'DK',
+ 'FI', 'IE', 'PT', 'GR', 'PL', 'CZ', 'HU', 'RO', 'BG', 'HR', 'SI', 'SK',
+ 'EE', 'LV', 'LT'
+ ])->update([
+ 'export_zone_parcel' => 3,
+ 'export_zone_doc' => 3,
+ 'import_zone_parcel' => 3,
+ 'import_zone_doc' => 3,
+ ]);
+ $this->command->info(" ✅ Zone 3 (Europe): {$europeanCountries} countries");
+
+ // شرق آسیا = Zone 4
+ $zone4IsoCodes = ['CN', 'JP', 'KR', 'SG', 'MY', 'TH', 'VN', 'PH', 'ID', 'TW', 'HK'];
+ Country::whereIn('iso_code', $zone4IsoCodes)->update([
+ 'export_zone_parcel' => 4,
+ 'export_zone_doc' => 4,
+ 'import_zone_parcel' => 4,
+ 'import_zone_doc' => 4,
+ ]);
+ $this->command->info(' ✅ Zone 4 (East Asia): ' . count($zone4IsoCodes) . ' countries');
+
+ // آمریکا = Zone 5
+ $zone5IsoCodes = ['US', 'CA', 'MX', 'BR', 'AR', 'CL', 'AU', 'NZ'];
+ Country::whereIn('iso_code', $zone5IsoCodes)->update([
+ 'export_zone_parcel' => 5,
+ 'export_zone_doc' => 5,
+ 'import_zone_parcel' => 5,
+ 'import_zone_doc' => 5,
+ ]);
+ $this->command->info(' ✅ Zone 5 (Americas/Oceania): ' . count($zone5IsoCodes) . ' countries');
+
+ // کشورهای باقیمانده = Zone 6 (اگر وجود دارد) یا Zone 5
+ $defaultZone = $zoneCount >= 6 ? 6 : ($zoneCount >= 5 ? 5 : min($zoneCount, 3));
+ $updated = Country::whereNull('export_zone_parcel')
+ ->orWhere('export_zone_parcel', 0)
+ ->update([
+ 'export_zone_parcel' => $defaultZone,
+ 'export_zone_doc' => $defaultZone,
+ 'import_zone_parcel' => $defaultZone,
+ 'import_zone_doc' => $defaultZone,
+ ]);
+ $this->command->info(" ✅ Remaining → Zone {$defaultZone}: {$updated} countries");
+
+ $this->command->info('');
+ $this->command->info('🎉 All shipping rates and zones seeded successfully!');
+ }
+}
\ No newline at end of file
diff --git a/04_Laravel/routes/api.php b/04_Laravel/routes/api.php
index 782ddb2..02ced1a 100644
--- a/04_Laravel/routes/api.php
+++ b/04_Laravel/routes/api.php
@@ -1,39 +1,68 @@
prefix('v1')->group(function () {
Route::get('/track/{awb_no}', [TrackController::class, 'show']);
});
-// APIهای احراز هویت شده (نیاز به Sanctum یا Bearer Token)
+// ══════════════════════════════════════════════════════════════
+// Auth API (عمومی - برای دریافت توکن)
+// ══════════════════════════════════════════════════════════════
+Route::post('/v1/auth/login', [AuthController::class, 'login']);
+
+Route::middleware(['auth:sanctum'])->post('/v1/auth/logout', [AuthController::class, 'logout']);
+
+// ══════════════════════════════════════════════════════════════
+// API برای کاربران لاگینشده (با Sanctum / Bearer Token)
+// ══════════════════════════════════════════════════════════════
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
-
- // Wallet APIها (کاربر عادی)
+
+ // ─── Wallet API (کاربر عادی) ───
Route::get('/wallet/balance', [WalletController::class, 'balance']);
Route::get('/wallet/transactions', [WalletController::class, 'transactions']);
-
- // Payment APIها
+
+ // ─── Payment API ───
Route::post('/payment/redirect', [PaymentController::class, 'redirectToGateway']);
Route::get('/payment/check/{transaction}', [PaymentController::class, 'checkStatus']);
-
- // Admin Wallet APIها
+
+ // ─── Admin Wallet API ───
Route::post('/wallet/admin-adjust', [WalletController::class, 'adminAdjust']);
Route::post('/wallet/{wallet}/freeze', [WalletController::class, 'freeze']);
Route::post('/wallet/{wallet}/unfreeze', [WalletController::class, 'unfreeze']);
Route::get('/wallet/{wallet}/activity-log', [WalletController::class, 'activityLog']);
+
+ // ─── Customer Orders API (جدید) ───
+ Route::prefix('customer')->group(function () {
+ // پروفایل و آمار
+ Route::get('/profile', [CustomerOrderController::class, 'profile']);
+
+ // لیست کشورها
+ Route::get('/countries', [CustomerOrderController::class, 'countries']);
+
+ // سفارشات
+ Route::get('/orders', [CustomerOrderController::class, 'index']);
+ Route::post('/orders', [CustomerOrderController::class, 'store']);
+ Route::get('/orders/{shipment}', [CustomerOrderController::class, 'show']);
+ Route::post('/orders/{shipment}/cancel', [CustomerOrderController::class, 'cancel']);
+ });
});
+
+// ══════════════════════════════════════════════════════════════
// Mock Gateway Routes (عمومی - برای شبیهسازی درگاه)
+// ══════════════════════════════════════════════════════════════
Route::prefix('v1/payment')->group(function () {
Route::get('/mock-gateway', [MockGatewayController::class, 'showGateway']);
Route::get('/mock-gateway/success', [MockGatewayController::class, 'simulateSuccess']);
@@ -44,7 +73,9 @@ Route::prefix('v1/payment')->group(function () {
Route::any('/v1/payment/callback', [PaymentController::class, 'callback'])
->name('payment.callback');
+// ══════════════════════════════════════════════════════════════
// APIهای عمومی (بدون auth)
+// ══════════════════════════════════════════════════════════════
Route::post('/v1/calculate', [PricingController::class, 'calculate']);
Route::get('/v1/discount-codes', [DiscountCodeController::class, 'index']);
Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']);
\ No newline at end of file
diff --git a/04_Laravel/test-api.php b/04_Laravel/test-api.php
new file mode 100644
index 0000000..5199d11
--- /dev/null
+++ b/04_Laravel/test-api.php
@@ -0,0 +1,136 @@
+make(Illuminate\Contracts\Console\Kernel::class);
+$kernel->bootstrap();
+
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Hash;
+use App\Models\User;
+
+echo "══════════════════════════════════════════\n";
+echo " 🧪 IFNEX API Test Suite\n";
+echo "══════════════════════════════════════════\n\n";
+
+// ─── 0. تنظیم رمز عبور برای تست ─────────────────────────────
+$testEmail = 'kazem@vernasoft.group';
+$testPassword = 'Test@12345';
+
+$user = User::where('email', $testEmail)->first();
+if (!$user) {
+ echo "❌ کاربر {$testEmail} پیدا نشد!\n";
+ exit(1);
+}
+
+$user->password = Hash::make($testPassword);
+$user->save();
+echo "✅ رمز کاربر {$user->name} به '{$testPassword}' تغییر یافت\n\n";
+
+// ─── 1. تست Login ──────────────────────────────────────────
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+echo "تست ۱: دریافت توکن (Login)\n";
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+
+$response = Http::post('http://localhost:8000/api/v1/auth/login', [
+ 'email' => $testEmail,
+ 'password' => $testPassword,
+ 'token_name' => 'test-api',
+]);
+
+echo "Status: " . $response->status() . "\n";
+$data = $response->json();
+print_r($data);
+
+if ($response->status() !== 200 || !isset($data['token'])) {
+ echo "\n❌ Login ناموفق بود! بقیه تستها را نمیتوان انجام داد.\n";
+ exit(1);
+}
+
+$token = $data['token'];
+echo "\n✅ Login موفق! توکن دریافت شد.\n\n";
+
+// ─── 2. تست Profile ────────────────────────────────────────
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+echo "تست ۲: دریافت پروفایل\n";
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+
+$response2 = Http::withToken($token)
+ ->get('http://localhost:8000/api/v1/customer/profile');
+
+echo "Status: " . $response2->status() . "\n";
+print_r($response2->json());
+echo "\n";
+
+// ─── 3. تست Countries ──────────────────────────────────────
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+echo "تست ۳: لیست کشورها\n";
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+
+$response3 = Http::withToken($token)
+ ->get('http://localhost:8000/api/v1/customer/countries');
+
+echo "Status: " . $response3->status() . "\n";
+$countries = $response3->json();
+echo "تعداد کشورها: " . (isset($countries['data']) ? count($countries['data']) : 0) . "\n";
+if (isset($countries['data']) && count($countries['data']) > 0) {
+ echo "اولین کشور: " . print_r($countries['data'][0], true) . "\n";
+}
+echo "\n";
+
+// ─── 4. تست ساخت سفارش ────────────────────────────────────
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+echo "تست ۴: ثبت سفارش جدید\n";
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+
+$orderData = [
+ 'direction' => 'export',
+ 'type' => 'PARCEL',
+ 'from_country_id' => 1,
+ 'to_country_id' => 2,
+ 'weight' => 2.5,
+ 'volumetric_weight' => 3,
+ 'sender_name' => 'Kazem Test',
+ 'sender_phone' => '09123456789',
+ 'sender_address' => 'Tehran, Valiasr St.',
+ 'sender_city' => 'Tehran',
+ 'receiver_name' => 'Ali Customer',
+ 'receiver_phone' => '+971501234567',
+ 'receiver_address' => 'Dubai, Marina',
+ 'receiver_city' => 'Dubai',
+];
+
+$response4 = Http::withToken($token)
+ ->post('http://localhost:8000/api/v1/customer/orders', $orderData);
+
+echo "Status: " . $response4->status() . "\n";
+$orderResult = $response4->json();
+print_r($orderResult);
+echo "\n";
+
+// ─── 5. تست لیست سفارشات ───────────────────────────────────
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+echo "تست ۵: لیست سفارشات کاربر\n";
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
+
+$response5 = Http::withToken($token)
+ ->get('http://localhost:8000/api/v1/customer/orders');
+
+echo "Status: " . $response5->status() . "\n";
+$orders = $response5->json();
+echo "تعداد سفارشات: " . (isset($orders['data']) ? count($orders['data']) : 0) . "\n";
+if (isset($orders['data']) && count($orders['data']) > 0) {
+ echo "اولین سفارش: AWB = " . ($orders['data'][0]['awb_no'] ?? '—') . "\n";
+}
+echo "\n";
+
+// ─── جمعبندی ───────────────────────────────────────────────
+echo "══════════════════════════════════════════\n";
+echo " 🎯 نتیجه نهایی\n";
+echo "══════════════════════════════════════════\n";
+echo "Login: " . ($response->status() === 200 ? "✅" : "❌") . "\n";
+echo "Profile: " . ($response2->status() === 200 ? "✅" : "❌") . "\n";
+echo "Countries: " . ($response3->status() === 200 ? "✅" : "❌") . "\n";
+echo "Create Order:" . ($response4->status() === 201 ? "✅" : "❌") . "\n";
+echo "List Orders: " . ($response5->status() === 200 ? "✅" : "❌") . "\n";
+echo "══════════════════════════════════════════\n";
\ No newline at end of file