add controllers, servoices
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Discounts;
|
||||
|
||||
use App\Models\DiscountVoucher;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DiscountApplyService
|
||||
{
|
||||
private DiscountContextService $context;
|
||||
private DiscountInvoiceService $invoiceService;
|
||||
|
||||
public function __construct(DiscountContextService $context, DiscountInvoiceService $invoiceService)
|
||||
{
|
||||
$this->context = $context;
|
||||
$this->invoiceService = $invoiceService;
|
||||
}
|
||||
|
||||
public function applyVoucher(int $voucherId, array $parentIds, bool $allowAdditional, int $actorId): array
|
||||
{
|
||||
$schoolYear = $this->context->getSchoolYear();
|
||||
$semester = $this->context->getSemester();
|
||||
|
||||
$voucher = DiscountVoucher::query()->find($voucherId);
|
||||
if (!$voucher) {
|
||||
return ['ok' => false, 'message' => 'Voucher not found.'];
|
||||
}
|
||||
|
||||
$voucherDescription = trim((string) ($voucher->description ?? ''));
|
||||
$voucherDescription = preg_replace(
|
||||
'/^Auto-generated on \\d{4}-\\d{2}-\\d{2}\\s*[—-]\\s*reason:\\s*/i',
|
||||
'',
|
||||
$voucherDescription
|
||||
);
|
||||
|
||||
$maxUsesRaw = $voucher->max_uses;
|
||||
$maxUses = ($maxUsesRaw === null || $maxUsesRaw === '') ? null : (int) $maxUsesRaw;
|
||||
$timesUsed = (int) ($voucher->times_used ?? 0);
|
||||
$remainingUses = ($maxUses === null) ? PHP_INT_MAX : ($maxUses - $timesUsed);
|
||||
if ($remainingUses <= 0) {
|
||||
return ['ok' => false, 'message' => 'This voucher has reached its maximum allowed uses.'];
|
||||
}
|
||||
|
||||
if (!$allowAdditional) {
|
||||
$rows = DB::table('discount_usages as du')
|
||||
->select('i.parent_id')
|
||||
->join('invoices as i', 'i.id', '=', 'du.invoice_id')
|
||||
->whereIn('i.parent_id', $parentIds)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->groupBy('i.parent_id')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$blockedParentIds = array_map(static fn ($r) => (int) ($r['parent_id'] ?? 0), $rows);
|
||||
if (!empty($blockedParentIds)) {
|
||||
$parentIds = array_values(array_diff(array_map('intval', $parentIds), $blockedParentIds));
|
||||
}
|
||||
if (empty($parentIds)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'All selected parents already have discounts. Enable "Allow additional discounts" to apply another.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$paymentDate = function_exists('utc_now') ? utc_now() : now('UTC')->toDateTimeString();
|
||||
$fullyCoveredInvoiceIds = [];
|
||||
$touchedInvoiceIds = [];
|
||||
$appliedCount = 0;
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($parentIds as $parentId) {
|
||||
$invoices = Invoice::getInvoicesByUserId((int) $parentId, $schoolYear);
|
||||
if (empty($invoices)) {
|
||||
Log::warning('Discount apply: parent has no invoices', [
|
||||
'parent_id' => (int) $parentId,
|
||||
'voucher_id' => $voucherId,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
if ($remainingUses <= 0) {
|
||||
break 2;
|
||||
}
|
||||
|
||||
$invoiceId = (int) ($invoice['id'] ?? 0);
|
||||
if ($invoiceId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$initialPreBalance = (float) $this->invoiceService->getCurrentInvoiceBalance($invoiceId, $schoolYear);
|
||||
if ($initialPreBalance <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$allowAdditional) {
|
||||
$exists = DB::table('discount_usages as du')
|
||||
->join('invoices as i', 'du.invoice_id', '=', 'i.id')
|
||||
->where('du.voucher_id', $voucherId)
|
||||
->where('du.invoice_id', $invoiceId)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->count();
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$rawDiscount = $voucher->discount_type === 'percent'
|
||||
? round(((float) ($invoice['total_amount'] ?? 0) * (float) $voucher->discount_value) / 100, 2)
|
||||
: (float) $voucher->discount_value;
|
||||
|
||||
$discount = min($rawDiscount, $initialPreBalance);
|
||||
if ($discount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$now = function_exists('utc_now') ? utc_now() : now('UTC')->toDateTimeString();
|
||||
DB::table('discount_usages')->insert([
|
||||
'voucher_id' => $voucherId,
|
||||
'invoice_id' => $invoiceId,
|
||||
'parent_id' => (int) $parentId,
|
||||
'discount_amount' => $discount,
|
||||
'description' => $voucherDescription,
|
||||
'school_year' => $schoolYear,
|
||||
'updated_by' => $actorId,
|
||||
'used_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
'semester' => $semester,
|
||||
]);
|
||||
|
||||
$newBalance = max(0.0, round($initialPreBalance - $discount, 2));
|
||||
DB::table('invoices')
|
||||
->where('id', $invoiceId)
|
||||
->update([
|
||||
'balance' => $newBalance,
|
||||
'has_discount' => 1,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$postBalance = round($initialPreBalance - $discount, 2);
|
||||
if ($postBalance < 0) {
|
||||
$postBalance = 0.0;
|
||||
}
|
||||
|
||||
$currentBalance = (float) $this->invoiceService->getCurrentInvoiceBalance($invoiceId, $schoolYear);
|
||||
|
||||
DB::table('discount_vouchers')
|
||||
->where('id', $voucherId)
|
||||
->update([
|
||||
'times_used' => DB::raw('COALESCE(times_used,0) + 1'),
|
||||
]);
|
||||
|
||||
$this->triggerPaymentReceived(
|
||||
$invoiceId,
|
||||
(string) $voucherId,
|
||||
$discount,
|
||||
$paymentDate,
|
||||
$initialPreBalance,
|
||||
$postBalance,
|
||||
$schoolYear,
|
||||
$semester
|
||||
);
|
||||
|
||||
$touchedInvoiceIds[] = $invoiceId;
|
||||
$appliedCount++;
|
||||
|
||||
$epsilon = 0.01;
|
||||
$fullyCovered = (abs($discount - $initialPreBalance) <= $epsilon)
|
||||
|| ($postBalance <= $epsilon)
|
||||
|| ($currentBalance <= $epsilon);
|
||||
|
||||
if ($fullyCovered) {
|
||||
$fullyCoveredInvoiceIds[] = $invoiceId;
|
||||
}
|
||||
|
||||
$remainingUses--;
|
||||
|
||||
if ($remainingUses <= 0 && $maxUses !== null) {
|
||||
DB::table('discount_vouchers')
|
||||
->where('id', $voucherId)
|
||||
->update([
|
||||
'is_active' => 0,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return ['ok' => false, 'message' => 'Voucher application failed. Transaction rolled back.'];
|
||||
}
|
||||
|
||||
if ($appliedCount === 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'No discounts were applied. The voucher may already be used for these invoices or balances are zero.',
|
||||
];
|
||||
}
|
||||
|
||||
if ($maxUses !== null) {
|
||||
$row = (array) DB::table('discount_vouchers')
|
||||
->select('times_used')
|
||||
->where('id', $voucherId)
|
||||
->first();
|
||||
$usedNow = (int) ($row['times_used'] ?? 0);
|
||||
if ($usedNow >= $maxUses) {
|
||||
DB::table('discount_vouchers')
|
||||
->where('id', $voucherId)
|
||||
->update([
|
||||
'is_active' => 0,
|
||||
'updated_at' => function_exists('utc_now') ? utc_now() : now('UTC')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_unique($touchedInvoiceIds) as $iid) {
|
||||
try {
|
||||
$this->invoiceService->recalculateInvoice($iid, $schoolYear);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('recalculateInvoice failed for invoice {id}: {err}', [
|
||||
'id' => $iid,
|
||||
'err' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$updatedTotal = 0;
|
||||
foreach (array_unique($fullyCoveredInvoiceIds) as $iid) {
|
||||
try {
|
||||
$updatedTotal += $this->invoiceService->updateEnrollmentStatusIfPaid($iid, $schoolYear);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('updateEnrollmentStatusIfPaid failed for invoice {id}: {err}', [
|
||||
'id' => $iid,
|
||||
'err' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => $updatedTotal > 0
|
||||
? 'Voucher applied successfully and enrollments updated.'
|
||||
: 'Voucher applied successfully.',
|
||||
'appliedCount' => $appliedCount,
|
||||
'updatedEnrollments' => $updatedTotal,
|
||||
'touchedInvoices' => array_values(array_unique($touchedInvoiceIds)),
|
||||
];
|
||||
}
|
||||
|
||||
private function triggerPaymentReceived(
|
||||
int $invoiceId,
|
||||
string $voucherId,
|
||||
float $amount,
|
||||
string $paymentDate,
|
||||
float $preBalance,
|
||||
float $postBalance,
|
||||
string $schoolYear,
|
||||
string $semester
|
||||
): void {
|
||||
try {
|
||||
[$eventData, $studentData] = $this->invoiceService->buildPaymentEventData(
|
||||
$invoiceId,
|
||||
$voucherId,
|
||||
$amount,
|
||||
'discount',
|
||||
$paymentDate,
|
||||
'',
|
||||
0,
|
||||
$preBalance,
|
||||
$postBalance,
|
||||
$schoolYear,
|
||||
$semester
|
||||
);
|
||||
|
||||
if (class_exists(\CodeIgniter\Events\Events::class)) {
|
||||
\CodeIgniter\Events\Events::trigger('paymentReceived', $eventData, $studentData);
|
||||
} elseif (function_exists('event')) {
|
||||
event('paymentReceived', [$eventData, $studentData]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('paymentReceived hook failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Discounts;
|
||||
|
||||
use App\Models\Configuration;
|
||||
|
||||
class DiscountContextService
|
||||
{
|
||||
public function getSchoolYear(): string
|
||||
{
|
||||
return (string) (Configuration::getConfig('school_year') ?? '');
|
||||
}
|
||||
|
||||
public function getSemester(): string
|
||||
{
|
||||
return (string) (Configuration::getConfig('semester') ?? '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?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 Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class DiscountInvoiceService
|
||||
{
|
||||
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' => url('parent/invoices/' . $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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Discounts;
|
||||
|
||||
use App\Models\DiscountUsage;
|
||||
use App\Models\User;
|
||||
|
||||
class DiscountParentService
|
||||
{
|
||||
public function listParentsWithDiscounts(string $schoolYear): array
|
||||
{
|
||||
$parents = User::getParents();
|
||||
|
||||
foreach ($parents as &$parent) {
|
||||
$total = DiscountUsage::getTotalDiscountByParentIdAndSchoolYear(
|
||||
(int) ($parent['id'] ?? 0),
|
||||
$schoolYear
|
||||
);
|
||||
|
||||
$parent['total_discount'] = $total;
|
||||
$parent['has_discount'] = $total > 0 ? 1 : 0;
|
||||
}
|
||||
unset($parent);
|
||||
|
||||
return $parents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Discounts;
|
||||
|
||||
use App\Models\DiscountVoucher;
|
||||
|
||||
class DiscountVoucherService
|
||||
{
|
||||
public function listAll(): array
|
||||
{
|
||||
return DiscountVoucher::query()
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function listActive(): array
|
||||
{
|
||||
return DiscountVoucher::query()
|
||||
->where('is_active', 1)
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function create(array $payload): DiscountVoucher
|
||||
{
|
||||
return DiscountVoucher::query()->create($payload);
|
||||
}
|
||||
|
||||
public function update(int $id, array $payload): DiscountVoucher
|
||||
{
|
||||
$voucher = DiscountVoucher::query()->findOrFail($id);
|
||||
$voucher->fill($payload);
|
||||
$voucher->save();
|
||||
return $voucher;
|
||||
}
|
||||
|
||||
public function find(int $id): DiscountVoucher
|
||||
{
|
||||
return DiscountVoucher::query()->findOrFail($id);
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
DiscountVoucher::query()->whereKey($id)->delete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user