add controllers, servoices

This commit is contained in:
root
2026-03-09 02:52:13 -04:00
parent c8de5f7edc
commit d76c871cb7
501 changed files with 34439 additions and 21843 deletions
@@ -0,0 +1,235 @@
<?php
namespace App\Services\ExtraCharges;
use App\Models\AdditionalCharge;
use App\Models\Invoice;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ExtraChargesChargeService
{
public function __construct(
private ExtraChargesContextService $context,
private ExtraChargesInvoiceService $invoiceService
) {}
public function createCharge(array $payload): array
{
$invoiceId = $payload['invoice_id'] ?? null;
$chargeType = $payload['charge_type'];
$amountAbs = round(abs((float) $payload['amount']), 2);
$signedAmount = $chargeType === 'add' ? $amountAbs : -$amountAbs;
$schoolYear = $payload['school_year'] ?? $this->context->getSchoolYear();
$semester = $payload['semester'] ?? $this->context->getSemester();
$data = [
'parent_id' => (int) $payload['parent_id'],
'invoice_id' => $invoiceId,
'school_year' => $schoolYear,
'semester' => $semester,
'charge_type' => $chargeType,
'title' => trim($payload['title']),
'description' => trim((string) ($payload['description'] ?? '')),
'amount' => $signedAmount,
'due_date' => $payload['due_date'] ?? null,
'status' => $invoiceId ? 'applied' : 'pending',
'created_by' => (int) ($payload['created_by'] ?? 0),
'created_at' => $payload['created_at'] ?? $this->utcNow(),
];
$invoiceBefore = $invoiceId ? Invoice::getInvoicesByParentId((int) $payload['parent_id'], $schoolYear) : [];
$invoiceAfter = [];
DB::beginTransaction();
try {
$chargeId = AdditionalCharge::query()->create($data)->id;
if ($invoiceId) {
$this->invoiceService->applyToInvoice((int) $invoiceId, $chargeType, $amountAbs);
}
$invoiceAfter = $invoiceId ? Invoice::getInvoicesByParentId((int) $payload['parent_id'], $schoolYear) : [];
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
Log::error('Extra charge create failed: ' . $e->getMessage());
return ['ok' => false, 'message' => 'Failed to save charge.'];
}
$parentUser = User::getUserInfoById((int) $payload['parent_id']);
$this->triggerExtraChargeEvent($parentUser, $data, (int) $chargeId, $invoiceId, $invoiceBefore, $invoiceAfter);
return [
'ok' => true,
'id' => (int) $chargeId,
'invoice_id' => $invoiceId ? (int) $invoiceId : null,
'parent_id' => (int) $payload['parent_id'],
];
}
public function updateCharge(AdditionalCharge $charge, array $payload): bool
{
$chargeType = $payload['charge_type'] ?? $charge->charge_type ?? 'add';
$amountRaw = isset($payload['amount']) ? (float) $payload['amount'] : (float) $charge->amount;
$newAmount = $chargeType === 'add' ? abs($amountRaw) : -abs($amountRaw);
$delta = $newAmount - (float) $charge->amount;
DB::beginTransaction();
try {
$charge->title = trim($payload['title'] ?? $charge->title);
$charge->description = trim($payload['description'] ?? $charge->description);
$charge->amount = $newAmount;
$charge->due_date = $payload['due_date'] ?? $charge->due_date;
$charge->charge_type = $chargeType;
if (array_key_exists('updated_by', $payload)) {
$charge->updated_by = $payload['updated_by'];
}
$charge->save();
if ($charge->status === 'applied' && !empty($charge->invoice_id) && abs($delta) > 0.00001) {
$this->invoiceService->adjustDelta((int) $charge->invoice_id, $delta);
}
DB::commit();
return true;
} catch (\Throwable $e) {
DB::rollBack();
Log::error('Extra charge update failed: ' . $e->getMessage());
return false;
}
}
public function voidCharge(AdditionalCharge $charge): bool
{
DB::beginTransaction();
try {
$invoiceId = (int) ($charge->invoice_id ?? 0);
$amountAbs = round(abs((float) ($charge->amount ?? 0)), 2);
$chargeType = (string) ($charge->charge_type ?? 'add');
$status = (string) ($charge->status ?? 'pending');
if ($status === 'applied' && $invoiceId > 0 && $amountAbs > 0) {
$this->invoiceService->reverseOnInvoice($invoiceId, $chargeType, $amountAbs);
}
$charge->status = 'void';
$charge->save();
DB::commit();
return true;
} catch (\Throwable $e) {
DB::rollBack();
Log::error('Extra charge void failed: ' . $e->getMessage());
return false;
}
}
public function reverseCharge(AdditionalCharge $charge): bool
{
$invoiceId = (int) ($charge->invoice_id ?? 0);
$amountAbs = round(abs((float) ($charge->amount ?? 0)), 2);
$chargeType = (string) ($charge->charge_type ?? 'add');
$status = (string) ($charge->status ?? 'pending');
if ($status !== 'applied' || $invoiceId <= 0 || $amountAbs <= 0) {
return false;
}
DB::beginTransaction();
try {
$this->invoiceService->reverseOnInvoice($invoiceId, $chargeType, $amountAbs);
$charge->status = 'pending';
$charge->invoice_id = null;
$charge->save();
DB::commit();
return true;
} catch (\Throwable $e) {
DB::rollBack();
Log::error('Extra charge reverse failed: ' . $e->getMessage());
return false;
}
}
private function triggerExtraChargeEvent(?array $parentUser, array $payload, int $chargeId, ?int $invoiceId, array $invoiceBefore, array $invoiceAfter): void
{
if (!$parentUser) {
return;
}
$first = $parentUser['first_name'] ?? $parentUser['firstname'] ?? '';
$last = $parentUser['last_name'] ?? $parentUser['lastname'] ?? '';
$parentName = trim($first . ' ' . $last);
$before = $this->normalizeRow($invoiceBefore);
$after = $this->normalizeRow($invoiceAfter);
$invoiceTotal = $after ? (float) ($after['total_amount'] ?? 0) : 0.0;
$preBalance = $before ? (float) ($before['balance'] ?? 0) : 0.0;
$postBalance = $after ? (float) ($after['balance'] ?? 0) : 0.0;
$eventData = [
'user_id' => (int) ($parentUser['id'] ?? 0),
'email' => $parentUser['email'] ?? null,
'firstname' => $first,
'lastname' => $last,
'parentName' => $parentName,
'school_year' => $payload['school_year'],
'semester' => $payload['semester'],
'invoice_id' => $invoiceId,
'invoice_number' => $after['invoice_number'] ?? ($invoiceId ? 'INV-' . $invoiceId : null),
'charge_id' => $chargeId,
'charge_title' => $payload['title'],
'charge_desc' => $payload['description'],
'charge_type' => $payload['charge_type'],
'amount_signed' => $payload['amount'],
'amount_abs' => abs((float) $payload['amount']),
'due_date' => $payload['due_date'],
'created_at' => $payload['created_at'],
'pre_balance' => $preBalance,
'post_balance' => $postBalance,
'invoice_total' => $invoiceTotal,
'portal_link' => url('/login'),
'invoice_link' => $invoiceId ? url('parent/invoices/view/' . $invoiceId) : url('parent/invoices'),
];
$students = [];
try {
if (class_exists(\CodeIgniter\Events\Events::class)) {
\CodeIgniter\Events\Events::trigger('extraCharge', $eventData, $students);
} elseif (function_exists('event')) {
event('extraCharge', [$eventData, $students]);
}
} catch (\Throwable $e) {
Log::warning('extraCharge event failed: ' . $e->getMessage());
}
}
private function normalizeRow($row): ?array
{
if (!$row) {
return null;
}
if (is_array($row) && isset($row[0]) && is_array($row[0])) {
return $row[0];
}
return is_array($row) ? $row : null;
}
private function utcNow(): string
{
if (function_exists('utc_now')) {
return (string) utc_now();
}
return now('UTC')->toDateTimeString();
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Services\ExtraCharges;
use App\Models\Configuration;
class ExtraChargesContextService
{
public function getSchoolYear(): string
{
return (string) (Configuration::getConfig('school_year') ?? '');
}
public function getSemester(): string
{
return (string) (Configuration::getConfig('semester') ?? '');
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Services\ExtraCharges;
use App\Models\Invoice;
class ExtraChargesInvoiceService
{
public function listInvoicesForParent(int $parentId, string $schoolYear): array
{
if ($parentId <= 0) {
return [];
}
$rows = Invoice::getInvoicesByParentId($parentId, $schoolYear) ?? [];
return array_map(function ($inv) {
$id = (int) ($inv['id'] ?? 0);
$num = (string) ($inv['invoice_number'] ?? ('INV-' . $id));
$status = strtolower((string) ($inv['status'] ?? ''));
$balance = (float) ($inv['balance'] ?? 0);
$issue = $inv['issue_date'] ?? null;
return [
'id' => $id,
'invoice_number' => $num,
'status' => $status,
'balance' => $balance,
'issue_date' => $issue,
'text' => $num
. ($status ? ' — ' . ucfirst($status) : '')
. ' (Bal: $' . number_format($balance, 2) . ')',
];
}, $rows);
}
public function applyToInvoice(int $invoiceId, string $chargeType, float $amountAbs): void
{
if ($invoiceId <= 0 || $amountAbs <= 0) {
return;
}
if ($chargeType === 'add') {
Invoice::applyAdditionalCharge($invoiceId, $amountAbs);
} else {
Invoice::deductAdditionalCharge($invoiceId, $amountAbs);
}
}
public function reverseOnInvoice(int $invoiceId, string $chargeType, float $amountAbs): void
{
if ($invoiceId <= 0 || $amountAbs <= 0) {
return;
}
if ($chargeType === 'add') {
Invoice::reverseAdditionalCharge($invoiceId, $amountAbs);
} else {
Invoice::applyAdditionalCharge($invoiceId, $amountAbs);
}
}
public function adjustDelta(int $invoiceId, float $delta): void
{
if ($invoiceId <= 0 || abs($delta) <= 0.00001) {
return;
}
if ($delta > 0) {
Invoice::applyAdditionalCharge($invoiceId, $delta);
} else {
Invoice::reverseAdditionalCharge($invoiceId, abs($delta));
}
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Services\ExtraCharges;
use App\Models\AdditionalCharge;
class ExtraChargesListService
{
public function listForTerm(string $schoolYear, string $semester, ?string $status, ?string $q, int $perPage): array
{
$rows = AdditionalCharge::listAllForTerm($schoolYear, $semester, $status, $q, $perPage);
$meta = [
'perPage' => method_exists($rows, 'perPage') ? $rows->perPage() : $perPage,
'page' => method_exists($rows, 'currentPage') ? $rows->currentPage() : null,
'total' => method_exists($rows, 'total') ? $rows->total() : null,
'pageCount' => method_exists($rows, 'lastPage') ? $rows->lastPage() : null,
];
$items = method_exists($rows, 'items') ? $rows->items() : (array) $rows;
return [$items, $meta];
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Services\ExtraCharges;
use Illuminate\Support\Facades\DB;
class ExtraChargesMetaService
{
public function getSchoolYears(string $fallbackYear = ''): array
{
$schoolYears = [];
try {
$rows = DB::table('additional_charges')
->selectRaw('DISTINCT school_year')
->whereNotNull('school_year')
->orderByDesc('school_year')
->get()
->map(fn ($row) => (array) $row)
->all();
foreach ($rows as $row) {
$val = (string) ($row['school_year'] ?? '');
if ($val !== '' && !in_array($val, $schoolYears, true)) {
$schoolYears[] = $val;
}
}
} catch (\Throwable $e) {
}
try {
$rows = DB::table('invoices')
->selectRaw('DISTINCT school_year')
->whereNotNull('school_year')
->orderByDesc('school_year')
->get()
->map(fn ($row) => (array) $row)
->all();
foreach ($rows as $row) {
$val = (string) ($row['school_year'] ?? '');
if ($val !== '' && !in_array($val, $schoolYears, true)) {
$schoolYears[] = $val;
}
}
} catch (\Throwable $e) {
}
if (empty($schoolYears) && $fallbackYear !== '') {
$schoolYears[] = $fallbackYear;
if (str_contains($fallbackYear, '-')) {
[$start] = explode('-', $fallbackYear) + [0 => ''];
$start = (int) $start;
if ($start > 0) {
for ($i = 1; $i <= 3; $i++) {
$schoolYears[] = ($start - $i) . '-' . (($start - $i) + 1);
}
}
}
}
rsort($schoolYears);
return array_values(array_unique($schoolYears));
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Services\ExtraCharges;
use Illuminate\Support\Facades\DB;
class ExtraChargesParentService
{
public function searchParents(string $q = '', int $limit = 20): array
{
$builder = DB::table('users')
->select('users.id', 'users.firstname', 'users.lastname', 'users.email')
->join('user_roles', 'user_roles.user_id', '=', 'users.id')
->join('roles', 'roles.id', '=', 'user_roles.role_id')
->where('roles.name', 'parent')
->whereNull('user_roles.deleted_at');
if ($q !== '') {
$builder->where(function ($query) use ($q) {
$query->where('users.firstname', 'like', "%{$q}%")
->orWhere('users.lastname', 'like', "%{$q}%")
->orWhere('users.email', 'like', "%{$q}%");
});
}
$rows = $builder
->orderBy('users.lastname', 'ASC')
->orderBy('users.firstname', 'ASC')
->limit($limit)
->get()
->map(fn ($row) => (array) $row)
->all();
return array_map(function ($row) {
$name = trim(($row['lastname'] ?? '') . ', ' . ($row['firstname'] ?? ''));
$email = $row['email'] ?? '';
$label = $name !== ',' ? $name : ('User #' . $row['id']);
if ($email) {
$label .= ' — ' . $email;
}
$label .= ' (ID: ' . $row['id'] . ')';
return [
'id' => (int) $row['id'],
'text' => $label,
];
}, $rows);
}
}