Files
alrahma_sunday_school_api/app/Models/Invoice.php
T
2026-03-08 16:33:24 -04:00

323 lines
9.9 KiB
PHP
Executable File

<?php
namespace App\Models;
use App\Models\BaseModel;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class Invoice extends BaseModel
{
protected $table = 'invoices';
/**
* CI: useTimestamps = false because DB defaults/triggers manage created_at/updated_at.
* In Laravel, keep timestamps OFF and let DB handle them.
*/
public $timestamps = true;
protected $fillable = [
'parent_id',
'invoice_number',
'total_amount',
'balance',
'paid_amount',
'has_discount',
'issue_date',
'due_date',
'status',
'description',
'school_year',
'created_at',
'updated_at',
'updated_by',
'semester',
];
protected $casts = [
'parent_id' => 'integer',
'updated_by' => 'integer',
'has_discount' => 'boolean',
'total_amount' => 'decimal:2',
'balance' => 'decimal:2',
'paid_amount' => 'decimal:2',
'issue_date' => 'datetime',
'due_date' => 'datetime',
// created_at/updated_at exist but are DB-managed; you can still cast if you like:
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/* Optional relationships */
public function parent()
{
return $this->belongsTo(User::class, 'parent_id');
}
public function events()
{
return $this->hasMany(InvoiceEvent::class, 'invoice_id');
}
public function discountUsages()
{
return $this->hasMany(DiscountUsage::class, 'invoice_id');
}
/* =========================
* CI beforeInsert/beforeUpdate equivalents (normalize to UTC)
* ========================= */
protected static function booted(): void
{
static::creating(function (self $m) {
$m->normalizeDateTimes();
});
static::updating(function (self $m) {
$m->normalizeDateTimes();
});
}
private function normalizeDateTimes(): void
{
foreach (['issue_date', 'due_date', 'created_at', 'updated_at'] as $field) {
if (!$this->isDirty($field)) continue;
$val = $this->{$field};
if ($val === null || $val === '' || $val === '0000-00-00' || $val === '0000-00-00 00:00:00') {
// keep NULL for due_date; keep other values as-is (DB defaults/triggers)
if ($field === 'due_date') $this->{$field} = null;
continue;
}
$normalized = static::toUtcDateTimeString($val);
if ($normalized !== null) {
$this->{$field} = $normalized;
}
}
}
private static function toUtcDateTimeString($raw): ?string
{
if ($raw instanceof \DateTimeInterface) {
return Carbon::instance($raw)->utc()->format('Y-m-d H:i:s');
}
$s = trim((string) $raw);
if ($s === '') return null;
// strict datetime
try {
$dt = Carbon::createFromFormat('Y-m-d H:i:s', $s, 'UTC');
return $dt->utc()->format('Y-m-d H:i:s');
} catch (\Throwable $e) {
// ignore
}
// date-only sneaks in => store at 12:00:00
try {
$d = Carbon::createFromFormat('Y-m-d', $s, 'UTC')->setTime(12, 0, 0);
return $d->utc()->format('Y-m-d H:i:s');
} catch (\Throwable $e) {
// ignore
}
// fallback parser (ISO8601 etc.)
try {
return Carbon::parse($s)->utc()->format('Y-m-d H:i:s');
} catch (\Throwable $e) {
return null;
}
}
/* =========================
* Queries (ported from CI)
* ========================= */
public static function getInvoicesByUserId(int $userId, ?string $schoolYear = null): array
{
$q = DB::table('invoices as i')
->select('i.*')
->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount')
->where('i.parent_id', $userId);
if ($schoolYear !== null) {
$q->where('i.school_year', $schoolYear);
}
return $q->get()->map(fn ($r) => (array) $r)->all();
}
public static function updateInvoiceStatus(int $invoiceId, string $status): bool
{
return DB::table('invoices')->where('id', $invoiceId)->update(['status' => $status]) >= 0;
}
public static function getUnpaidInvoices(): array
{
return DB::table('invoices')
->whereIn('status', ['Unpaid', 'Partially Paid', 'unpaid', 'partially paid'])
->orderBy('due_date', 'ASC')
->get()
->map(fn ($r) => (array) $r)
->all();
}
public static function getLatestInvoiceTotalAmount(int $parentId)
{
return DB::table('invoices')
->where('parent_id', $parentId)
->orderByDesc('created_at')
->value('total_amount');
}
public static function getLatestInvoicePaidAmount(int $parentId)
{
return DB::table('invoices')
->where('parent_id', $parentId)
->orderByDesc('created_at')
->value('paid_amount');
}
public static function getLatestInvoiceBalance(int $parentId)
{
return DB::table('invoices')
->where('parent_id', $parentId)
->orderByDesc('created_at')
->value('balance');
}
public static function getInvoiceEventCharges(int $invoiceId): array
{
return DB::table('invoice_event')
->where('invoice_id', $invoiceId)
->get()
->map(fn ($r) => (array) $r)
->all();
}
public static function getInvoicesByParentId(int $parentId, ?string $schoolYear = null): array
{
$q = DB::table('invoices as i')
->select('i.*')
->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount')
->where('i.parent_id', $parentId);
if (!empty($schoolYear)) {
$q->where('i.school_year', $schoolYear);
}
return $q->orderByDesc('i.created_at')->get()->map(fn ($r) => (array) $r)->all();
}
public static function getLatestInvoiceBalanceByYear(int $parentId, string $schoolYear): ?array
{
$row = DB::table('invoices')
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->orderByDesc('created_at')
->select(['total_amount', 'paid_amount', 'balance', 'updated_at'])
->first();
return $row ? (array) $row : null;
}
public static function getDiscountedInvoicesByParent(int $parentId): array
{
return DB::table('invoices')
->where('parent_id', $parentId)
->where('has_discount', 1)
->get()
->map(fn ($r) => (array) $r)
->all();
}
/**
* Multi-parent variant (preload invoices for multiple parents).
*/
public static function getInvoicesByUserIds(array $parentIds, string $schoolYear): array
{
$parentIds = array_values(array_unique(array_map('intval', array_filter($parentIds))));
if (empty($parentIds)) return [];
return DB::table('invoices as i')
->select(['i.id','i.parent_id','i.invoice_number','i.status','i.balance','i.total_amount','i.issue_date','i.due_date'])
->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount')
->whereIn('i.parent_id', $parentIds)
->where('i.school_year', $schoolYear)
->orderByDesc('i.issue_date')
->orderByDesc('i.id')
->get()
->map(fn ($r) => (array) $r)
->all();
}
public static function getAllInvoicesByUserIds(array $parentIds, string $schoolYear): array
{
$parentIds = array_values(array_unique(array_map('intval', array_filter($parentIds))));
if (empty($parentIds)) return [];
return DB::table('invoices as i')
->select(['i.id','i.parent_id','i.invoice_number','i.status','i.balance','i.issue_date'])
->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount')
->whereIn('i.parent_id', $parentIds)
->where('i.school_year', $schoolYear)
->orderByDesc('i.issue_date')
->orderByDesc('i.id')
->get()
->map(fn ($r) => (array) $r)
->all();
}
/* =======================
* Additional charge math (atomic)
* ======================= */
public static function applyAdditionalCharge(int $invoiceId, float $amount): bool
{
$val = number_format(abs($amount), 2, '.', '');
$affected = DB::update(
"UPDATE invoices
SET total_amount = total_amount + {$val},
balance = balance + {$val}
WHERE id = ?",
[$invoiceId]
);
return $affected > 0;
}
public static function deductAdditionalCharge(int $invoiceId, float $amount): bool
{
$val = number_format(abs($amount), 2, '.', '');
$affected = DB::update(
"UPDATE invoices
SET total_amount = GREATEST(total_amount - {$val}, 0),
balance = GREATEST(balance - {$val}, 0)
WHERE id = ?",
[$invoiceId]
);
return $affected > 0;
}
public static function reverseAdditionalCharge(int $invoiceId, float $amount): bool
{
$amount = round($amount, 2);
$affected = DB::table('invoices')
->where('id', $invoiceId)
->update([
'total_amount' => DB::raw('GREATEST(total_amount - ' . $amount . ', 0)'),
'balance' => DB::raw('GREATEST(balance - ' . $amount . ', 0)'),
]);
return $affected >= 0;
}
}