ifnex/04_Laravel/app/Models/ManualInvoice.php
Kazem Alghasi 7fe368bc46 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.
2026-10-29 23:59:19 +03:30

162 lines
4.4 KiB
PHP

<?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),
];
}
}