feat(invoices): thousand-separator formatting + fix total calc + integrate invoices into customer portal
Polish the manual invoice feature with 4 improvements: 1. Admin invoice form: numeric fields (unit_price, discount_amount) now display values with thousand-separators (e.g. 1,000,000 instead of 1000000) via formatStateUsing/dehydrateStateUsing for easier reading of large Rial amounts. The placeholder/subtotal/total calculations also strip commas before arithmetic. 2. Fix 'total amount payable = 0' bug in the form. The placeholder had a typo: 'foreach ( as )' overwrote the loop variable and made the inner calculation use the array instead of each item, so the sum was always 0. Now correctly iterates with . 3. Customer dashboard widget: add an 'Invoices pending payment' card showing the count, with a link to /my-account/?tab=invoices if > 0. Relabel the existing 'Pending payment' card to 'Pending payment (orders)' to clarify it's for orders, not invoices. 4. Customer profile financial status: include manual invoices in the accounting_balance and pending_payments_total. Show a breakdown list (orders vs invoices) under the balance card, plus two new stat cards (pending invoices count/total, paid invoices count/total) with a link to the invoices tab.
This commit is contained in:
parent
848f95f32e
commit
7fe368bc46
@ -246,13 +246,37 @@ function ifnex_user_profile_shortcode($atts) {
|
||||
<div style="font-size: 24px; font-weight: bold; color: <?php echo ($profile['account_balance'] ?? 0) >= 0 ? '#059669' : '#dc2626'; ?>;">
|
||||
<?php echo number_format($profile['account_balance'] ?? 0); ?> ریال
|
||||
</div>
|
||||
<?php if (($profile['pending_payments_total'] ?? 0) > 0): ?>
|
||||
<div style="font-size: 11px; color: #dc2626; margin-top: 5px;">
|
||||
⚠️ بدهی در انتظار پرداخت: <?php echo number_format($profile['pending_payments_total']); ?> ریال
|
||||
|
||||
<?php
|
||||
// تفکیک بدهیها
|
||||
$breakdown = $profile['pending_payments_breakdown'] ?? null;
|
||||
$pending_total = $profile['pending_payments_total'] ?? 0;
|
||||
$orders_debt = $breakdown['orders'] ?? $pending_total;
|
||||
$invoices_debt = $breakdown['invoices'] ?? 0;
|
||||
$invoices_count = $profile['pending_invoices_count'] ?? 0;
|
||||
?>
|
||||
|
||||
<?php if ($pending_total > 0): ?>
|
||||
<div style="font-size: 11px; color: #dc2626; margin-top: 8px; padding-top: 8px; border-top: 1px dashed #fecaca;">
|
||||
⚠️ مجموع بدهی در انتظار پرداخت: <strong><?php echo number_format($pending_total); ?> ریال</strong>
|
||||
</div>
|
||||
<ul style="margin: 8px 0 0 0; padding-right: 20px; font-size: 11px; color: #666;">
|
||||
<li>پرداختی سفارشات: <?php echo number_format($orders_debt); ?> ریال</li>
|
||||
<li>
|
||||
پرداختی فاکتورهای دستی
|
||||
<?php if ($invoices_count > 0): ?>
|
||||
(<?php echo (int)$invoices_count; ?> فاکتور)
|
||||
<?php endif; ?>:
|
||||
<?php echo number_format($invoices_debt); ?> ریال
|
||||
<?php if ($invoices_count > 0): ?>
|
||||
— <a href="<?php echo esc_url(home_url('/my-account/?tab=invoices')); ?>" style="color:#dc2626;font-weight:600;">مشاهده فاکتورها ←</a>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<!-- بدهیهای چندارزی -->
|
||||
|
||||
<!-- بدهیهای چندارزی -->
|
||||
<?php if (!empty($profile['foreign_credits'])): ?>
|
||||
<div style="background: white; border-radius: 8px; padding: 15px; margin-bottom: 15px; border: 1px solid #fed7aa;">
|
||||
<div style="font-size: 12px; color: #666; margin-bottom: 10px; font-weight: bold;">
|
||||
@ -331,7 +355,7 @@ function ifnex_user_profile_shortcode($atts) {
|
||||
</div>
|
||||
<div class="ifnex-stat-card ifnex-stat-info">
|
||||
<div class="ifnex-stat-value"><?php echo number_format($profile['stats']['pending_payment'] ?? 0); ?></div>
|
||||
<div class="ifnex-stat-label">در انتظار پرداخت</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>
|
||||
@ -342,6 +366,38 @@ function ifnex_user_profile_shortcode($atts) {
|
||||
<div class="ifnex-stat-label">تحویل شده</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// بخش فاکتورهای دستی — اگه هرگز آمار فاکتور داره
|
||||
$inv_pending_count = $profile['stats']['pending_invoices_count'] ?? 0;
|
||||
$inv_pending_total = $profile['stats']['pending_invoices_total'] ?? 0;
|
||||
$inv_paid_count = $profile['stats']['paid_invoices_count'] ?? 0;
|
||||
$inv_paid_total = $profile['stats']['paid_invoices_total'] ?? 0;
|
||||
?>
|
||||
<?php if ($inv_pending_count > 0 || $inv_paid_count > 0): ?>
|
||||
<div class="ifnex-stats-cards" style="margin-top:12px;">
|
||||
<div class="ifnex-stat-card ifnex-stat-warning">
|
||||
<div class="ifnex-stat-value"><?php echo (int)$inv_pending_count; ?></div>
|
||||
<div class="ifnex-stat-label">فاکتور در انتظار پرداخت</div>
|
||||
<div style="font-size:11px;color:#92400e;margin-top:4px;">
|
||||
<?php echo number_format($inv_pending_total); ?> ریال
|
||||
</div>
|
||||
</div>
|
||||
<div class="ifnex-stat-card ifnex-stat-success">
|
||||
<div class="ifnex-stat-value"><?php echo (int)$inv_paid_count; ?></div>
|
||||
<div class="ifnex-stat-label">فاکتور پرداختشده</div>
|
||||
<div style="font-size:11px;color:#065f46;margin-top:4px;">
|
||||
<?php echo number_format($inv_paid_total); ?> ریال
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:12px;margin-top:8px;">
|
||||
<a href="<?php echo esc_url(home_url('/my-account/?tab=invoices')); ?>" style="color:#0369a1;font-weight:600;">
|
||||
📋 مشاهده همه فاکتورها ←
|
||||
</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($profile['stats']['total_spent'] > 0): ?>
|
||||
<p class="ifnex-total-spent">
|
||||
<strong>مجموع خرید:</strong>
|
||||
@ -1616,6 +1672,7 @@ function ifnex_icon($name, $size = 20) {
|
||||
'check' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>',
|
||||
'clock' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/>',
|
||||
'bell' => '<path stroke-linecap="round" stroke-linejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>',
|
||||
'invoice' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>',
|
||||
];
|
||||
|
||||
$path = $icons[$name] ?? $icons['dashboard'];
|
||||
@ -1643,7 +1700,7 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
}
|
||||
|
||||
$tab = sanitize_key($_GET['tab'] ?? 'dashboard');
|
||||
$allowed_tabs = ['dashboard', 'notifications', 'orders', 'new-order', 'wallet', 'transactions', 'tracking', 'profile'];
|
||||
$allowed_tabs = ['dashboard', 'notifications', 'orders', 'new-order', 'invoices', 'wallet', 'transactions', 'tracking', 'profile'];
|
||||
if (!in_array($tab, $allowed_tabs)) $tab = 'dashboard';
|
||||
|
||||
$bridge = new IFNEX_User_Bridge();
|
||||
@ -1664,6 +1721,13 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
$unread_notifications = (int) $notif_count_result['unread_count'];
|
||||
}
|
||||
|
||||
// تعداد فاکتورهای در انتظار پرداخت برای badge
|
||||
$pending_invoices_count = 0;
|
||||
$invoices_result = $bridge->authenticated_request($user_id, '/customer/invoices?status=pending_payment&per_page=100', 'GET');
|
||||
if (!is_wp_error($invoices_result) && isset($invoices_result['data']['invoices'])) {
|
||||
$pending_invoices_count = count($invoices_result['data']['invoices']);
|
||||
}
|
||||
|
||||
$recent_orders = [];
|
||||
if ($tab === 'dashboard') {
|
||||
$orders_result = $bridge->get_customer_orders($user_id, 3);
|
||||
@ -1676,6 +1740,7 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
'notifications'=> ['icon' => 'bell', 'label' => 'اعلانها'],
|
||||
'orders' => ['icon' => 'orders', 'label' => 'سفارشات من'],
|
||||
'new-order' => ['icon' => 'plus', 'label' => 'ثبت سفارش جدید'],
|
||||
'invoices' => ['icon' => 'invoice', 'label' => 'فاکتورهای دستی'],
|
||||
'wallet' => ['icon' => 'wallet', 'label' => 'کیف پول'],
|
||||
'transactions' => ['icon' => 'chart', 'label' => 'تراکنشها'],
|
||||
'tracking' => ['icon' => 'tracking', 'label' => 'رهگیری مرسوله'],
|
||||
@ -1684,6 +1749,7 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
|
||||
$tab_titles = [
|
||||
'dashboard' => 'داشبورد', 'notifications' => 'اعلانها', 'orders' => 'سفارشات من', 'new-order' => 'ثبت سفارش جدید',
|
||||
'invoices' => 'فاکتورهای دستی',
|
||||
'wallet' => 'کیف پول', 'transactions' => 'تراکنشها', 'tracking' => 'رهگیری مرسوله', 'profile' => 'پروفایل',
|
||||
];
|
||||
|
||||
@ -1707,6 +1773,9 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
<?php if ($key === 'notifications' && $unread_notifications > 0): ?>
|
||||
<span class="ifnx-menu-badge"><?php echo (int)$unread_notifications; ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($key === 'invoices' && $pending_invoices_count > 0): ?>
|
||||
<span class="ifnx-menu-badge"><?php echo (int)$pending_invoices_count; ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<div class="ifnx-menu-divider"></div>
|
||||
@ -1744,7 +1813,19 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
<div class="ifnx-stat-icon"><?php echo ifnex_icon('clock', 22); ?></div>
|
||||
<div class="ifnx-stat-info">
|
||||
<span class="ifnx-stat-value"><?php echo number_format($stats['pending_payment'] ?? 0); ?></span>
|
||||
<span class="ifnx-stat-label">در انتظار پرداخت</span>
|
||||
<span class="ifnx-stat-label">در انتظار پرداخت (سفارش)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ifnx-stat" style="--tint:#fff7ed; --clr:#ea580c;">
|
||||
<div class="ifnx-stat-icon"><?php echo ifnex_icon('invoice', 22); ?></div>
|
||||
<div class="ifnx-stat-info">
|
||||
<span class="ifnx-stat-value"><?php echo (int)($stats['pending_invoices_count'] ?? 0); ?></span>
|
||||
<span class="ifnx-stat-label">
|
||||
فاکتور دستی در انتظار پرداخت
|
||||
<?php if (($stats['pending_invoices_count'] ?? 0) > 0): ?>
|
||||
<a href="<?php echo esc_url(home_url('/my-account/?tab=invoices')); ?>" style="font-size:10px;color:#ea580c;font-weight:600;display:block;margin-top:2px;">مشاهده ←</a>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ifnx-stat" style="--tint:#f5f3ff; --clr:#8b5cf6;">
|
||||
@ -1792,6 +1873,282 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
<?php echo do_shortcode('[ifnex_orders_list]'); ?>
|
||||
<?php elseif ($tab === 'new-order'): ?>
|
||||
<?php echo do_shortcode('[ifnex_order_form]'); ?>
|
||||
<?php elseif ($tab === 'invoices'): ?>
|
||||
<?php
|
||||
// نمایش لیست یا جزئیات فاکتور بر اساس پارامتر ?invoice=X
|
||||
$single_invoice_id = isset($_GET['invoice']) ? intval($_GET['invoice']) : 0;
|
||||
if ($single_invoice_id > 0) {
|
||||
// دریافت جزئیات فاکتور از API
|
||||
$single_result = $bridge->authenticated_request($user_id, "/customer/invoices/{$single_invoice_id}", 'GET');
|
||||
if (is_wp_error($single_result)) {
|
||||
echo '<div class="ifnex-alert ifnex-alert-danger">خطا در دریافت فاکتور: ' . esc_html($single_result->get_error_message()) . '</div>';
|
||||
} else {
|
||||
$inv = $single_result['data'] ?? null;
|
||||
if ($inv):
|
||||
$status_labels = [
|
||||
'pending_payment' => 'در انتظار پرداخت',
|
||||
'paid' => 'پرداخت شده',
|
||||
'cancelled' => 'لغو شده',
|
||||
'expired' => 'منقضی',
|
||||
];
|
||||
$status_class = $inv['status'] ?? '';
|
||||
$status_label = $status_labels[$inv['status']] ?? $inv['status'];
|
||||
$items = $inv['items'] ?? [];
|
||||
$subtotal = (float)($inv['subtotal'] ?? 0);
|
||||
$discount = (float)($inv['discount_amount'] ?? 0);
|
||||
$total = (float)($inv['total_amount'] ?? 0);
|
||||
?>
|
||||
<style>
|
||||
.ifnex-invoice-detail {background:#fff;border-radius:12px;padding:24px;border:1px solid #e2e8f0;}
|
||||
.ifnex-invoice-detail h3 {margin:0 0 16px;color:#0f172a;font-size:20px;}
|
||||
.ifnex-invoice-meta {display:flex;flex-wrap:wrap;gap:16px;margin-bottom:20px;padding:14px 16px;background:#f8fafc;border-radius:8px;}
|
||||
.ifnex-invoice-meta div {font-size:13px;color:#475569;}
|
||||
.ifnex-invoice-meta strong {color:#0f172a;font-weight:600;}
|
||||
.ifnex-status-badge {padding:4px 12px;border-radius:999px;font-size:12px;font-weight:700;display:inline-block;}
|
||||
.ifnex-status-pending_payment {background:#fef3c7;color:#92400e;}
|
||||
.ifnex-status-paid {background:#ecfdf5;color:#065f46;}
|
||||
.ifnex-status-cancelled {background:#fee2e2;color:#991b1b;}
|
||||
.ifnex-status-expired {background:#f1f5f9;color:#475569;}
|
||||
.ifnex-items-table {width:100%;border-collapse:collapse;margin:16px 0;}
|
||||
.ifnex-items-table th, .ifnex-items-table td {padding:10px 12px;text-align:right;border-bottom:1px solid #e2e8f0;}
|
||||
.ifnex-items-table th {background:#f1f5f9;font-weight:600;color:#334155;font-size:13px;}
|
||||
.ifnex-items-table td {font-size:14px;color:#1e293b;}
|
||||
.ifnex-items-table .num {text-align:left;font-variant-numeric:tabular-nums;}
|
||||
.ifnex-totals {margin-top:16px;padding:14px 16px;background:#f8fafc;border-radius:8px;}
|
||||
.ifnex-totals div {display:flex;justify-content:space-between;padding:6px 0;font-size:14px;}
|
||||
.ifnex-totals .grand {font-size:18px;font-weight:700;color:#059669;border-top:2px solid #10b981;margin-top:8px;padding-top:10px;}
|
||||
.ifnex-note {margin-top:16px;padding:12px 16px;background:#fef3c7;border-right:4px solid #f59e0b;border-radius:6px;font-size:13px;color:#78350f;}
|
||||
.ifnex-invoice-actions {margin-top:20px;display:flex;gap:10px;}
|
||||
.ifnex-pay-btn {background:#10b981;color:#fff;border:none;padding:12px 24px;border-radius:8px;font-size:15px;font-weight:700;cursor:pointer;}
|
||||
.ifnex-pay-btn:hover {background:#059669;}
|
||||
.ifnex-pay-btn:disabled {background:#9ca3af;cursor:not-allowed;}
|
||||
.ifnex-back-btn {background:#e5e7eb;color:#1e293b;padding:12px 24px;border-radius:8px;text-decoration:none;font-size:14px;display:inline-flex;align-items:center;}
|
||||
</style>
|
||||
<div class="ifnex-invoice-detail">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;">
|
||||
<div>
|
||||
<h3>فاکتور <?php echo esc_html($inv['invoice_number']); ?></h3>
|
||||
<span class="ifnex-status-badge ifnex-status-<?php echo esc_attr($status_class); ?>"><?php echo esc_html($status_label); ?></span>
|
||||
</div>
|
||||
<a href="<?php echo esc_url(home_url('/my-account/?tab=invoices')); ?>" class="ifnex-back-btn">→ بازگشت به لیست</a>
|
||||
</div>
|
||||
|
||||
<div class="ifnex-invoice-meta">
|
||||
<div>صادرکننده: <strong><?php echo esc_html($inv['issued_by']['name'] ?? '—'); ?></strong></div>
|
||||
<div>تاریخ صدور: <strong><?php echo esc_html(ifnex_format_jalali_date($inv['created_at'])); ?></strong></div>
|
||||
<?php if (!empty($inv['due_date'])): ?>
|
||||
<div>موعد پرداخت: <strong><?php echo esc_html($inv['due_date']); ?></strong></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($inv['paid_at'])): ?>
|
||||
<div>تاریخ پرداخت: <strong><?php echo esc_html(ifnex_format_jalali_date($inv['paid_at'])); ?></strong></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($items)): ?>
|
||||
<table class="ifnex-items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>شرح خدمت/کالا</th>
|
||||
<th class="num">تعداد</th>
|
||||
<th class="num">قیمت واحد (ریال)</th>
|
||||
<th class="num">مجموع (ریال)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $i => $item): ?>
|
||||
<?php
|
||||
$qty = (float)($item['quantity'] ?? 0);
|
||||
$unit = (float)($item['unit_price'] ?? 0);
|
||||
$line_total = $qty * $unit;
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo (int)$i + 1; ?></td>
|
||||
<td><?php echo esc_html($item['description'] ?? ''); ?></td>
|
||||
<td class="num"><?php echo number_format($qty); ?></td>
|
||||
<td class="num"><?php echo number_format($unit); ?></td>
|
||||
<td class="num"><?php echo number_format($line_total); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ifnex-totals">
|
||||
<div><span>مجموع اقلام:</span><strong><?php echo number_format($subtotal); ?> ریال</strong></div>
|
||||
<?php if ($discount > 0): ?>
|
||||
<div><span>تخفیف:</span><strong>- <?php echo number_format($discount); ?> ریال</strong></div>
|
||||
<?php endif; ?>
|
||||
<div class="grand"><span>مبلغ قابل پرداخت:</span><span><?php echo number_format($total); ?> ریال</span></div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($inv['notes_to_customer'])): ?>
|
||||
<div class="ifnex-note"><?php echo esc_html($inv['notes_to_customer']); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($inv['can_pay'])): ?>
|
||||
<div class="ifnex-invoice-actions">
|
||||
<button type="button" class="ifnex-pay-btn" data-invoice-id="<?php echo (int)$inv['id']; ?>">💳 پرداخت از کیف پول</button>
|
||||
</div>
|
||||
<?php elseif ($inv['status'] === 'paid'): ?>
|
||||
<div style="margin-top:20px;padding:14px 16px;background:#ecfdf5;border:1px solid #10b981;border-radius:8px;color:#065f46;font-weight:600;">
|
||||
✅ این فاکتور پرداخت شده است.
|
||||
</div>
|
||||
<?php elseif ($inv['status'] === 'cancelled'): ?>
|
||||
<div style="margin-top:20px;padding:14px 16px;background:#fee2e2;border:1px solid #ef4444;border-radius:8px;color:#991b1b;font-weight:600;">
|
||||
❌ این فاکتور لغو شده است.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<script>
|
||||
jQuery(function($) {
|
||||
$('.ifnex-pay-btn').on('click', function() {
|
||||
var btn = $(this);
|
||||
var invoiceId = btn.data('invoice-id');
|
||||
if (!confirm('پرداخت این فاکتور از کیف پول شما انجام شود؟')) return;
|
||||
btn.prop('disabled', true).text('در حال پرداخت...');
|
||||
$.ajax({
|
||||
url: ifnex_ajax.ajax_url,
|
||||
method: 'POST',
|
||||
data: { action: 'ifnex_pay_invoice', nonce: ifnex_ajax.nonce, invoice_id: invoiceId },
|
||||
success: function(res) {
|
||||
if (res.success) {
|
||||
alert('✅ فاکتور با موفقیت پرداخت شد.');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ ' + (res.data || 'خطا در پرداخت.'));
|
||||
btn.prop('disabled', false).text('💳 پرداخت از کیف پول');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('❌ خطا در ارتباط با سرور.');
|
||||
btn.prop('disabled', false).text('💳 پرداخت از کیف پول');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php else: ?>
|
||||
<div class="ifnex-alert ifnex-alert-warning">فاکتور یافت نشد.</div>
|
||||
<?php endif; ?>
|
||||
<?php } ?>
|
||||
<?php } else { // نمایش لیست فاکتورها
|
||||
// فیلتر از URL
|
||||
$status_filter = isset($_GET['status']) ? sanitize_key($_GET['status']) : '';
|
||||
$invoices_url = '/customer/invoices?per_page=50';
|
||||
if (!empty($status_filter)) {
|
||||
$invoices_url .= '&status=' . $status_filter;
|
||||
}
|
||||
$list_result = $bridge->authenticated_request($user_id, $invoices_url, 'GET');
|
||||
$invoices = [];
|
||||
if (!is_wp_error($list_result) && isset($list_result['data']['invoices'])) {
|
||||
$invoices = $list_result['data']['invoices'];
|
||||
}
|
||||
$status_labels = [
|
||||
'pending_payment' => 'در انتظار پرداخت',
|
||||
'paid' => 'پرداخت شده',
|
||||
'cancelled' => 'لغو شده',
|
||||
'expired' => 'منقضی',
|
||||
];
|
||||
$status_classes = [
|
||||
'pending_payment' => 'pending',
|
||||
'paid' => 'paid',
|
||||
'cancelled' => 'cancelled',
|
||||
'expired' => 'expired',
|
||||
];
|
||||
?>
|
||||
<style>
|
||||
.ifnex-inv-filters {display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;}
|
||||
.ifnex-inv-filter-btn {padding:8px 16px;border-radius:8px;border:1px solid #e2e8f0;background:#fff;color:#475569;text-decoration:none;font-size:13px;font-weight:500;}
|
||||
.ifnex-inv-filter-btn.active {background:#1e3a5f;color:#fff;border-color:#1e3a5f;}
|
||||
.ifnex-inv-card {background:#fff;border-radius:12px;padding:18px;border:1px solid #e2e8f0;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center;transition:box-shadow 0.2s;}
|
||||
.ifnex-inv-card:hover {box-shadow:0 4px 12px rgba(0,0,0,0.08);}
|
||||
.ifnex-inv-card-meta {font-size:12px;color:#64748b;margin-top:4px;}
|
||||
.ifnex-inv-card-amount {font-size:18px;font-weight:700;color:#0f172a;}
|
||||
.ifnex-status-badge {padding:4px 12px;border-radius:999px;font-size:11px;font-weight:700;display:inline-block;}
|
||||
.ifnex-status-pending {background:#fef3c7;color:#92400e;}
|
||||
.ifnex-status-paid {background:#ecfdf5;color:#065f46;}
|
||||
.ifnex-status-cancelled {background:#fee2e2;color:#991b1b;}
|
||||
.ifnex-status-expired {background:#f1f5f9;color:#475569;}
|
||||
.ifnex-empty {text-align:center;padding:40px 20px;color:#64748b;}
|
||||
.ifnex-empty-icon {font-size:48px;margin-bottom:12px;}
|
||||
.ifnex-inv-actions {display:flex;gap:8px;}
|
||||
.ifnex-inv-btn {padding:8px 16px;border-radius:6px;text-decoration:none;font-size:13px;font-weight:600;}
|
||||
.ifnex-inv-btn-view {background:#e5e7eb;color:#1e293b;}
|
||||
.ifnex-inv-btn-pay {background:#10b981;color:#fff;border:none;cursor:pointer;}
|
||||
</style>
|
||||
<div class="ifnex-inv-filters">
|
||||
<a href="<?php echo esc_url(home_url('/my-account/?tab=invoices')); ?>" class="ifnex-inv-filter-btn <?php echo empty($status_filter) ? 'active' : ''; ?>">همه</a>
|
||||
<a href="<?php echo esc_url(home_url('/my-account/?tab=invoices&status=pending_payment')); ?>" class="ifnex-inv-filter-btn <?php echo $status_filter === 'pending_payment' ? 'active' : ''; ?>">در انتظار پرداخت</a>
|
||||
<a href="<?php echo esc_url(home_url('/my-account/?tab=invoices&status=paid')); ?>" class="ifnex-inv-filter-btn <?php echo $status_filter === 'paid' ? 'active' : ''; ?>">پرداخت شده</a>
|
||||
<a href="<?php echo esc_url(home_url('/my-account/?tab=invoices&status=cancelled')); ?>" class="ifnex-inv-filter-btn <?php echo $status_filter === 'cancelled' ? 'active' : ''; ?>">لغو شده</a>
|
||||
</div>
|
||||
<?php if (empty($invoices)): ?>
|
||||
<div class="ifnex-empty">
|
||||
<div class="ifnex-empty-icon">📄</div>
|
||||
<p>فاکتوری برای نمایش وجود ندارد.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($invoices as $inv):
|
||||
$inv_status = $inv['status'] ?? 'pending_payment';
|
||||
$status_label = $status_labels[$inv_status] ?? $inv_status;
|
||||
$status_class = $status_classes[$inv_status] ?? '';
|
||||
$items_count = is_array($inv['items'] ?? null) ? count($inv['items']) : 0;
|
||||
$detail_url = home_url('/my-account/?tab=invoices&invoice=' . $inv['id']);
|
||||
$can_pay = !empty($inv['can_pay']);
|
||||
?>
|
||||
<div class="ifnex-inv-card">
|
||||
<div>
|
||||
<div style="font-weight:700;color:#0f172a;font-size:15px;"><?php echo esc_html($inv['invoice_number']); ?></div>
|
||||
<div class="ifnex-inv-card-meta">
|
||||
<?php echo (int)$items_count; ?> قلم | <?php echo esc_html(ifnex_format_jalali_date($inv['created_at'])); ?>
|
||||
<?php if (!empty($inv['due_date'])): ?> | موعد: <?php echo esc_html($inv['due_date']); ?><?php endif; ?>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<span class="ifnex-status-badge ifnex-status-<?php echo esc_attr($status_class); ?>"><?php echo esc_html($status_label); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:left;">
|
||||
<div class="ifnex-inv-card-amount"><?php echo number_format((float)$inv['total_amount']); ?> <small style="font-size:11px;color:#64748b;font-weight:normal;">ریال</small></div>
|
||||
<div class="ifnex-inv-actions" style="margin-top:8px;">
|
||||
<a href="<?php echo esc_url($detail_url); ?>" class="ifnex-inv-btn ifnex-inv-btn-view">مشاهده</a>
|
||||
<?php if ($can_pay): ?>
|
||||
<button type="button" class="ifnex-inv-btn ifnex-inv-btn-pay ifnex-btn-pay-list" data-invoice-id="<?php echo (int)$inv['id']; ?>">پرداخت</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<script>
|
||||
jQuery(function($) {
|
||||
$('.ifnex-btn-pay-list').on('click', function() {
|
||||
var btn = $(this);
|
||||
var invoiceId = btn.data('invoice-id');
|
||||
if (!confirm('پرداخت این فاکتور از کیف پول شما انجام شود؟')) return;
|
||||
btn.prop('disabled', true).text('...');
|
||||
$.ajax({
|
||||
url: ifnex_ajax.ajax_url,
|
||||
method: 'POST',
|
||||
data: { action: 'ifnex_pay_invoice', nonce: ifnex_ajax.nonce, invoice_id: invoiceId },
|
||||
success: function(res) {
|
||||
if (res.success) {
|
||||
alert('✅ فاکتور با موفقیت پرداخت شد.');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ ' + (res.data || 'خطا در پرداخت.'));
|
||||
btn.prop('disabled', false).text('پرداخت');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('❌ خطا در ارتباط با سرور.');
|
||||
btn.prop('disabled', false).text('پرداخت');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php } ?>
|
||||
<?php elseif ($tab === 'wallet'): ?>
|
||||
<div class="ifnx-wallet-hero">
|
||||
<div class="ifnx-wallet-label">موجودی فعلی کیف پول</div>
|
||||
@ -1921,7 +2278,17 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
$action = $data['action_url'] ?? '';
|
||||
$created = $n['created_at'] ?? '';
|
||||
$n_type = $data['type'] ?? '';
|
||||
$icon_name = ($n_type === 'order_approved') ? 'check' : (($n_type === 'order_needs_changes') ? 'clock' : 'bell');
|
||||
$icon_name = match($n_type) {
|
||||
'order_approved' => 'check',
|
||||
'order_needs_changes' => 'clock',
|
||||
'manual_invoice_created' => 'invoice',
|
||||
default => 'bell',
|
||||
};
|
||||
$invoice_no = $data['invoice_number'] ?? '';
|
||||
// اگه action_url برای فاکتور نیست، خودکار به تب فاکتورها هدایت کن
|
||||
if ($n_type === 'manual_invoice_created' && empty($action)) {
|
||||
$action = '/my-account/?tab=invoices';
|
||||
}
|
||||
?>
|
||||
<div class="ifnex-notification <?php echo $is_unread ? 'unread' : ''; ?>">
|
||||
<div class="ifnex-notification-icon"><?php echo ifnex_icon($icon_name, 22); ?></div>
|
||||
@ -1947,4 +2314,149 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// فاکتورهای دستی (Manual Invoices)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* شورتکد لیست فاکتورهای دستی مشتری.
|
||||
* استفاده: [ifnex_invoices]
|
||||
*/
|
||||
add_shortcode('ifnex_invoices', 'ifnex_invoices_list_shortcode');
|
||||
function ifnex_invoices_list_shortcode($atts) {
|
||||
if (!is_user_logged_in()) {
|
||||
return '<div class="ifnex-alert ifnex-alert-warning">برای مشاهده فاکتورها باید وارد شوید.</div>';
|
||||
}
|
||||
|
||||
$bridge = new IFNEX_User_Bridge();
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// فراخوانی API برای دریافت فاکتورها
|
||||
$result = $bridge->authenticated_request($user_id, '/customer/invoices', 'GET');
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return '<div class="ifnex-alert ifnex-alert-danger">خطا در دریافت فاکتورها: ' . esc_html($result->get_error_message()) . '</div>';
|
||||
}
|
||||
|
||||
$invoices = $result['data']['invoices'] ?? [];
|
||||
$pagination = $result['data']['pagination'] ?? null;
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="ifnex-invoices-wrap">
|
||||
<h2 class="ifnex-section-title">فاکتورهای دستی</h2>
|
||||
|
||||
<?php if (empty($invoices)): ?>
|
||||
<div class="ifnex-empty-state">
|
||||
<div class="ifnex-empty-icon">📄</div>
|
||||
<p>فاکتور دستی برای شما ثبت نشده است.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="ifnex-invoices-list">
|
||||
<?php foreach ($invoices as $inv): ?>
|
||||
<?php
|
||||
$status_label = [
|
||||
'pending_payment' => 'در انتظار پرداخت',
|
||||
'paid' => 'پرداخت شده',
|
||||
'cancelled' => 'لغو شده',
|
||||
'expired' => 'منقضی',
|
||||
][$inv['status']] ?? $inv['status'];
|
||||
$status_class = [
|
||||
'pending_payment' => 'pending',
|
||||
'paid' => 'paid',
|
||||
'cancelled' => 'cancelled',
|
||||
'expired' => 'expired',
|
||||
][$inv['status']] ?? '';
|
||||
$items_count = is_array($inv['items'] ?? null) ? count($inv['items']) : 0;
|
||||
?>
|
||||
<div class="ifnex-invoice-card ifnex-status-<?php echo esc_attr($status_class); ?>">
|
||||
<div class="ifnex-invoice-header">
|
||||
<div>
|
||||
<div class="ifnex-invoice-number"><?php echo esc_html($inv['invoice_number']); ?></div>
|
||||
<div class="ifnex-invoice-date">صدور: <?php echo esc_html(ifnex_format_jalali_date($inv['created_at'])); ?></div>
|
||||
</div>
|
||||
<span class="ifnex-invoice-status ifnex-status-<?php echo esc_attr($status_class); ?>"><?php echo esc_html($status_label); ?></span>
|
||||
</div>
|
||||
<div class="ifnex-invoice-body">
|
||||
<div class="ifnex-invoice-row">
|
||||
<span>تعداد اقلام:</span>
|
||||
<strong><?php echo (int)$items_count; ?> قلم</strong>
|
||||
</div>
|
||||
<div class="ifnex-invoice-row">
|
||||
<span>مبلغ نهایی:</span>
|
||||
<strong class="ifnex-amount"><?php echo number_format((float)$inv['total_amount']); ?> ریال</strong>
|
||||
</div>
|
||||
<?php if (!empty($inv['due_date'])): ?>
|
||||
<div class="ifnex-invoice-row">
|
||||
<span>موعد پرداخت:</span>
|
||||
<strong><?php echo esc_html($inv['due_date']); ?></strong>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($inv['notes_to_customer'])): ?>
|
||||
<div class="ifnex-invoice-note"><?php echo esc_html($inv['notes_to_customer']); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="ifnex-invoice-actions">
|
||||
<a href="<?php echo esc_url(home_url('/my-account/?tab=invoices&invoice=' . $inv['id'])); ?>" class="ifnex-btn ifnex-btn-sm">جزئیات</a>
|
||||
<?php if (!empty($inv['can_pay'])): ?>
|
||||
<button type="button" class="ifnex-btn ifnex-btn-sm ifnex-btn-pay" data-invoice-id="<?php echo (int)$inv['id']; ?>">پرداخت از کیف پول</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<script>
|
||||
jQuery(function($) {
|
||||
$(document).on('click', '.ifnex-btn-pay', function() {
|
||||
var btn = $(this);
|
||||
var invoiceId = btn.data('invoice-id');
|
||||
if (!confirm('پرداخت این فاکتور از کیف پول شما انجام شود؟')) return;
|
||||
btn.prop('disabled', true).text('در حال پرداخت...');
|
||||
|
||||
$.ajax({
|
||||
url: ifnex_ajax.ajax_url,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'ifnex_pay_invoice',
|
||||
nonce: ifnex_ajax.nonce,
|
||||
invoice_id: invoiceId
|
||||
},
|
||||
success: function(res) {
|
||||
if (res.success) {
|
||||
alert('✅ فاکتور با موفقیت پرداخت شد.');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ ' + (res.data || 'خطا در پرداخت فاکتور.'));
|
||||
btn.prop('disabled', false).text('پرداخت از کیف پول');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('❌ خطا در ارتباط با سرور.');
|
||||
btn.prop('disabled', false).text('پرداخت از کیف پول');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* فرمت تاریخ شمسی (helper).
|
||||
*/
|
||||
if (!function_exists('ifnex_format_jalali_date')) {
|
||||
function ifnex_format_jalali_date($iso_date) {
|
||||
if (empty($iso_date)) return '';
|
||||
try {
|
||||
$ts = strtotime($iso_date);
|
||||
return jdate('Y/m/d H:i', $ts);
|
||||
} catch (\Throwable $e) {
|
||||
return $iso_date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1002,4 +1002,37 @@ function ifnex_upload_commitment_form_ajax() {
|
||||
if (!$result || !isset($result['success'])) wp_send_json_error('خطا در پردازش پاسخ سرور.');
|
||||
|
||||
wp_send_json_success($result['data']);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// AJAX: پرداخت فاکتور دستی از کیف پول
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
add_action('wp_ajax_ifnex_pay_invoice', 'ifnex_pay_invoice_ajax');
|
||||
function ifnex_pay_invoice_ajax() {
|
||||
check_ajax_referer('ifnex_ajax_nonce', 'nonce');
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error('باید وارد شوید.');
|
||||
}
|
||||
|
||||
$invoice_id = intval($_POST['invoice_id'] ?? 0);
|
||||
if (!$invoice_id) {
|
||||
wp_send_json_error('شناسه فاکتور نامعتبر است.');
|
||||
}
|
||||
|
||||
$bridge = new IFNEX_User_Bridge();
|
||||
$result = $bridge->authenticated_request(
|
||||
get_current_user_id(),
|
||||
"/customer/invoices/{$invoice_id}/pay",
|
||||
'POST'
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ enum TransactionType: string
|
||||
case ORDER_PAYMENT = 'order_payment'; // پرداخت سفارش
|
||||
case REFUND = 'refund'; // بازگشت وجه
|
||||
case CREDIT_SETTLEMENT = 'credit_settlement'; // تسویه اعتبار چندارزی از کیف پول
|
||||
case INVOICE_PAYMENT = 'invoice_payment'; // پرداخت فاکتور دستی از کیف پول
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
@ -18,6 +19,7 @@ enum TransactionType: string
|
||||
self::ORDER_PAYMENT => 'پرداخت سفارش',
|
||||
self::REFUND => 'بازگشت وجه',
|
||||
self::CREDIT_SETTLEMENT => 'تسویه اعتبار',
|
||||
self::INVOICE_PAYMENT => 'پرداخت فاکتور',
|
||||
};
|
||||
}
|
||||
|
||||
@ -29,6 +31,7 @@ enum TransactionType: string
|
||||
self::ORDER_PAYMENT => 'warning',
|
||||
self::REFUND => 'info',
|
||||
self::CREDIT_SETTLEMENT => 'info',
|
||||
self::INVOICE_PAYMENT => 'warning',
|
||||
};
|
||||
}
|
||||
}
|
||||
317
04_Laravel/app/Filament/Resources/ManualInvoiceResource.php
Normal file
317
04_Laravel/app/Filament/Resources/ManualInvoiceResource.php
Normal file
@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Models\ManualInvoice;
|
||||
use App\Models\User;
|
||||
use App\Services\ManualInvoiceService;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class ManualInvoiceResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ManualInvoice::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-document-text';
|
||||
protected static ?string $navigationLabel = 'فاکتورهای دستی';
|
||||
protected static ?string $navigationGroup = 'مالی';
|
||||
protected static ?int $navigationSort = 6;
|
||||
protected static ?string $slug = 'manual-invoices';
|
||||
protected static ?string $recordTitleAttribute = 'invoice_number';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
$isEdit = $form->getRecord() !== null;
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('اطلاعات فاکتور')
|
||||
->schema([
|
||||
Select::make('user_id')
|
||||
->label('مشتری')
|
||||
->options(fn () => User::role('customer')->pluck('name', 'id'))
|
||||
->searchable()
|
||||
->required()
|
||||
->disabled($isEdit)
|
||||
->helperText($isEdit ? 'مشتری بعد از صدور فاکتور قابل تغییر نیست.' : 'کاربری که فاکتور برایش صادر میشود.'),
|
||||
|
||||
TextInput::make('invoice_number')
|
||||
->label('شماره فاکتور')
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->hidden(fn ($record) => $record === null)
|
||||
->formatStateUsing(fn ($record) => $record?->invoice_number ?? '— تولید خودکار بعد از ذخیره —'),
|
||||
|
||||
DatePicker::make('due_date')
|
||||
->label('موعد پرداخت (اختیاری)')
|
||||
->minDate(now()->toDateString())
|
||||
->helperText('بعد از این تاریخ، فاکتور بهصورت خودکار منقضی میشود.'),
|
||||
])->columns(2),
|
||||
|
||||
Section::make('اقلام فاکتور')
|
||||
->description('اقلامی که به مشتری فاکتور میشوند. هر قلم: شرح، تعداد، قیمت واحد (ریال). اعداد بهصورت خودکار ۳ رقم جدا میشوند.')
|
||||
->schema([
|
||||
Repeater::make('items')
|
||||
->label('اقلام')
|
||||
->schema([
|
||||
TextInput::make('description')
|
||||
->label('شرح خدمت/کالا')
|
||||
->required()
|
||||
->columnSpan(2),
|
||||
TextInput::make('quantity')
|
||||
->label('تعداد')
|
||||
->numeric()
|
||||
->default(1)
|
||||
->minValue(0.01)
|
||||
->step(0.01)
|
||||
->required(),
|
||||
TextInput::make('unit_price')
|
||||
->label('قیمت واحد (ریال)')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->step(1)
|
||||
->required()
|
||||
->suffix('ریال')
|
||||
->formatStateUsing(fn ($state) => is_numeric($state) && $state > 0 ? number_format((float) $state, 0, '.', ',') : $state)
|
||||
->dehydrateStateUsing(fn ($state) => is_string($state) ? (float) str_replace(',', '', $state) : (float) $state)
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Forms\Set $set) {
|
||||
self::recalcTotals($get, $set);
|
||||
}),
|
||||
])
|
||||
->columns(4)
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state) => ($state['description'] ?? 'قلم جدید'))
|
||||
->defaultItems(1)
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Forms\Set $set) {
|
||||
self::recalcTotals($get, $set);
|
||||
})
|
||||
->deleteAction(fn ($action) => $action->after(fn (Get $get, Forms\Set $set) => self::recalcTotals($get, $set)))
|
||||
->reorderable(false),
|
||||
]),
|
||||
|
||||
Section::make('تخفیف و مبالغ نهایی')
|
||||
->schema([
|
||||
TextInput::make('discount_amount')
|
||||
->label('مبلغ تخفیف (ریال)')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->minValue(0)
|
||||
->step(1)
|
||||
->suffix('ریال')
|
||||
->formatStateUsing(fn ($state) => is_numeric($state) && $state > 0 ? number_format((float) $state, 0, '.', ',') : $state)
|
||||
->dehydrateStateUsing(fn ($state) => is_string($state) ? (float) str_replace(',', '', $state) : (float) $state)
|
||||
->live()
|
||||
->afterStateUpdated(fn (Get $get, Forms\Set $set) => self::recalcTotals($get, $set)),
|
||||
|
||||
Placeholder::make('subtotal_display')
|
||||
->label('مجموع اقلام (قبل از تخفیف)')
|
||||
->content(function (Get $get): HtmlString {
|
||||
$items = $get('items') ?? [];
|
||||
$subtotal = 0;
|
||||
foreach ($items as $item) {
|
||||
$rawQty = $item['quantity'] ?? 0;
|
||||
$rawUnit = $item['unit_price'] ?? 0;
|
||||
// پشتیبانی از اعداد با کاما (مثلاً "1,000,000")
|
||||
$qty = is_string($rawQty) ? (float) str_replace(',', '', $rawQty) : (float) $rawQty;
|
||||
$unit = is_string($rawUnit) ? (float) str_replace(',', '', $rawUnit) : (float) $rawUnit;
|
||||
$subtotal += $qty * $unit;
|
||||
}
|
||||
return new HtmlString('<strong style="font-size:16px">' . number_format($subtotal) . ' ریال</strong>');
|
||||
})
|
||||
->live(),
|
||||
|
||||
Placeholder::make('total_amount_display')
|
||||
->label('مبلغ نهایی قابل پرداخت')
|
||||
->content(function (Get $get): HtmlString {
|
||||
$items = $get('items') ?? [];
|
||||
$subtotal = 0;
|
||||
foreach ($items as $item) {
|
||||
$rawQty = $item['quantity'] ?? 0;
|
||||
$rawUnit = $item['unit_price'] ?? 0;
|
||||
// پشتیبانی از اعداد با کاما
|
||||
$qty = is_string($rawQty) ? (float) str_replace(',', '', $rawQty) : (float) $rawQty;
|
||||
$unit = is_string($rawUnit) ? (float) str_replace(',', '', $rawUnit) : (float) $rawUnit;
|
||||
$subtotal += $qty * $unit;
|
||||
}
|
||||
$rawDiscount = $get('discount_amount') ?? 0;
|
||||
$discount = is_string($rawDiscount) ? (float) str_replace(',', '', $rawDiscount) : (float) $rawDiscount;
|
||||
$total = max(0, $subtotal - $discount);
|
||||
return new HtmlString(
|
||||
'<strong style="font-size:18px;color:#059669">'
|
||||
. number_format($total)
|
||||
. ' ریال</strong>'
|
||||
);
|
||||
})
|
||||
->live(),
|
||||
])->columns(2),
|
||||
|
||||
Section::make('توضیحات')
|
||||
->schema([
|
||||
Textarea::make('notes_to_customer')
|
||||
->label('توضیحات برای مشتری')
|
||||
->rows(3)
|
||||
->helperText('این متن در پورتال مشتری نمایش داده میشود.'),
|
||||
Textarea::make('admin_notes')
|
||||
->label('یادداشت داخلی (نمایش داده نمیشود به مشتری)')
|
||||
->rows(2),
|
||||
])->columns(2),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('invoice_number')
|
||||
->label('شماره فاکتور')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('user.name')
|
||||
->label('مشتری')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('total_amount')
|
||||
->label('مبلغ نهایی')
|
||||
->formatStateUsing(fn ($state) => number_format($state) . ' ریال')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('items_count')
|
||||
->label('تعداد اقلام')
|
||||
->state(fn (ManualInvoice $record) => count($record->items ?? [])),
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->label('وضعیت')
|
||||
->badge()
|
||||
->formatStateUsing(fn ($state) => match($state) {
|
||||
'pending_payment' => 'در انتظار پرداخت',
|
||||
'paid' => 'پرداخت شده',
|
||||
'cancelled' => 'لغو شده',
|
||||
'expired' => 'منقضی',
|
||||
default => $state,
|
||||
})
|
||||
->color(fn ($state) => match($state) {
|
||||
'pending_payment' => 'warning',
|
||||
'paid' => 'success',
|
||||
'cancelled' => 'danger',
|
||||
'expired' => 'gray',
|
||||
default => 'gray',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('due_date')
|
||||
->label('موعد پرداخت')
|
||||
->date('Y-m-d')
|
||||
->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('paid_at')
|
||||
->label('تاریخ پرداخت')
|
||||
->dateTime('Y-m-d H:i')
|
||||
->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('issuedBy.name')
|
||||
->label('صادرکننده')
|
||||
->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('تاریخ صدور')
|
||||
->dateTime('Y-m-d H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('status')
|
||||
->label('وضعیت')
|
||||
->options([
|
||||
'pending_payment' => 'در انتظار پرداخت',
|
||||
'paid' => 'پرداخت شده',
|
||||
'cancelled' => 'لغو شده',
|
||||
'expired' => 'منقضی',
|
||||
]),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->visible(fn (ManualInvoice $record) => $record->isPending()),
|
||||
Action::make('cancel')
|
||||
->label('لغو فاکتور')
|
||||
->icon('heroicon-o-x-circle')
|
||||
->color('danger')
|
||||
->visible(fn (ManualInvoice $record) => $record->isPending())
|
||||
->requiresConfirmation()
|
||||
->form([
|
||||
Textarea::make('reason')
|
||||
->label('علت لغو (اختیاری)')
|
||||
->rows(2),
|
||||
])
|
||||
->action(function (ManualInvoice $record, array $data) {
|
||||
try {
|
||||
$service = app(ManualInvoiceService::class);
|
||||
$service->cancelInvoice($record, auth()->id(), $data['reason'] ?? null);
|
||||
Notification::make()
|
||||
->title('فاکتور لغو شد.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()
|
||||
->title('خطا در لغو فاکتور')
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
])
|
||||
->bulkActions([])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => \App\Filament\Resources\ManualInvoiceResource\Pages\ListManualInvoices::route('/'),
|
||||
'create' => \App\Filament\Resources\ManualInvoiceResource\Pages\CreateManualInvoice::route('/create'),
|
||||
'view' => \App\Filament\Resources\ManualInvoiceResource\Pages\ViewManualInvoice::route('/{record}'),
|
||||
'edit' => \App\Filament\Resources\ManualInvoiceResource\Pages\EditManualInvoice::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* محاسبه و تنظیم subtotal و total در فرم.
|
||||
*/
|
||||
public static function recalcTotals(Get $get, Forms\Set $set): void
|
||||
{
|
||||
$items = $get('items') ?? [];
|
||||
$subtotal = 0;
|
||||
foreach ($items as $item) {
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$unit = (float) ($item['unit_price'] ?? 0);
|
||||
$subtotal += $qty * $unit;
|
||||
}
|
||||
$discount = (float) ($get('discount_amount') ?? 0);
|
||||
$total = max(0, $subtotal - $discount);
|
||||
}
|
||||
|
||||
/**
|
||||
* بعد از ایجاد رکورد، شماره فاکتور رو تولید کن و ذخیره کن.
|
||||
*/
|
||||
public static function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
// محاسبه subtotal و total
|
||||
$totals = ManualInvoice::calculateTotal($data['items'] ?? [], (float) ($data['discount_amount'] ?? 0));
|
||||
$data['subtotal'] = $totals['subtotal'];
|
||||
$data['discount_amount'] = $totals['discount_amount'];
|
||||
$data['total_amount'] = $totals['total_amount'];
|
||||
$data['issued_by'] = auth()->id();
|
||||
$data['invoice_number'] = ManualInvoice::generateInvoiceNumber();
|
||||
$data['status'] = 'pending_payment';
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ManualInvoiceResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ManualInvoiceResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateManualInvoice extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ManualInvoiceResource::class;
|
||||
|
||||
protected static ?string $title = 'صدور فاکتور دستی';
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
return ManualInvoiceResource::mutateFormDataBeforeCreate($data);
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getCreatedNotificationTitle(): ?string
|
||||
{
|
||||
return 'فاکتور با موفقیت صادر شد.';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ManualInvoiceResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ManualInvoiceResource;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditManualInvoice extends EditRecord
|
||||
{
|
||||
protected static string $resource = ManualInvoiceResource::class;
|
||||
|
||||
protected static ?string $title = 'ویرایش فاکتور';
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// فقط اگه فاکتور pending_payment باشه قابل ویرایشه
|
||||
// محاسبه مجدد subtotal و total
|
||||
$totals = \App\Models\ManualInvoice::calculateTotal(
|
||||
$data['items'] ?? [],
|
||||
(float) ($data['discount_amount'] ?? 0)
|
||||
);
|
||||
$data['subtotal'] = $totals['subtotal'];
|
||||
$data['discount_amount'] = $totals['discount_amount'];
|
||||
$data['total_amount'] = $totals['total_amount'];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ManualInvoiceResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ManualInvoiceResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListManualInvoices extends ListRecords
|
||||
{
|
||||
protected static string $resource = ManualInvoiceResource::class;
|
||||
|
||||
protected static ?string $title = 'فاکتورهای دستی';
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
\Filament\Actions\CreateAction::make()
|
||||
->label('صدور فاکتور جدید'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ManualInvoiceResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ManualInvoiceResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewManualInvoice extends ViewRecord
|
||||
{
|
||||
protected static string $resource = ManualInvoiceResource::class;
|
||||
|
||||
protected static ?string $title = 'مشاهده فاکتور';
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Customer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ManualInvoice;
|
||||
use App\Services\ManualInvoiceService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* CustomerInvoiceController — API فاکتورهای دستی برای مشتری (در پورتال وردپرس)
|
||||
*
|
||||
* مسیرها:
|
||||
* GET /api/v1/customer/invoices — لیست فاکتورها
|
||||
* GET /api/v1/customer/invoices/{id} — جزئیات فاکتور
|
||||
* POST /api/v1/customer/invoices/{id}/pay — پرداخت از کیف پول
|
||||
*/
|
||||
class CustomerInvoiceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected ManualInvoiceService $invoiceService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* GET /api/v1/customer/invoices
|
||||
* لیست فاکتورهای مشتری با امکان فیلتر بر اساس وضعیت.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'status' => ['nullable', 'string', 'in:pending_payment,paid,cancelled,expired'],
|
||||
'per_page' => ['nullable', 'integer', 'min:10', 'max:100'],
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$invoices = $this->invoiceService->getCustomerInvoices($user, $validated);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'invoices' => $invoices->items(),
|
||||
'pagination' => [
|
||||
'current_page' => $invoices->currentPage(),
|
||||
'last_page' => $invoices->lastPage(),
|
||||
'per_page' => $invoices->perPage(),
|
||||
'total' => $invoices->total(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/customer/invoices/{id}
|
||||
* جزئیات یک فاکتور.
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$invoice = ManualInvoice::where('id', $id)
|
||||
->where('user_id', $user->id)
|
||||
->with(['issuedBy:id,name'])
|
||||
->first();
|
||||
|
||||
if (! $invoice) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'فاکتور یافت نشد.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $this->invoiceService->toArray($invoice),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/customer/invoices/{id}/pay
|
||||
* پرداخت فاکتور از کیف پول مشتری.
|
||||
*/
|
||||
public function pay(int $id): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$invoice = ManualInvoice::where('id', $id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (! $invoice) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'فاکتور یافت نشد.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$paid = $this->invoiceService->payFromWallet($invoice, $user->id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'فاکتور با موفقیت پرداخت شد.',
|
||||
'data' => $this->invoiceService->toArray($paid),
|
||||
]);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'خطای داخلی سرور هنگام پرداخت فاکتور.',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ use App\Models\Shipment;
|
||||
use App\Models\ShipmentItem;
|
||||
use App\Models\ShipmentPackage;
|
||||
use App\Models\Country;
|
||||
use App\Models\ManualInvoice;
|
||||
use App\Enums\ShipmentStatus;
|
||||
use App\Enums\ReviewState;
|
||||
use App\Models\ShipmentReview;
|
||||
@ -723,9 +724,21 @@ class CustomerOrderController extends Controller
|
||||
->where('status', ShipmentStatus::Approved)
|
||||
->sum('total_fee');
|
||||
|
||||
// مانده حساب = موجودی کیف پول - بدهیها
|
||||
// محاسبه مجموع فاکتورهای دستی در انتظار پرداخت
|
||||
$pending_invoices_total = ManualInvoice::where('user_id', $user->id)
|
||||
->where('status', 'pending_payment')
|
||||
->sum('total_amount');
|
||||
|
||||
$pending_invoices_count = ManualInvoice::where('user_id', $user->id)
|
||||
->where('status', 'pending_payment')
|
||||
->count();
|
||||
|
||||
// کل بدهی = بدهی سفارشات + بدهی فاکتورهای دستی
|
||||
$total_pending_payments = $pending_payments_total + $pending_invoices_total;
|
||||
|
||||
// مانده حساب = موجودی کیف پول - کل بدهیها
|
||||
$wallet_balance = $user->wallet ? $user->wallet->balance : 0;
|
||||
$account_balance = $wallet_balance - $pending_payments_total;
|
||||
$account_balance = $wallet_balance - $total_pending_payments;
|
||||
|
||||
// بدهیهای چندارزی (EUR/USD/AED/CNY) — به همان ارز خارجی، نه معادل ریالی
|
||||
$foreign_credits = app(\App\Services\CustomerCreditService::class)
|
||||
@ -741,6 +754,16 @@ class CustomerOrderController extends Controller
|
||||
->where('status', ShipmentStatus::Approved)
|
||||
->count(),
|
||||
'pending_payment_total' => $pending_payments_total,
|
||||
// ─── فاکتورهای دستی ───
|
||||
'pending_invoices_count' => $pending_invoices_count,
|
||||
'pending_invoices_total' => $pending_invoices_total,
|
||||
'paid_invoices_count' => ManualInvoice::where('user_id', $user->id)
|
||||
->where('status', 'paid')
|
||||
->count(),
|
||||
'paid_invoices_total' => ManualInvoice::where('user_id', $user->id)
|
||||
->where('status', 'paid')
|
||||
->sum('total_amount'),
|
||||
// ─── پایان فاکتورهای دستی ───
|
||||
'active_orders' => Shipment::where('user_id', $user->id)
|
||||
->whereIn('status', [
|
||||
ShipmentStatus::Processed,
|
||||
@ -773,7 +796,14 @@ class CustomerOrderController extends Controller
|
||||
'is_frozen' => $user->wallet->is_frozen,
|
||||
] : null,
|
||||
'account_balance' => $account_balance,
|
||||
'pending_payments_total' => $pending_payments_total,
|
||||
'pending_payments_total' => $total_pending_payments,
|
||||
// تفکیک برای نمایش در پورتال
|
||||
'pending_payments_breakdown' => [
|
||||
'orders' => $pending_payments_total,
|
||||
'invoices' => $pending_invoices_total,
|
||||
'total' => $total_pending_payments,
|
||||
],
|
||||
'pending_invoices_count' => $pending_invoices_count,
|
||||
'foreign_credits' => $foreign_credits,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
|
||||
161
04_Laravel/app/Models/ManualInvoice.php
Normal file
161
04_Laravel/app/Models/ManualInvoice.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use App\Traits\Auditable;
|
||||
|
||||
/**
|
||||
* ManualInvoice — فاکتور دستی صادر شده توسط مدیر
|
||||
*
|
||||
* سناریو:
|
||||
* - مشتری خدماتی دریافت کرده (مثلاً مشاوره تلفنی، انبارداری اضافی)
|
||||
* - مدیر برای او فاکتور صادر میکند با اقلام + مبالغ
|
||||
* - مشتری در پورتالش فاکتور را میبیند
|
||||
* - مشتری از کیف پول یا درگاه پرداخت میکند
|
||||
* - پس از پرداخت، مبلغ در تراکنشهای کیف پول ثبت میشود
|
||||
*
|
||||
* همه مبالغ به ریال ایران (IRR) ذخیره میشوند.
|
||||
*/
|
||||
class ManualInvoice extends Model
|
||||
{
|
||||
use Auditable;
|
||||
|
||||
protected $table = 'manual_invoices';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'issued_by',
|
||||
'invoice_number',
|
||||
'subtotal',
|
||||
'discount_amount',
|
||||
'total_amount',
|
||||
'items',
|
||||
'notes_to_customer',
|
||||
'admin_notes',
|
||||
'status',
|
||||
'due_date',
|
||||
'paid_at',
|
||||
'wallet_transaction_id',
|
||||
'payment_id',
|
||||
'cancelled_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'items' => 'array', // اقلام بهصورت JSON
|
||||
'subtotal' => 'decimal:2',
|
||||
'discount_amount' => 'decimal:2',
|
||||
'total_amount' => 'decimal:2',
|
||||
'due_date' => 'date',
|
||||
'paid_at' => 'datetime',
|
||||
];
|
||||
|
||||
// ─── Relationships ───────────────────────────────
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function issuedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'issued_by');
|
||||
}
|
||||
|
||||
public function cancelledBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'cancelled_by');
|
||||
}
|
||||
|
||||
public function walletTransaction(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WalletTransaction::class);
|
||||
}
|
||||
|
||||
public function payment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Payment::class);
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->status === 'pending_payment';
|
||||
}
|
||||
|
||||
public function isPaid(): bool
|
||||
{
|
||||
return $this->status === 'paid';
|
||||
}
|
||||
|
||||
public function isCancelled(): bool
|
||||
{
|
||||
return $this->status === 'cancelled';
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->status === 'expired';
|
||||
}
|
||||
|
||||
/**
|
||||
* آیا فاکتور منقضی شده است؟ (بررسی بر اساس due_date)
|
||||
*/
|
||||
public function checkExpired(): bool
|
||||
{
|
||||
if ($this->status !== 'pending_payment') {
|
||||
return false;
|
||||
}
|
||||
if (! $this->due_date) {
|
||||
return false;
|
||||
}
|
||||
return $this->due_date->isPast();
|
||||
}
|
||||
|
||||
/**
|
||||
* تولید شماره فاکتور بعدی.
|
||||
* فرمت: INV-2026-000001
|
||||
*/
|
||||
public static function generateInvoiceNumber(): string
|
||||
{
|
||||
$year = now()->year;
|
||||
$prefix = "INV-{$year}-";
|
||||
|
||||
$last = self::where('invoice_number', 'like', "{$prefix}%")
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$next = 1;
|
||||
if ($last) {
|
||||
$parts = explode('-', $last->invoice_number);
|
||||
$next = (int) end($parts) + 1;
|
||||
}
|
||||
|
||||
return $prefix . str_pad($next, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* محاسبه مبلغ نهایی بر اساس اقلام و تخفیف.
|
||||
*/
|
||||
public static function calculateTotal(array $items, float $discount = 0): array
|
||||
{
|
||||
$subtotal = 0;
|
||||
foreach ($items as $item) {
|
||||
$qty = (float) ($item['quantity'] ?? 1);
|
||||
$unit = (float) ($item['unit_price'] ?? 0);
|
||||
$subtotal += $qty * $unit;
|
||||
}
|
||||
|
||||
$discount = max(0, $discount);
|
||||
$total = max(0, $subtotal - $discount);
|
||||
|
||||
return [
|
||||
'subtotal' => round($subtotal, 2),
|
||||
'discount_amount' => round($discount, 2),
|
||||
'total_amount' => round($total, 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\ManualInvoice;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
/**
|
||||
* اعلان به مشتری: فاکتور دستی جدیدی برای شما صادر شده است.
|
||||
*
|
||||
* Trigger: ManualInvoiceService::createInvoice()
|
||||
*/
|
||||
class ManualInvoiceCreatedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public ManualInvoice $invoice,
|
||||
public ?User $issuedBy = null,
|
||||
) {}
|
||||
|
||||
public function via($notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'manual_invoice_created',
|
||||
'invoice_id' => $this->invoice->id,
|
||||
'invoice_number' => $this->invoice->invoice_number,
|
||||
'total_amount' => $this->invoice->total_amount,
|
||||
'due_date' => $this->invoice->due_date?->toDateString(),
|
||||
'message' => "فاکتور جدید {$this->invoice->invoice_number} به مبلغ "
|
||||
. number_format($this->invoice->total_amount) . " ریال برای شما صادر شد.",
|
||||
'action_url' => '/my-account/?tab=invoices',
|
||||
'issued_by' => $this->issuedBy?->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
291
04_Laravel/app/Services/ManualInvoiceService.php
Normal file
291
04_Laravel/app/Services/ManualInvoiceService.php
Normal file
@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ManualInvoice;
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletTransaction;
|
||||
use App\Notifications\ManualInvoiceCreatedNotification;
|
||||
use App\Enums\TransactionType;
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Enums\PaymentGateway;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* ManualInvoiceService — سرویس صدور و تسویه فاکتورهای دستی
|
||||
*
|
||||
* مسئولیتها:
|
||||
* 1. صدور فاکتور جدید توسط مدیر
|
||||
* 2. لغو فاکتور
|
||||
* 3. پرداخت فاکتور از کیف پول مشتری
|
||||
* 4. دریافت لیست فاکتورها برای مشتری/مدیر
|
||||
*
|
||||
* قاعده ارزی: همه مبالغ به ریال (IRR).
|
||||
* قاعده کیفپولی: پرداخت فاکتور از کیف پول مشتری کسر میشود (معادل هزینهای که مدیر گرفته).
|
||||
*/
|
||||
class ManualInvoiceService
|
||||
{
|
||||
/**
|
||||
* صدور فاکتور جدید.
|
||||
*
|
||||
* @param array $data {
|
||||
* @var int $user_id آیدی مشتری
|
||||
* @var int $issued_by آیدی مدیر/کارمند صادرکننده
|
||||
* @var array $items [{description, quantity, unit_price}, ...]
|
||||
* @var float $discount_amount مبلغ تخفیف (ریال)
|
||||
* @var string|null $notes_to_customer توضیحات برای مشتری
|
||||
* @var string|null $admin_notes یادداشت داخلی
|
||||
* @var string|null $due_date موعد پرداخت (Y-m-d)
|
||||
* }
|
||||
*/
|
||||
public function createInvoice(array $data): ManualInvoice
|
||||
{
|
||||
$this->validateCreateData($data);
|
||||
|
||||
$totals = ManualInvoice::calculateTotal($data['items'], $data['discount_amount'] ?? 0);
|
||||
|
||||
return DB::transaction(function () use ($data, $totals) {
|
||||
$invoice = ManualInvoice::create([
|
||||
'user_id' => $data['user_id'],
|
||||
'issued_by' => $data['issued_by'],
|
||||
'invoice_number' => ManualInvoice::generateInvoiceNumber(),
|
||||
'subtotal' => $totals['subtotal'],
|
||||
'discount_amount' => $totals['discount_amount'],
|
||||
'total_amount' => $totals['total_amount'],
|
||||
'items' => $data['items'],
|
||||
'notes_to_customer' => $data['notes_to_customer'] ?? null,
|
||||
'admin_notes' => $data['admin_notes'] ?? null,
|
||||
'due_date' => $data['due_date'] ?? null,
|
||||
'status' => 'pending_payment',
|
||||
]);
|
||||
|
||||
// ارسال اعلان به مشتری
|
||||
$customer = User::find($data['user_id']);
|
||||
$issuedBy = User::find($data['issued_by']);
|
||||
if ($customer) {
|
||||
try {
|
||||
$customer->notify(new ManualInvoiceCreatedNotification($invoice, $issuedBy));
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('ManualInvoice: failed to send notification: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $invoice;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* لغو فاکتور توسط مدیر.
|
||||
*/
|
||||
public function cancelInvoice(ManualInvoice $invoice, int $cancelledBy, ?string $reason = null): ManualInvoice
|
||||
{
|
||||
if (! $invoice->isPending()) {
|
||||
throw new RuntimeException(
|
||||
"این فاکتور قابل لغو نیست. وضعیت فعلی: {$invoice->status}"
|
||||
);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($invoice, $cancelledBy, $reason) {
|
||||
$invoice->update([
|
||||
'status' => 'cancelled',
|
||||
'cancelled_by' => $cancelledBy,
|
||||
'admin_notes' => $reason
|
||||
? ($invoice->admin_notes ? $invoice->admin_notes . "\n\n[علت لغو]: " . $reason : "[علت لغو]: " . $reason)
|
||||
: $invoice->admin_notes,
|
||||
]);
|
||||
|
||||
return $invoice->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* پرداخت فاکتور از کیف پول مشتری.
|
||||
*
|
||||
* @param ManualInvoice $invoice
|
||||
* @param int $paidBy آیدی کاربر که پرداخت رو انجام میده (معمولاً مشتری)
|
||||
* @return ManualInvoice
|
||||
* @throws RuntimeException اگر فاکتور قابل پرداخت نباشد یا موجودی ناکافی باشد
|
||||
*/
|
||||
public function payFromWallet(ManualInvoice $invoice, int $paidBy): ManualInvoice
|
||||
{
|
||||
if (! $invoice->isPending()) {
|
||||
throw new RuntimeException("این فاکتور قابل پرداخت نیست. وضعیت: {$invoice->status}");
|
||||
}
|
||||
|
||||
if ($invoice->checkExpired()) {
|
||||
throw new RuntimeException('این فاکتور منقضی شده است.');
|
||||
}
|
||||
|
||||
$amount = (float) $invoice->total_amount;
|
||||
if ($amount <= 0) {
|
||||
throw new RuntimeException('مبلغ فاکتور صفر یا منفی است.');
|
||||
}
|
||||
|
||||
$wallet = Wallet::where('user_id', $invoice->user_id)->first();
|
||||
if (! $wallet) {
|
||||
throw new RuntimeException('مشتری کیف پول ندارد.');
|
||||
}
|
||||
if ($wallet->isFrozen()) {
|
||||
throw new RuntimeException('کیف پول مشتری مسدود است.');
|
||||
}
|
||||
if (! $wallet->hasSufficientBalance($amount)) {
|
||||
throw new RuntimeException(
|
||||
'موجودی کیف پول کافی نیست. موجودی: ' . number_format($wallet->balance)
|
||||
. ' | نیاز: ' . number_format($amount) . ' ریال'
|
||||
);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($invoice, $paidBy, $amount, $wallet) {
|
||||
// قفل کیف پول برای جلوگیری از race condition
|
||||
$wallet = Wallet::lockForUpdate()->find($wallet->id);
|
||||
|
||||
// بررسی مجدد موجودی
|
||||
if (! $wallet->hasSufficientBalance($amount)) {
|
||||
throw new RuntimeException('موجودی کیف پول تغییر کرده — مجدداً تلاش کنید.');
|
||||
}
|
||||
|
||||
// ثبت تراکنش کسر از کیف پول
|
||||
$transaction = WalletTransaction::create([
|
||||
'wallet_id' => $wallet->id,
|
||||
'amount' => -$amount,
|
||||
'balance_before' => $wallet->balance,
|
||||
'balance_after' => $wallet->balance - $amount,
|
||||
'type' => TransactionType::INVOICE_PAYMENT->value,
|
||||
'status' => TransactionStatus::COMPLETED,
|
||||
'gateway' => PaymentGateway::WALLET,
|
||||
'description' => "پرداخت فاکتور {$invoice->invoice_number} از کیف پول",
|
||||
'transactionable_type' => ManualInvoice::class,
|
||||
'transactionable_id' => $invoice->id,
|
||||
'created_by' => $paidBy,
|
||||
]);
|
||||
|
||||
// کسر مبلغ از کیف پول
|
||||
$wallet->update([
|
||||
'balance' => $wallet->balance - $amount,
|
||||
'total_withdrawn' => $wallet->total_withdrawn + $amount,
|
||||
]);
|
||||
|
||||
// بهروزرسانی فاکتور
|
||||
$invoice->update([
|
||||
'status' => 'paid',
|
||||
'paid_at' => now(),
|
||||
'wallet_transaction_id' => $transaction->id,
|
||||
]);
|
||||
|
||||
return $invoice->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* لیست فاکتورهای مشتری (برای نمایش در پورتال).
|
||||
*
|
||||
* @param User $customer
|
||||
* @param array $filters {status?, per_page?}
|
||||
*/
|
||||
public function getCustomerInvoices(User $customer, array $filters = [])
|
||||
{
|
||||
$query = ManualInvoice::where('user_id', $customer->id)
|
||||
->with(['issuedBy:id,name'])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if (! empty($filters['status'])) {
|
||||
$query->where('status', $filters['status']);
|
||||
}
|
||||
|
||||
$perPage = min(100, max(10, (int) ($filters['per_page'] ?? 20)));
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* لیست همه فاکتورها (برای پنل ادمین).
|
||||
*/
|
||||
public function getAllInvoices(array $filters = [])
|
||||
{
|
||||
$query = ManualInvoice::with(['user:id,name', 'issuedBy:id,name'])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if (! empty($filters['status'])) {
|
||||
$query->where('status', $filters['status']);
|
||||
}
|
||||
if (! empty($filters['user_id'])) {
|
||||
$query->where('user_id', $filters['user_id']);
|
||||
}
|
||||
|
||||
$perPage = min(100, max(10, (int) ($filters['per_page'] ?? 20)));
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* تبدیل فاکتور به آرایه نمایشی (برای API).
|
||||
*/
|
||||
public function toArray(ManualInvoice $invoice): array
|
||||
{
|
||||
return [
|
||||
'id' => $invoice->id,
|
||||
'invoice_number' => $invoice->invoice_number,
|
||||
'user' => $invoice->user ? [
|
||||
'id' => $invoice->user->id,
|
||||
'name' => $invoice->user->name,
|
||||
] : null,
|
||||
'issued_by' => $invoice->issuedBy ? [
|
||||
'id' => $invoice->issuedBy->id,
|
||||
'name' => $invoice->issuedBy->name,
|
||||
] : null,
|
||||
'items' => $invoice->items ?? [],
|
||||
'subtotal' => (float) $invoice->subtotal,
|
||||
'discount_amount' => (float) $invoice->discount_amount,
|
||||
'total_amount' => (float) $invoice->total_amount,
|
||||
'notes_to_customer' => $invoice->notes_to_customer,
|
||||
'status' => $invoice->status,
|
||||
'due_date' => $invoice->due_date?->toDateString(),
|
||||
'paid_at' => $invoice->paid_at?->toIso8601String(),
|
||||
'created_at' => $invoice->created_at->toIso8601String(),
|
||||
'can_pay' => $invoice->isPending() && ! $invoice->checkExpired(),
|
||||
'is_expired' => $invoice->checkExpired(),
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Validation ───────────────────────────────────
|
||||
|
||||
protected function validateCreateData(array $data): void
|
||||
{
|
||||
if (empty($data['user_id'])) {
|
||||
throw new RuntimeException('انتخاب مشتری الزامی است.');
|
||||
}
|
||||
if (empty($data['issued_by'])) {
|
||||
throw new RuntimeException('صادرکننده فاکتور مشخص نیست.');
|
||||
}
|
||||
if (empty($data['items']) || ! is_array($data['items'])) {
|
||||
throw new RuntimeException('حداقل یک قلم الزامی است.');
|
||||
}
|
||||
foreach ($data['items'] as $i => $item) {
|
||||
if (empty($item['description'])) {
|
||||
throw new RuntimeException("شرح قلم شماره " . ($i + 1) . " الزامی است.");
|
||||
}
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$unit = (float) ($item['unit_price'] ?? 0);
|
||||
if ($qty <= 0) {
|
||||
throw new RuntimeException("تعداد قلم شماره " . ($i + 1) . " باید مثبت باشد.");
|
||||
}
|
||||
if ($unit < 0) {
|
||||
throw new RuntimeException("قیمت واحد قلم شماره " . ($i + 1) . " نمیتواند منفی باشد.");
|
||||
}
|
||||
}
|
||||
if ((float) ($data['discount_amount'] ?? 0) < 0) {
|
||||
throw new RuntimeException('مبلغ تخفیف نمیتواند منفی باشد.');
|
||||
}
|
||||
|
||||
// بررسی اینکه کاربر مشتری هست
|
||||
$user = User::find($data['user_id']);
|
||||
if (! $user) {
|
||||
throw new RuntimeException('مشتری یافت نشد.');
|
||||
}
|
||||
if (! $user->hasRole('customer')) {
|
||||
throw new RuntimeException('کاربر انتخابی نقش مشتری ندارد.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Migration: ایجاد جدول فاکتورهای دستی مدیر
|
||||
*
|
||||
* فاکتور دستی = سندی که مدیر/کارمند برای مشتری صادر میکند تا هزینههای خارج از سفارش
|
||||
* (خدمات تلفنی، هزینههای جانبی، انبارداری اضافی و...) را از مشتری دریافت کند.
|
||||
*
|
||||
* مشتری فاکتور را در پورتالش میبیند و از کیف پول یا درگاه پرداخت میکند.
|
||||
*
|
||||
* جریان:
|
||||
* 1. مدیر فاکتور صادر میکند → status = 'pending_payment'
|
||||
* 2. مشتری در پورتالش میبیند
|
||||
* 3. مشتری از کیف پول پرداخت میکند → status = 'paid'
|
||||
* 4. مبلغ از کیف پول مشتری کسر و در تراکنشها ثبت میشود
|
||||
* 5. در صورت لغو توسط مدیر → status = 'cancelled'
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('manual_invoices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// مشتری و صادرکننده — ستونها اول ایجاد میشوند، foreign keys بعد از ایجاد جدول اضافه میشوند
|
||||
$table->unsignedBigInteger('user_id')->comment('مشتری که فاکتور برایش صادر شده');
|
||||
$table->unsignedBigInteger('issued_by')->nullable()->comment('مدیر یا کارمندی که فاکتور را صادر کرده');
|
||||
|
||||
// شماره فاکتور (ترتیبی)
|
||||
$table->string('invoice_number', 50)->unique()
|
||||
->comment('شماره فاکتور قابل نمایش: INV-YYYY-XXXXXX');
|
||||
|
||||
// مبالغ (همگی به ریال ایران)
|
||||
$table->decimal('subtotal', 20, 2)->default(0)
|
||||
->comment('مجموع اقلام قبل از تخفیف');
|
||||
$table->decimal('discount_amount', 20, 2)->default(0)
|
||||
->comment('مبلغ تخفیف (ریال)');
|
||||
$table->decimal('total_amount', 20, 2)->default(0)
|
||||
->comment('مبلغ نهایی قابل پرداخت (ریال) = subtotal - discount');
|
||||
|
||||
// اقلام فاکتور (JSON آرایه)
|
||||
$table->json('items')
|
||||
->comment('اقلام فاکتور بهصورت JSON');
|
||||
|
||||
// توضیحات
|
||||
$table->text('notes_to_customer')->nullable()
|
||||
->comment('توضیحات نمایش داده شده به مشتری');
|
||||
$table->text('admin_notes')->nullable()
|
||||
->comment('یادداشت داخلی ادمین (برای مشتری نمایش داده نمیشود)');
|
||||
|
||||
// وضعیت
|
||||
$table->enum('status', [
|
||||
'pending_payment',
|
||||
'paid',
|
||||
'cancelled',
|
||||
'expired',
|
||||
])->default('pending_payment');
|
||||
|
||||
// موعد پرداخت (اختیاری)
|
||||
$table->date('due_date')->nullable()
|
||||
->comment('موعد پرداخت (اختیاری)');
|
||||
|
||||
// پرداخت
|
||||
$table->timestamp('paid_at')->nullable();
|
||||
$table->unsignedBigInteger('wallet_transaction_id')->nullable();
|
||||
$table->unsignedBigInteger('payment_id')->nullable();
|
||||
$table->unsignedBigInteger('cancelled_by')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// ایندکسها
|
||||
$table->index(['user_id', 'status']);
|
||||
$table->index('issued_by');
|
||||
$table->index('due_date');
|
||||
$table->index('status');
|
||||
});
|
||||
|
||||
// اضافهکردن foreign keys بهصورت جداگانه (روش امنتر، جلوگیری از errno 150)
|
||||
Schema::table('manual_invoices', function (Blueprint $table) {
|
||||
$table->foreign('user_id')
|
||||
->references('id')
|
||||
->on('users')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreign('issued_by')
|
||||
->references('id')
|
||||
->on('users')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->foreign('cancelled_by')
|
||||
->references('id')
|
||||
->on('users')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->foreign('wallet_transaction_id')
|
||||
->references('id')
|
||||
->on('wallet_transactions')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->foreign('payment_id')
|
||||
->references('id')
|
||||
->on('payments')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('manual_invoices');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Migration: افزودن 'invoice_payment' به enum type در wallet_transactions
|
||||
*
|
||||
* برای پشتیبانی از فاکتورهای دستی مدیر، یک نوع تراکنش جدید اضافه میکنیم:
|
||||
* 'invoice_payment' — کسر مبلغ فاکتور از کیف پول مشتری.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// MySQL: ALTER COLUMN بهروزرسانی enum
|
||||
// مقادیر جدید شامل همه موارد قبلی + 'invoice_payment'
|
||||
DB::statement("
|
||||
ALTER TABLE wallet_transactions
|
||||
MODIFY COLUMN type ENUM(
|
||||
'deposit',
|
||||
'withdrawal',
|
||||
'order_payment',
|
||||
'refund',
|
||||
'credit_settlement',
|
||||
'invoice_payment'
|
||||
) NOT NULL
|
||||
COMMENT 'نوع تراکنش'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// بازگشت به حالت قبلی — فقط اگه هیچ رکورد با type='invoice_payment' نباشد
|
||||
$count = DB::table('wallet_transactions')
|
||||
->where('type', 'invoice_payment')
|
||||
->count();
|
||||
if ($count > 0) {
|
||||
throw new RuntimeException(
|
||||
"Cannot rollback: {$count} wallet transactions have type='invoice_payment'. "
|
||||
. "Delete or reclassify them first."
|
||||
);
|
||||
}
|
||||
|
||||
DB::statement("
|
||||
ALTER TABLE wallet_transactions
|
||||
MODIFY COLUMN type ENUM(
|
||||
'deposit',
|
||||
'withdrawal',
|
||||
'order_payment',
|
||||
'refund',
|
||||
'credit_settlement'
|
||||
) NOT NULL
|
||||
COMMENT 'نوع تراکنش'
|
||||
");
|
||||
}
|
||||
};
|
||||
@ -83,10 +83,16 @@ Route::middleware(['auth:sanctum', 'throttle:customer'])->prefix('v1')->group(fu
|
||||
Route::prefix('customer')->group(function () {
|
||||
// پروفایل و آمار
|
||||
Route::get('/profile', [CustomerOrderController::class, 'profile']);
|
||||
|
||||
|
||||
// فاکتورهای دستی (صدور شده توسط مدیر)
|
||||
Route::get('/invoices', [\App\Http\Controllers\Api\Customer\CustomerInvoiceController::class, 'index']);
|
||||
Route::get('/invoices/{invoice}', [\App\Http\Controllers\Api\Customer\CustomerInvoiceController::class, 'show']);
|
||||
Route::post('/invoices/{invoice}/pay', [\App\Http\Controllers\Api\Customer\CustomerInvoiceController::class, 'pay'])
|
||||
->middleware('throttle:wallet');
|
||||
|
||||
// لیست کشورها (عمومی - بدون نیاز به احراز هویت)
|
||||
// این endpoint در گروه auth:sanctum نیست
|
||||
|
||||
|
||||
// سفارشات
|
||||
Route::get('/orders', [CustomerOrderController::class, 'index']);
|
||||
Route::post('/orders', [CustomerOrderController::class, 'store']);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user