From 7fe368bc46d15823892a260a3eedd751b69d88fe Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Thu, 29 Oct 2026 23:59:19 +0330 Subject: [PATCH] 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. --- .../ifnex-bridge/includes/shortcodes.php | 530 +++++++++++++++++- .../ifnex-bridge/includes/user-bridge.php | 35 +- 04_Laravel/app/Enums/TransactionType.php | 3 + .../Resources/ManualInvoiceResource.php | 317 +++++++++++ .../Pages/CreateManualInvoice.php | 28 + .../Pages/EditManualInvoice.php | 33 ++ .../Pages/ListManualInvoices.php | 21 + .../Pages/ViewManualInvoice.php | 13 + .../Customer/CustomerInvoiceController.php | 118 ++++ .../Api/Customer/CustomerOrderController.php | 36 +- 04_Laravel/app/Models/ManualInvoice.php | 161 ++++++ .../ManualInvoiceCreatedNotification.php | 43 ++ .../app/Services/ManualInvoiceService.php | 291 ++++++++++ ...29_150000_create_manual_invoices_table.php | 117 ++++ ...ce_payment_to_wallet_transactions_type.php | 60 ++ 04_Laravel/routes/api.php | 10 +- 16 files changed, 1801 insertions(+), 15 deletions(-) create mode 100644 04_Laravel/app/Filament/Resources/ManualInvoiceResource.php create mode 100644 04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/CreateManualInvoice.php create mode 100644 04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/EditManualInvoice.php create mode 100644 04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/ListManualInvoices.php create mode 100644 04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/ViewManualInvoice.php create mode 100644 04_Laravel/app/Http/Controllers/Api/Customer/CustomerInvoiceController.php create mode 100644 04_Laravel/app/Models/ManualInvoice.php create mode 100644 04_Laravel/app/Notifications/ManualInvoiceCreatedNotification.php create mode 100644 04_Laravel/app/Services/ManualInvoiceService.php create mode 100644 04_Laravel/database/migrations/2026_10_29_150000_create_manual_invoices_table.php create mode 100644 04_Laravel/database/migrations/2026_10_29_160000_add_invoice_payment_to_wallet_transactions_type.php diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php index d57484b..a1278d3 100644 --- a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php +++ b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php @@ -246,13 +246,37 @@ function ifnex_user_profile_shortcode($atts) {
ریال
- 0): ?> -
- ⚠️ بدهی در انتظار پرداخت: ریال + + + + 0): ?> +
+ ⚠️ مجموع بدهی در انتظار پرداخت: ریال
+
- + +
@@ -331,7 +355,7 @@ function ifnex_user_profile_shortcode($atts) {
-
در انتظار پرداخت
+
در انتظار پرداخت (سفارش)
@@ -342,6 +366,38 @@ function ifnex_user_profile_shortcode($atts) {
تحویل شده
+ + + 0 || $inv_paid_count > 0): ?> +
+
+
+
فاکتور در انتظار پرداخت
+
+ ریال +
+
+
+
+
فاکتور پرداخت‌شده
+
+ ریال +
+
+
+

+ + 📋 مشاهده همه فاکتورها ← + +

+ + 0): ?>

مجموع خرید: @@ -1616,6 +1672,7 @@ function ifnex_icon($name, $size = 20) { 'check' => '', 'clock' => '', 'bell' => '', + 'invoice' => '', ]; $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) { 0): ?> + 0): ?> + +

@@ -1744,7 +1813,19 @@ function ifnex_customer_dashboard_shortcode($atts) {
- در انتظار پرداخت + در انتظار پرداخت (سفارش) +
+ +
+
+
+ + + فاکتور دستی در انتظار پرداخت + 0): ?> + مشاهده ← + +
@@ -1792,6 +1873,282 @@ function ifnex_customer_dashboard_shortcode($atts) { + + 0) { + // دریافت جزئیات فاکتور از API + $single_result = $bridge->authenticated_request($user_id, "/customer/invoices/{$single_invoice_id}", 'GET'); + if (is_wp_error($single_result)) { + echo '
خطا در دریافت فاکتور: ' . esc_html($single_result->get_error_message()) . '
'; + } 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); + ?> + +
+
+
+

فاکتور

+ +
+ → بازگشت به لیست +
+ +
+
صادرکننده:
+
تاریخ صدور:
+ +
موعد پرداخت:
+ + +
تاریخ پرداخت:
+ +
+ + + + + + + + + + + + + + $item): ?> + + + + + + + + + + +
#شرح خدمت/کالاتعدادقیمت واحد (ریال)مجموع (ریال)
+ + +
+
مجموع اقلام: ریال
+ 0): ?> +
تخفیف:- ریال
+ +
مبلغ قابل پرداخت: ریال
+
+ + +
+ + + +
+ +
+ +
+ ✅ این فاکتور پرداخت شده است. +
+ +
+ ❌ این فاکتور لغو شده است. +
+ +
+ + +
فاکتور یافت نشد.
+ + + 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', + ]; + ?> + + + +
+
📄
+

فاکتوری برای نمایش وجود ندارد.

