Files
alrahma_sunday_school_api/app/Services/Discounts/DiscountInvoiceService.php
T
2026-06-08 23:45:55 -04:00

401 lines
14 KiB
PHP

<?php
namespace App\Services\Discounts;
use App\Models\AdditionalCharge;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\Enrollment;
use App\Models\EventCharges;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\ApplicationUrlService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class DiscountInvoiceService
{
public function __construct(
private ApplicationUrlService $urls,
) {
}
public function getCurrentInvoiceBalance(int $invoiceId, string $schoolYear): float
{
$invoice = Invoice::query()->find($invoiceId);
if (!$invoice) {
return 0.0;
}
$paymentQuery = Payment::query()
->selectRaw('COALESCE(SUM(paid_amount),0) AS total_paid')
->where('invoice_id', $invoiceId)
->where('school_year', $schoolYear);
if (Schema::hasColumn('payments', 'status')) {
$paymentQuery->where(function ($q) {
$q->whereNotIn('status', [
'void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled',
])->orWhereNull('status');
});
}
if (Schema::hasColumn('payments', 'is_void')) {
$paymentQuery->where(function ($q) {
$q->where('is_void', 0)->orWhereNull('is_void');
});
}
$rowPaid = (array) $paymentQuery->first();
$totalPaid = (float) ($rowPaid['total_paid'] ?? 0);
$rowDisc = (array) DB::table('discount_usages as du')
->join('invoices as i', 'i.id', '=', 'du.invoice_id')
->where('du.invoice_id', $invoiceId)
->where('i.school_year', $schoolYear)
->selectRaw('COALESCE(SUM(du.discount_amount),0) AS total_disc')
->first();
$totalDisc = (float) ($rowDisc['total_disc'] ?? 0);
$rowRefund = (array) DB::table('refunds')
->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS total_refund_paid')
->where('invoice_id', $invoiceId)
->where('school_year', $schoolYear)
->whereIn('status', ['Partial', 'Paid'])
->first();
$totalRefundPaid = (float) ($rowRefund['total_refund_paid'] ?? 0);
$total = (float) ($invoice->total_amount ?? 0);
return max(0.0, round($total - $totalPaid - $totalDisc - $totalRefundPaid, 2));
}
public function recalculateInvoice(int $invoiceId, string $schoolYear): void
{
$invoice = Invoice::query()->find($invoiceId);
if (!$invoice) {
return;
}
$parentId = (int) ($invoice->parent_id ?? 0);
if ($parentId <= 0) {
return;
}
$enrollments = Enrollment::query()
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->get()
->map(fn ($row) => (array) $row)
->all();
$registered = [];
$withdrawn = [];
foreach ($enrollments as $e) {
$row = [
'student_id' => (int) ($e['student_id'] ?? 0),
'class_section_id' => $e['class_section_id'] ?? null,
'enrollment_status' => (string) ($e['enrollment_status'] ?? ''),
];
if (in_array($row['enrollment_status'], ['enrolled', 'payment pending'], true)) {
$registered[] = $row;
} elseif (in_array($row['enrollment_status'], ['withdrawn', 'refund pending', 'withdraw under review'], true)) {
$withdrawn[] = $row;
}
}
$refundDeadline = (string) (Configuration::getConfig('refund_deadline') ?? '');
$refundAllowed = true;
if ($refundDeadline !== '') {
try {
$tzName = function_exists('user_timezone') ? (string) user_timezone() : 'UTC';
$tz = new \DateTimeZone($tzName);
$today = new \DateTimeImmutable('today', $tz);
$deadline = new \DateTimeImmutable($refundDeadline, $tz);
$refundAllowed = $today <= $deadline;
} catch (\Throwable $e) {
$refundAllowed = true;
}
}
$tuitionStudents = $registered;
if (!$refundAllowed) {
$tuitionStudents = array_merge($tuitionStudents, $withdrawn);
}
$gradeFee = (int) (Configuration::getConfig('grade_fee') ?? 9);
$firstStudentFee = (float) (Configuration::getConfig('first_student_fee') ?? 350);
$secondStudentFee = (float) (Configuration::getConfig('second_student_fee') ?? 200);
$youthFee = (float) (Configuration::getConfig('youth_fee') ?? 180);
foreach ($tuitionStudents as &$s) {
$name = null;
if (!empty($s['class_section_id'])) {
$name = ClassSection::getClassSectionNameBySectionId($s['class_section_id']);
}
$s['grade_name'] = is_string($name) ? strtoupper(trim($name)) : 'N/A';
}
unset($s);
$regularCount = 0;
$youthCount = 0;
foreach ($tuitionStudents as $s) {
$lvl = $this->parseGradeLevel((string) ($s['grade_name'] ?? ''), $gradeFee);
if ($lvl > $gradeFee) {
$youthCount++;
} else {
$regularCount++;
}
}
$tuitionSubtotal = 0.0;
$tuitionSubtotal += $youthCount * $youthFee;
if ($regularCount >= 2) {
$tuitionSubtotal += $firstStudentFee + ($regularCount - 1) * $secondStudentFee;
} elseif ($regularCount === 1) {
$tuitionSubtotal += $firstStudentFee;
}
$eventSubtotal = 0.0;
$events = EventCharges::getChargesWithEventInfo($parentId, $schoolYear) ?? [];
foreach ($events as $ev) {
$eventSubtotal += (float) ($ev['charged'] ?? 0.0);
}
$additionalSubtotal = 0.0;
$rows = AdditionalCharge::query()
->select('charge_type', 'amount')
->where('invoice_id', $invoiceId)
->where('status', 'applied')
->get()
->map(fn ($row) => (array) $row)
->all();
foreach ($rows as $r) {
$amt = (float) ($r['amount'] ?? 0);
$typ = strtolower((string) ($r['charge_type'] ?? 'add'));
$amt = $typ === 'deduct' ? -abs($amt) : abs($amt);
$additionalSubtotal += $amt;
}
$newTotal = round($tuitionSubtotal + $eventSubtotal + $additionalSubtotal, 2);
$paymentQuery = Payment::query()
->where('invoice_id', $invoiceId)
->where('school_year', $schoolYear);
if (Schema::hasColumn('payments', 'status')) {
$paymentQuery->where(function ($q) {
$q->whereNotIn('status', [
'void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled',
])->orWhereNull('status');
});
}
if (Schema::hasColumn('payments', 'is_void')) {
$paymentQuery->where(function ($q) {
$q->where('is_void', 0)->orWhereNull('is_void');
});
}
$payments = $paymentQuery->get()->map(fn ($row) => (array) $row)->all();
$totalPaid = 0.0;
foreach ($payments as $p) {
$totalPaid += (float) ($p['paid_amount'] ?? 0);
}
$discRow = (array) DB::table('discount_usages')
->selectRaw('COALESCE(SUM(discount_amount),0) AS total_disc')
->where('invoice_id', $invoiceId)
->first();
$totalDisc = (float) ($discRow['total_disc'] ?? 0);
$refundRow = (array) DB::table('refunds')
->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS total_refund_paid')
->where('invoice_id', $invoiceId)
->whereIn('status', ['Partial', 'Paid'])
->first();
$totalRefundPaid = (float) ($refundRow['total_refund_paid'] ?? 0);
$newBalance = max(0.0, $newTotal - $totalDisc - $totalPaid - $totalRefundPaid);
$newStatus = $newBalance <= 0.00001 ? 'Paid' : ($totalPaid > 0 ? 'Partially Paid' : 'Unpaid');
$updateData = [
'total_amount' => $newTotal,
'paid_amount' => $totalPaid,
'balance' => $newBalance,
'status' => $newStatus,
'has_discount' => $totalDisc > 0.0 ? 1 : 0,
];
if (Schema::hasColumn('invoices', 'discount')) {
$updateData['discount'] = $totalDisc;
}
Invoice::query()->whereKey($invoiceId)->update($updateData);
}
public function updateEnrollmentStatusIfPaid(int $invoiceId, string $schoolYear): int
{
$invoice = Invoice::query()->find($invoiceId);
if (!$invoice) {
return 0;
}
$total = (float) ($invoice->total_amount ?? 0);
$balance = (float) ($invoice->balance ?? 0);
if (!($total > 0 && $balance < $total)) {
return 0;
}
$parentId = (int) ($invoice->parent_id ?? 0);
if ($parentId <= 0 || $schoolYear === '') {
return 0;
}
$paidEnrollmentIds = [];
if (Schema::hasTable('invoice_items') && Schema::hasColumn('invoice_items', 'enrollment_id')) {
$rows = DB::table('invoice_items')
->select('enrollment_id')
->where('invoice_id', $invoiceId)
->whereNotNull('enrollment_id')
->get()
->map(fn ($row) => (array) $row)
->all();
foreach ($rows as $r) {
$eid = (int) ($r['enrollment_id'] ?? 0);
if ($eid > 0) {
$paidEnrollmentIds[] = $eid;
}
}
}
$query = Enrollment::query()
->select('id')
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->where(function ($q) {
$q->where('enrollment_status', 'payment pending')
->orWhere('enrollment_status', 'Payment pending')
->orWhere('enrollment_status', 'Payment Pending')
->orWhere('enrollment_status', 'PAYMENT PENDING')
->orWhereRaw("LOWER(TRIM(REPLACE(enrollment_status, CHAR(160), ' '))) = 'payment pending'");
});
if (!empty($paidEnrollmentIds)) {
$query->whereIn('id', array_values(array_unique($paidEnrollmentIds)));
}
$toUpdateIds = $query->get()->pluck('id')->map(fn ($id) => (int) $id)->all();
if (empty($toUpdateIds)) {
return 0;
}
$affected = Enrollment::query()
->whereIn('id', $toUpdateIds)
->update([
'enrollment_status' => 'enrolled',
'enrollment_date' => $this->formatLocalDate('Y-m-d'),
]);
return (int) $affected;
}
public function buildPaymentEventData(
int $invoiceId,
string $transactionIdOrRef,
float $amount,
string $paymentMethod,
string $paymentDate,
?string $checkNumber,
int $installmentSeq,
float $preBalance,
float $postBalance,
string $schoolYear,
string $semester
): array {
$invoice = Invoice::query()->find($invoiceId);
if (!$invoice) {
throw new \RuntimeException("Invoice {$invoiceId} not found");
}
$parentRow = (array) DB::table('users')
->select('id', 'firstname', 'lastname', 'email')
->where('id', $invoice->parent_id)
->first();
if (empty($parentRow)) {
throw new \RuntimeException("Parent not found for invoice {$invoiceId}");
}
$studentRows = DB::table('students as s')
->select('s.id', 's.firstname', 's.lastname', 'sc.class_section_id')
->leftJoin('student_class as sc', 'sc.student_id', '=', 's.id')
->where('s.parent_id', $invoice->parent_id)
->where('sc.school_year', $schoolYear)
->get()
->map(fn ($row) => (array) $row)
->all();
$data = [
'user_id' => (int) $parentRow['id'],
'email' => $parentRow['email'],
'firstname' => $parentRow['firstname'],
'lastname' => $parentRow['lastname'],
'school_year' => $schoolYear,
'semester' => $semester,
'invoice_id' => $invoiceId,
'transaction_id' => $transactionIdOrRef,
'payment_date' => $paymentDate,
'amount' => $amount,
'method' => $paymentMethod,
'check_number' => $checkNumber,
'installment_seq' => $installmentSeq,
'invoice_total' => (float) $invoice->total_amount,
'pre_balance' => $preBalance,
'post_balance' => $postBalance,
'portalLink' => $this->urls->spaParentInvoiceDetailUrl($invoiceId),
];
return [$data, $studentRows];
}
private function parseGradeLevel(string $grade, int $gradeFee): int
{
if (is_numeric($grade)) {
return (int) $grade;
}
$g = strtoupper(trim($grade));
$g = preg_replace('/\\s+/', ' ', $g);
$g = str_replace(['.', '_', '-'], ['', '', ' '], $g);
$kg = ['K', 'KG', 'K G', 'KINDER', 'KINDERGARTEN'];
if (in_array($g, $kg, true)) {
return 1;
}
$pk = ['PK', 'P K', 'PREK', 'PRE K', 'PRE KINDER', 'PREKINDER'];
if (in_array($g, $pk, true)) {
return -1;
}
if (preg_match('/^Y(?:OUTH)?\\s*(\\d+)?$/', $g, $m)) {
$n = isset($m[1]) && $m[1] !== '' ? max(1, (int) $m[1]) : 1;
return $gradeFee + $n;
}
if (preg_match('/^(?:GR?ADE\\s*)?(\\d{1,2})\\s*([A-Z]*)$/', $g, $m)) {
return (int) $m[1];
}
return 999;
}
private function formatLocalDate(string $format): string
{
if (function_exists('local_date') && function_exists('utc_now')) {
return (string) local_date(utc_now(), $format);
}
return now()->format($format);
}
}