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__)
This commit is contained in:
parent
f61de6f3a5
commit
4ee1d27d44
@ -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);
|
||||||
|
}
|
||||||
@ -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('<option value="">انتخاب کشور مبدأ...</option>');
|
||||||
|
$to.empty().append('<option value="">انتخاب کشور مقصد...</option>');
|
||||||
|
$.each(countries, function(i, c) {
|
||||||
|
var opt = '<option value="' + c.id + '" data-iso="' + c.iso_code + '">' + c.name + '</option>';
|
||||||
|
$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 += '<div class="ifnex-price-row total"><span>مبلغ قابل پرداخت</span><span>' + formatNum(p.total_fee) + ' ریال</span></div>';
|
||||||
|
$('#ifnex-price-summary').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function priceRow(label, val) {
|
||||||
|
return '<div class="ifnex-price-row"><span>' + label + '</span><span>' + formatNum(val) + '</span></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
'<div class="ifnex-success-box">' +
|
||||||
|
'<h3>🎉 سفارش شما با موفقیت ثبت شد!</h3>' +
|
||||||
|
'<p>شماره پیگیری: <code>' + d.awb_no + '</code></p>' +
|
||||||
|
'<p>مبلغ قابل پرداخت: <strong>' + formatNum(d.total_fee) + ' ریال</strong></p>' +
|
||||||
|
'<p>وضعیت: ' + d.status.label + '</p>' +
|
||||||
|
'<a href="' + ifnex_ajax.orders_url + '" class="ifnex-btn ifnex-btn-primary">مشاهده سفارشات من</a>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -189,3 +189,419 @@ function ifnex_transactions_shortcode($atts) {
|
|||||||
<?php
|
<?php
|
||||||
return ob_get_clean();
|
return ob_get_clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// شورتکد پروفایل مشتری (جدید)
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
add_shortcode('ifnex_user_profile', 'ifnex_user_profile_shortcode');
|
||||||
|
|
||||||
|
function ifnex_user_profile_shortcode($atts) {
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
return '<p class="ifnex-error">برای مشاهده پروفایل، باید وارد شوید.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 '<p class="ifnex-error">' . esc_html($profile->get_error_message()) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="ifnex-user-profile">
|
||||||
|
<!-- اطلاعات کاربر -->
|
||||||
|
<div class="ifnex-profile-header">
|
||||||
|
<h2>👤 <?php echo esc_html($profile['user']['name']); ?></h2>
|
||||||
|
<div class="ifnex-profile-info">
|
||||||
|
<p><strong>ایمیل:</strong> <?php echo esc_html($profile['user']['email']); ?></p>
|
||||||
|
<p><strong>تلفن:</strong> <?php echo esc_html($profile['user']['phone'] ?? '—'); ?></p>
|
||||||
|
<p><strong>عضویت از:</strong> <?php echo esc_html($profile['user']['member_since']); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($atts['show_wallet'] === 'yes' && !empty($profile['wallet'])): ?>
|
||||||
|
<!-- موجودی کیف پول -->
|
||||||
|
<div class="ifnex-wallet-card">
|
||||||
|
<h3>💰 کیف پول</h3>
|
||||||
|
<div class="ifnex-wallet-balance">
|
||||||
|
<span class="ifnex-balance-amount">
|
||||||
|
<?php echo number_format($profile['wallet']['balance']); ?> ریال
|
||||||
|
</span>
|
||||||
|
<?php if ($profile['wallet']['is_frozen']): ?>
|
||||||
|
<span class="ifnex-badge ifnex-badge-danger">مسدود</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="ifnex-badge ifnex-badge-success">فعال</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($atts['show_stats'] === 'yes' && !empty($profile['stats'])): ?>
|
||||||
|
<!-- آمار سفارشات -->
|
||||||
|
<div class="ifnex-stats-grid">
|
||||||
|
<h3>📊 آمار سفارشات</h3>
|
||||||
|
<div class="ifnex-stats-cards">
|
||||||
|
<div class="ifnex-stat-card">
|
||||||
|
<div class="ifnex-stat-value"><?php echo number_format($profile['stats']['total_orders']); ?></div>
|
||||||
|
<div class="ifnex-stat-label">کل سفارشات</div>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-stat-card ifnex-stat-warning">
|
||||||
|
<div class="ifnex-stat-value"><?php echo number_format($profile['stats']['pending_orders']); ?></div>
|
||||||
|
<div class="ifnex-stat-label">در انتظار پرداخت</div>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-stat-card ifnex-stat-info">
|
||||||
|
<div class="ifnex-stat-value"><?php echo number_format($profile['stats']['active_orders']); ?></div>
|
||||||
|
<div class="ifnex-stat-label">در حال پردازش</div>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-stat-card ifnex-stat-success">
|
||||||
|
<div class="ifnex-stat-value"><?php echo number_format($profile['stats']['delivered_orders']); ?></div>
|
||||||
|
<div class="ifnex-stat-label">تحویل شده</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if ($profile['stats']['total_spent'] > 0): ?>
|
||||||
|
<p class="ifnex-total-spent">
|
||||||
|
<strong>مجموع خرید:</strong>
|
||||||
|
<?php echo number_format($profile['stats']['total_spent']); ?> ریال
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- لینکهای سریع -->
|
||||||
|
<div class="ifnex-quick-links">
|
||||||
|
<a href="<?php echo esc_url(home_url('/my-orders/')); ?>" class="ifnex-btn ifnex-btn-primary">
|
||||||
|
📦 مشاهده سفارشات
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url(home_url('/new-order/')); ?>" class="ifnex-btn ifnex-btn-success">
|
||||||
|
➕ ثبت سفارش جدید
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// شورتکد لیست سفارشات مشتری
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
add_shortcode('ifnex_orders_list', 'ifnex_orders_list_shortcode');
|
||||||
|
|
||||||
|
function ifnex_orders_list_shortcode($atts) {
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
return '<p class="ifnex-error">برای مشاهده سفارشات، باید وارد شوید.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 '<p class="ifnex-error">' . esc_html($result->get_error_message()) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$orders = $result['data'] ?? [];
|
||||||
|
$pagination = $result['pagination'] ?? [];
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="ifnex-orders-list">
|
||||||
|
<div class="ifnex-orders-header">
|
||||||
|
<h2>📦 سفارشات من</h2>
|
||||||
|
<a href="<?php echo esc_url(home_url('/new-order/')); ?>" class="ifnex-btn ifnex-btn-success">
|
||||||
|
➕ ثبت سفارش جدید
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- فیلتر وضعیت -->
|
||||||
|
<div class="ifnex-filter-bar">
|
||||||
|
<a href="?status=" class="<?php echo empty($atts['status']) ? 'active' : ''; ?>">همه</a>
|
||||||
|
<a href="?status=pending_payment" class="<?php echo $atts['status'] === 'pending_payment' ? 'active' : ''; ?>">در انتظار پرداخت</a>
|
||||||
|
<a href="?status=processed" class="<?php echo $atts['status'] === 'processed' ? 'active' : ''; ?>">پردازش شده</a>
|
||||||
|
<a href="?status=in_transit" class="<?php echo $atts['status'] === 'in_transit' ? 'active' : ''; ?>">در حال حمل</a>
|
||||||
|
<a href="?status=delivered" class="<?php echo $atts['status'] === 'delivered' ? 'active' : ''; ?>">تحویل شده</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($orders)): ?>
|
||||||
|
<div class="ifnex-empty-state">
|
||||||
|
<p>📭 هنوز سفارشی ثبت نکردهاید.</p>
|
||||||
|
<a href="<?php echo esc_url(home_url('/new-order/')); ?>" class="ifnex-btn ifnex-btn-primary">
|
||||||
|
اولین سفارش خود را ثبت کنید
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="ifnex-orders-grid">
|
||||||
|
<?php foreach ($orders as $order): ?>
|
||||||
|
<div class="ifnex-order-card">
|
||||||
|
<div class="ifnex-order-header">
|
||||||
|
<div class="ifnex-awb">
|
||||||
|
<strong>AWB:</strong>
|
||||||
|
<code><?php echo esc_html($order['awb_no']); ?></code>
|
||||||
|
</div>
|
||||||
|
<span class="ifnex-badge ifnex-badge-<?php echo esc_attr($order['status']['color']); ?>">
|
||||||
|
<?php echo esc_html($order['status']['label']); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ifnex-order-route">
|
||||||
|
<div class="ifnex-route-from">
|
||||||
|
<span class="ifnex-label">از:</span>
|
||||||
|
<span class="ifnex-country">
|
||||||
|
<?php echo esc_html($order['from_country']['name'] ?? '—'); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-route-arrow">➜</div>
|
||||||
|
<div class="ifnex-route-to">
|
||||||
|
<span class="ifnex-label">به:</span>
|
||||||
|
<span class="ifnex-country">
|
||||||
|
<?php echo esc_html($order['to_country']['name'] ?? '—'); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ifnex-order-details">
|
||||||
|
<div class="ifnex-detail-item">
|
||||||
|
<span class="ifnex-label">وزن:</span>
|
||||||
|
<span><?php echo esc_html($order['weight']); ?> kg</span>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-detail-item">
|
||||||
|
<span class="ifnex-label">مبلغ:</span>
|
||||||
|
<span class="ifnex-price">
|
||||||
|
<?php echo number_format($order['total_fee']); ?> ریال
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-detail-item">
|
||||||
|
<span class="ifnex-label">تاریخ:</span>
|
||||||
|
<span><?php echo esc_html($order['created_at_jalali']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ifnex-order-actions">
|
||||||
|
<a href="<?php echo esc_url(home_url('/order-detail/' . $order['id'] . '/')); ?>"
|
||||||
|
class="ifnex-btn ifnex-btn-sm ifnex-btn-info">
|
||||||
|
👁️ جزئیات
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php if ($order['status']['can_cancel']): ?>
|
||||||
|
<button class="ifnex-btn ifnex-btn-sm ifnex-btn-danger ifnex-cancel-order"
|
||||||
|
data-order-id="<?php echo esc_attr($order['id']); ?>">
|
||||||
|
❌ لغو سفارش
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!$order['status']['is_paid']): ?>
|
||||||
|
<a href="<?php echo esc_url(home_url('/payment/' . $order['id'] . '/')); ?>"
|
||||||
|
class="ifnex-btn ifnex-btn-sm ifnex-btn-success">
|
||||||
|
💳 پرداخت
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($pagination) && $pagination['last_page'] > 1): ?>
|
||||||
|
<div class="ifnex-pagination">
|
||||||
|
<?php for ($i = 1; $i <= $pagination['last_page']; $i++): ?>
|
||||||
|
<a href="?page=<?php echo $i; ?>"
|
||||||
|
class="<?php echo $i === $pagination['current_page'] ? 'active' : ''; ?>">
|
||||||
|
<?php echo $i; ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$('.ifnex-cancel-order').on('click', function() {
|
||||||
|
if (!confirm('آیا از لغو این سفارش اطمینان دارید؟')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderId = $(this).data('order-id');
|
||||||
|
const btn = $(this);
|
||||||
|
|
||||||
|
btn.prop('disabled', true).text('در حال لغو...');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ifnex_ajax.ajax_url,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'ifnex_cancel_order',
|
||||||
|
order_id: orderId,
|
||||||
|
nonce: ifnex_ajax.nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
alert('سفارش با موفقیت لغو شد.');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('خطا: ' + response.data);
|
||||||
|
btn.prop('disabled', false).text('❌ لغو سفارش');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('خطا در ارتباط با سرور.');
|
||||||
|
btn.prop('disabled', false).text('❌ لغو سفارش');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// شورتکد فرم ثبت سفارش چند مرحلهای
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
add_shortcode('ifnex_order_form', 'ifnex_order_form_shortcode');
|
||||||
|
|
||||||
|
function ifnex_order_form_shortcode($atts) {
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="ifnex-login-prompt">
|
||||||
|
<h3>🔐 ورود لازم است</h3>
|
||||||
|
<p>برای ثبت سفارش، ابتدا باید وارد حساب کاربری خود شوید.</p>
|
||||||
|
<a href="<?php echo esc_url(wp_login_url(get_permalink())); ?>" class="ifnex-btn ifnex-btn-primary">ورود به حساب</a>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="ifnex-order-form-container" id="ifnex-order-form">
|
||||||
|
|
||||||
|
<!-- Progress Steps -->
|
||||||
|
<div class="ifnex-steps">
|
||||||
|
<div class="ifnex-step active" data-step="1"><span>۱</span><br>مسیر و بسته</div>
|
||||||
|
<div class="ifnex-step" data-step="2"><span>۲</span><br>اطلاعات تماس</div>
|
||||||
|
<div class="ifnex-step" data-step="3"><span>۳</span><br>بررسی قیمت</div>
|
||||||
|
<div class="ifnex-step" data-step="4"><span>۴</span><br>تأیید نهایی</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ifnex-form-error" class="ifnex-error-box" style="display:none;"></div>
|
||||||
|
|
||||||
|
<!-- Step 1: Route & Package -->
|
||||||
|
<div class="ifnex-form-step active" data-step="1">
|
||||||
|
<div class="ifnex-form-card">
|
||||||
|
<h3>📦 مشخصات مرسوله</h3>
|
||||||
|
<div class="ifnex-form-grid">
|
||||||
|
<div class="ifnex-field">
|
||||||
|
<label>جهت ارسال *</label>
|
||||||
|
<select id="ifnex-direction">
|
||||||
|
<option value="export">صادرات (از ایران به خارج)</option>
|
||||||
|
<option value="import">واردات (از خارج به ایران)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-field">
|
||||||
|
<label>نوع سرویس *</label>
|
||||||
|
<select id="ifnex-type">
|
||||||
|
<option value="PARCEL">بسته (Parcel)</option>
|
||||||
|
<option value="DOC_NORMAL">مدارک عادی</option>
|
||||||
|
<option value="DOC_ECONOMY">مدارک اقتصادی</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-field">
|
||||||
|
<label>کشور مبدأ *</label>
|
||||||
|
<select id="ifnex-from-country"></select>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-field">
|
||||||
|
<label>کشور مقصد *</label>
|
||||||
|
<select id="ifnex-to-country"></select>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-field">
|
||||||
|
<label>وزن واقعی (kg) *</label>
|
||||||
|
<input type="number" id="ifnex-weight" step="0.1" min="0.1" placeholder="مثلاً 2.5">
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-field">
|
||||||
|
<label>وزن حجمی (kg)</label>
|
||||||
|
<input type="number" id="ifnex-volumetric-weight" step="0.1" min="0" placeholder="اختیاری">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-form-nav">
|
||||||
|
<span></span>
|
||||||
|
<button class="ifnex-btn ifnex-btn-primary ifnex-next-step">ادامه ←</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Sender & Receiver -->
|
||||||
|
<div class="ifnex-form-step" data-step="2">
|
||||||
|
<div class="ifnex-form-card">
|
||||||
|
<h3>👤 اطلاعات فرستنده</h3>
|
||||||
|
<div class="ifnex-form-grid">
|
||||||
|
<div class="ifnex-field"><label>نام و نام خانوادگی *</label><input type="text" id="ifnex-sender-name"></div>
|
||||||
|
<div class="ifnex-field"><label>شماره تماس *</label><input type="tel" id="ifnex-sender-phone"></div>
|
||||||
|
<div class="ifnex-field"><label>شهر</label><input type="text" id="ifnex-sender-city"></div>
|
||||||
|
<div class="ifnex-field full"><label>آدرس کامل *</label><textarea id="ifnex-sender-address" rows="2"></textarea></div>
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-top:24px;">📮 اطلاعات گیرنده</h3>
|
||||||
|
<div class="ifnex-form-grid">
|
||||||
|
<div class="ifnex-field"><label>نام و نام خانوادگی *</label><input type="text" id="ifnex-receiver-name"></div>
|
||||||
|
<div class="ifnex-field"><label>شماره تماس *</label><input type="tel" id="ifnex-receiver-phone"></div>
|
||||||
|
<div class="ifnex-field"><label>شهر</label><input type="text" id="ifnex-receiver-city"></div>
|
||||||
|
<div class="ifnex-field full"><label>آدرس کامل *</label><textarea id="ifnex-receiver-address" rows="2"></textarea></div>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-form-nav">
|
||||||
|
<button class="ifnex-btn ifnex-prev-step">→ قبلی</button>
|
||||||
|
<button class="ifnex-btn ifnex-btn-primary ifnex-next-step">ادامه ←</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Review & Discount -->
|
||||||
|
<div class="ifnex-form-step" data-step="3">
|
||||||
|
<div class="ifnex-form-card">
|
||||||
|
<h3>🏷️ کد تخفیف (اختیاری)</h3>
|
||||||
|
<div class="ifnex-form-grid">
|
||||||
|
<div class="ifnex-field full"><label>کد تخفیف</label><input type="text" id="ifnex-discount-code" placeholder="مثلاً WELCOME10"></div>
|
||||||
|
</div>
|
||||||
|
<div id="ifnex-calc-error" class="ifnex-error-box" style="display:none;"></div>
|
||||||
|
<div id="ifnex-calc-loading" style="display:none; text-align:center; padding:20px;">
|
||||||
|
⏳ در حال محاسبه قیمت...
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-form-nav">
|
||||||
|
<button class="ifnex-btn ifnex-prev-step">→ قبلی</button>
|
||||||
|
<button class="ifnex-btn ifnex-btn-success ifnex-next-step">💰 محاسبه قیمت</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: Confirm -->
|
||||||
|
<div class="ifnex-form-step" data-step="4">
|
||||||
|
<div class="ifnex-form-card">
|
||||||
|
<h3>✅ تأیید نهایی سفارش</h3>
|
||||||
|
<div class="ifnex-price-summary" id="ifnex-price-summary"></div>
|
||||||
|
<div id="ifnex-submit-error" class="ifnex-error-box" style="display:none; margin-top:16px;"></div>
|
||||||
|
<div class="ifnex-form-nav">
|
||||||
|
<button class="ifnex-btn ifnex-prev-step">→ قبلی</button>
|
||||||
|
<button class="ifnex-btn ifnex-btn-success" id="ifnex-submit-order">✅ تأیید و ثبت سفارش</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
@ -47,8 +47,10 @@ class IFNEX_User_Bridge {
|
|||||||
* درخواست توکن از لاراول
|
* درخواست توکن از لاراول
|
||||||
*/
|
*/
|
||||||
private function request_token($email, $password) {
|
private function request_token($email, $password) {
|
||||||
$url = $this->api_url . '/sanctum/token';
|
//$url = $this->api_url . '/sanctum/token';
|
||||||
|
|
||||||
|
$url = $this->api_url . '/auth/login';
|
||||||
|
|
||||||
$args = array(
|
$args = array(
|
||||||
'headers' => array(
|
'headers' => array(
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
@ -218,4 +220,245 @@ class IFNEX_User_Bridge {
|
|||||||
|
|
||||||
return $data;
|
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/'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,11 +4,78 @@ namespace App\Enums;
|
|||||||
|
|
||||||
enum ShipmentStatus: string
|
enum ShipmentStatus: string
|
||||||
{
|
{
|
||||||
case Processed = 'processed';
|
// ─── وضعیتهای مشتری (قبل از پرداخت) ───
|
||||||
case PickedUp = 'picked_up';
|
case PendingPayment = 'pending_payment'; // در انتظار پرداخت
|
||||||
case InTransit = 'in_transit';
|
case Cancelled = 'cancelled'; // لغو شده توسط مشتری
|
||||||
case OutForDelivery = 'out_for_delivery';
|
|
||||||
case Failed = 'failed';
|
// ─── وضعیتهای عملیاتی (بعد از پرداخت) ───
|
||||||
case Delivered = 'delivered';
|
case Processed = 'processed'; // پردازش شده (ادمین تأیید کرده)
|
||||||
case Returned = 'returned';
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,6 +45,21 @@ class ShipmentResource extends Resource
|
|||||||
'PARCEL' => 'Parcel',
|
'PARCEL' => 'Parcel',
|
||||||
])
|
])
|
||||||
->required(),
|
->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')
|
Forms\Components\Select::make('status')
|
||||||
->options([
|
->options([
|
||||||
'processed' => 'Processed',
|
'processed' => 'Processed',
|
||||||
@ -301,6 +316,8 @@ class ShipmentResource extends Resource
|
|||||||
->filters([
|
->filters([
|
||||||
Tables\Filters\SelectFilter::make('status')
|
Tables\Filters\SelectFilter::make('status')
|
||||||
->options([
|
->options([
|
||||||
|
'pending_payment' => 'در انتظار پرداخت',
|
||||||
|
'cancelled' => 'لغو شده',
|
||||||
'processed' => 'Processed',
|
'processed' => 'Processed',
|
||||||
'picked_up' => 'Picked Up',
|
'picked_up' => 'Picked Up',
|
||||||
'in_transit' => 'In Transit',
|
'in_transit' => 'In Transit',
|
||||||
|
|||||||
68
04_Laravel/app/Http/Controllers/Api/AuthController.php
Normal file
68
04_Laravel/app/Http/Controllers/Api/AuthController.php
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* ورود و دریافت توکن Sanctum
|
||||||
|
* POST /api/v1/auth/login
|
||||||
|
*/
|
||||||
|
public function login(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->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' => 'خروج با موفقیت انجام شد.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,492 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Customer;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Shipment;
|
||||||
|
use App\Models\ShipmentItem;
|
||||||
|
use App\Models\Country;
|
||||||
|
use App\Enums\ShipmentStatus;
|
||||||
|
use App\Enums\ShipmentDirection;
|
||||||
|
use App\Enums\ShipmentType;
|
||||||
|
use App\Services\PriceCalculatorService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class CustomerOrderController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected PriceCalculatorService $calculator
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* لیست سفارشات کاربر
|
||||||
|
* GET /api/v1/customer/orders
|
||||||
|
*/
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,3 +6,8 @@ abstract class Controller
|
|||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CustomerOrderController
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// اضافه کردن pending_payment و cancelled به enum
|
||||||
|
DB::statement("
|
||||||
|
ALTER TABLE `shipments`
|
||||||
|
MODIFY COLUMN `status` ENUM(
|
||||||
|
'pending_payment',
|
||||||
|
'cancelled',
|
||||||
|
'processed',
|
||||||
|
'picked_up',
|
||||||
|
'in_transit',
|
||||||
|
'out_for_delivery',
|
||||||
|
'delivered',
|
||||||
|
'failed',
|
||||||
|
'returned'
|
||||||
|
) DEFAULT 'pending_payment'
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// قبل از بازگشت، همه pending_payment و cancelled را به processed تغییر بده
|
||||||
|
DB::table('shipments')
|
||||||
|
->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'
|
||||||
|
");
|
||||||
|
}
|
||||||
|
};
|
||||||
214
04_Laravel/database/seeders/SampleShippingRatesSeeder.php
Normal file
214
04_Laravel/database/seeders/SampleShippingRatesSeeder.php
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Country;
|
||||||
|
use App\Models\ShippingRate;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class SampleShippingRatesSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// بررسی zoneهای موجود در جدول
|
||||||
|
$allColumns = Schema::getColumnListing('shipping_rates');
|
||||||
|
$zoneColumns = array_filter($allColumns, fn($col) => 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!');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,39 +1,68 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\Customer\CustomerOrderController;
|
||||||
use App\Http\Controllers\Api\DiscountCodeController;
|
use App\Http\Controllers\Api\DiscountCodeController;
|
||||||
|
use App\Http\Controllers\Api\MockGatewayController;
|
||||||
use App\Http\Controllers\Api\PaymentController;
|
use App\Http\Controllers\Api\PaymentController;
|
||||||
use App\Http\Controllers\Api\PricingController;
|
use App\Http\Controllers\Api\PricingController;
|
||||||
use App\Http\Controllers\Api\TrackController;
|
use App\Http\Controllers\Api\TrackController;
|
||||||
use App\Http\Controllers\Api\WalletController;
|
use App\Http\Controllers\Api\WalletController;
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use App\Http\Middleware\ApiKeyMiddleware;
|
use App\Http\Middleware\ApiKeyMiddleware;
|
||||||
use App\Http\Controllers\Api\MockGatewayController;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\Api\AuthController;
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// API عمومی با API Key
|
||||||
// APIهای عمومی با API Key
|
// ══════════════════════════════════════════════════════════════
|
||||||
Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () {
|
Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () {
|
||||||
Route::get('/track/{awb_no}', [TrackController::class, 'show']);
|
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 () {
|
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
||||||
|
|
||||||
// Wallet APIها (کاربر عادی)
|
// ─── Wallet API (کاربر عادی) ───
|
||||||
Route::get('/wallet/balance', [WalletController::class, 'balance']);
|
Route::get('/wallet/balance', [WalletController::class, 'balance']);
|
||||||
Route::get('/wallet/transactions', [WalletController::class, 'transactions']);
|
Route::get('/wallet/transactions', [WalletController::class, 'transactions']);
|
||||||
|
|
||||||
// Payment APIها
|
// ─── Payment API ───
|
||||||
Route::post('/payment/redirect', [PaymentController::class, 'redirectToGateway']);
|
Route::post('/payment/redirect', [PaymentController::class, 'redirectToGateway']);
|
||||||
Route::get('/payment/check/{transaction}', [PaymentController::class, 'checkStatus']);
|
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/admin-adjust', [WalletController::class, 'adminAdjust']);
|
||||||
Route::post('/wallet/{wallet}/freeze', [WalletController::class, 'freeze']);
|
Route::post('/wallet/{wallet}/freeze', [WalletController::class, 'freeze']);
|
||||||
Route::post('/wallet/{wallet}/unfreeze', [WalletController::class, 'unfreeze']);
|
Route::post('/wallet/{wallet}/unfreeze', [WalletController::class, 'unfreeze']);
|
||||||
Route::get('/wallet/{wallet}/activity-log', [WalletController::class, 'activityLog']);
|
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 (عمومی - برای شبیهسازی درگاه)
|
// Mock Gateway Routes (عمومی - برای شبیهسازی درگاه)
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
Route::prefix('v1/payment')->group(function () {
|
Route::prefix('v1/payment')->group(function () {
|
||||||
Route::get('/mock-gateway', [MockGatewayController::class, 'showGateway']);
|
Route::get('/mock-gateway', [MockGatewayController::class, 'showGateway']);
|
||||||
Route::get('/mock-gateway/success', [MockGatewayController::class, 'simulateSuccess']);
|
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'])
|
Route::any('/v1/payment/callback', [PaymentController::class, 'callback'])
|
||||||
->name('payment.callback');
|
->name('payment.callback');
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
// APIهای عمومی (بدون auth)
|
// APIهای عمومی (بدون auth)
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
Route::post('/v1/calculate', [PricingController::class, 'calculate']);
|
Route::post('/v1/calculate', [PricingController::class, 'calculate']);
|
||||||
Route::get('/v1/discount-codes', [DiscountCodeController::class, 'index']);
|
Route::get('/v1/discount-codes', [DiscountCodeController::class, 'index']);
|
||||||
Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']);
|
Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']);
|
||||||
136
04_Laravel/test-api.php
Normal file
136
04_Laravel/test-api.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
$kernel = $app->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";
|
||||||
Loading…
Reference in New Issue
Block a user