+
+ + +
+
+
+
+ قلم | + | موعد: +
+
+ +
+
+
+
ریال
+
+ مشاهده + + + +
+
+
+ + + +
موجودی فعلی کیف پول
@@ -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'; + } ?>
@@ -1947,4 +2314,149 @@ function ifnex_customer_dashboard_shortcode($atts) {
برای مشاهده فاکتورها باید وارد شوید.
'; + } + + $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 '
خطا در دریافت فاکتورها: ' . esc_html($result->get_error_message()) . '
'; + } + + $invoices = $result['data']['invoices'] ?? []; + $pagination = $result['data']['pagination'] ?? null; + + ob_start(); + ?> +
+

فاکتورهای دستی

+ + +
+
📄
+

فاکتور دستی برای شما ثبت نشده است.

+
+ +
+ + 'در انتظار پرداخت', + '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; + ?> +
+
+
+
+
صدور:
+
+ +
+
+
+ تعداد اقلام: + قلم +
+
+ مبلغ نهایی: + ریال +
+ +
+ موعد پرداخت: + +
+ + +
+ +
+
+ جزئیات + + + +
+
+ +
+ +
+ + 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); +} diff --git a/04_Laravel/app/Enums/TransactionType.php b/04_Laravel/app/Enums/TransactionType.php index f6217e6..1f8922e 100644 --- a/04_Laravel/app/Enums/TransactionType.php +++ b/04_Laravel/app/Enums/TransactionType.php @@ -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', }; } } \ No newline at end of file diff --git a/04_Laravel/app/Filament/Resources/ManualInvoiceResource.php b/04_Laravel/app/Filament/Resources/ManualInvoiceResource.php new file mode 100644 index 0000000..75a3728 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ManualInvoiceResource.php @@ -0,0 +1,317 @@ +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('' . number_format($subtotal) . ' ریال'); + }) + ->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( + '' + . number_format($total) + . ' ریال' + ); + }) + ->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; + } +} diff --git a/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/CreateManualInvoice.php b/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/CreateManualInvoice.php new file mode 100644 index 0000000..b54b136 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/CreateManualInvoice.php @@ -0,0 +1,28 @@ +getResource()::getUrl('index'); + } + + protected function getCreatedNotificationTitle(): ?string + { + return 'فاکتور با موفقیت صادر شد.'; + } +} diff --git a/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/EditManualInvoice.php b/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/EditManualInvoice.php new file mode 100644 index 0000000..b11eeeb --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/EditManualInvoice.php @@ -0,0 +1,33 @@ +getResource()::getUrl('index'); + } +} diff --git a/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/ListManualInvoices.php b/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/ListManualInvoices.php new file mode 100644 index 0000000..684fbd5 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/ListManualInvoices.php @@ -0,0 +1,21 @@ +label('صدور فاکتور جدید'), + ]; + } +} diff --git a/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/ViewManualInvoice.php b/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/ViewManualInvoice.php new file mode 100644 index 0000000..130bef3 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ManualInvoiceResource/Pages/ViewManualInvoice.php @@ -0,0 +1,13 @@ +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); + } + } +} diff --git a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php index 2cfb245..3c99f1a 100644 --- a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php +++ b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php @@ -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, ]); diff --git a/04_Laravel/app/Models/ManualInvoice.php b/04_Laravel/app/Models/ManualInvoice.php new file mode 100644 index 0000000..70fbfdf --- /dev/null +++ b/04_Laravel/app/Models/ManualInvoice.php @@ -0,0 +1,161 @@ + '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), + ]; + } +} diff --git a/04_Laravel/app/Notifications/ManualInvoiceCreatedNotification.php b/04_Laravel/app/Notifications/ManualInvoiceCreatedNotification.php new file mode 100644 index 0000000..d04dcee --- /dev/null +++ b/04_Laravel/app/Notifications/ManualInvoiceCreatedNotification.php @@ -0,0 +1,43 @@ + '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, + ]; + } +} diff --git a/04_Laravel/app/Services/ManualInvoiceService.php b/04_Laravel/app/Services/ManualInvoiceService.php new file mode 100644 index 0000000..25b6565 --- /dev/null +++ b/04_Laravel/app/Services/ManualInvoiceService.php @@ -0,0 +1,291 @@ +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('کاربر انتخابی نقش مشتری ندارد.'); + } + } +} diff --git a/04_Laravel/database/migrations/2026_10_29_150000_create_manual_invoices_table.php b/04_Laravel/database/migrations/2026_10_29_150000_create_manual_invoices_table.php new file mode 100644 index 0000000..3e0d009 --- /dev/null +++ b/04_Laravel/database/migrations/2026_10_29_150000_create_manual_invoices_table.php @@ -0,0 +1,117 @@ +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'); + } +}; \ No newline at end of file diff --git a/04_Laravel/database/migrations/2026_10_29_160000_add_invoice_payment_to_wallet_transactions_type.php b/04_Laravel/database/migrations/2026_10_29_160000_add_invoice_payment_to_wallet_transactions_type.php new file mode 100644 index 0000000..172883a --- /dev/null +++ b/04_Laravel/database/migrations/2026_10_29_160000_add_invoice_payment_to_wallet_transactions_type.php @@ -0,0 +1,60 @@ +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 'نوع تراکنش' + "); + } +}; diff --git a/04_Laravel/routes/api.php b/04_Laravel/routes/api.php index 0060ad5..151e676 100644 --- a/04_Laravel/routes/api.php +++ b/04_Laravel/routes/api.php @@ -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']);