add refund and event logic

This commit is contained in:
root
2026-03-10 23:12:49 -04:00
parent ba1206e314
commit f6be51576c
67 changed files with 4808 additions and 1000 deletions
@@ -0,0 +1,16 @@
<?php
namespace App\Services\Events;
class EventCategoryService
{
public static function categories(): array
{
return [
'workshops',
'orientations',
'field trips',
'Ramadan programs',
];
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Services\Events;
use App\Models\EventCharges;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class EventChargeQueryService
{
public function list(array $filters): array
{
$perPage = (int) ($filters['per_page'] ?? 25);
$page = (int) ($filters['page'] ?? 1);
$query = EventCharges::query()
->select([
'event_charges.*',
'users.firstname AS parent_firstname',
'users.lastname AS parent_lastname',
'students.firstname AS student_firstname',
'students.lastname AS student_lastname',
'events.event_name',
])
->leftJoin('users', 'users.id', '=', 'event_charges.parent_id')
->leftJoin('students', 'students.id', '=', 'event_charges.student_id')
->leftJoin('events', 'events.id', '=', 'event_charges.event_id');
if (!empty($filters['school_year'])) {
$query->where('event_charges.school_year', (string) $filters['school_year']);
}
if (!empty($filters['semester'])) {
$query->where('event_charges.semester', (string) $filters['semester']);
}
if (!empty($filters['parent_id'])) {
$query->where('event_charges.parent_id', (int) $filters['parent_id']);
}
if (!empty($filters['event_id'])) {
$query->where('event_charges.event_id', (int) $filters['event_id']);
}
$paginator = $query->orderByDesc('event_charges.created_at')
->paginate($perPage, ['*'], 'page', $page);
return [
'charges' => $paginator,
'pagination' => $this->paginationPayload($paginator),
];
}
private function paginationPayload(LengthAwarePaginator $paginator): array
{
return [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'total_pages' => $paginator->lastPage(),
];
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
namespace App\Services\Events;
use App\Models\Enrollment;
use App\Models\Event;
use App\Models\EventCharges;
use App\Services\Invoices\InvoiceGenerationService;
class EventChargeService
{
public function __construct(private InvoiceGenerationService $invoiceGeneration)
{
}
public function seedChargesForEvent(Event $event, string $schoolYear, string $semester, float $amount, int $actorId): array
{
$enrollments = Enrollment::query()
->select('enrollments.student_id', 'students.parent_id')
->join('students', 'students.id', '=', 'enrollments.student_id')
->where('enrollments.school_year', $schoolYear)
->whereIn('enrollments.enrollment_status', ['enrolled', 'payment pending'])
->get()
->map(fn ($row) => (array) $row)
->all();
$parentIds = [];
$created = 0;
foreach ($enrollments as $row) {
$studentId = (int) ($row['student_id'] ?? 0);
$parentId = (int) ($row['parent_id'] ?? 0);
if ($studentId <= 0 || $parentId <= 0) {
continue;
}
$exists = EventCharges::query()
->where('event_id', $event->id)
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->first();
if ($exists) {
continue;
}
EventCharges::query()->create([
'event_id' => (int) $event->id,
'parent_id' => $parentId,
'student_id' => $studentId,
'participation' => 'yes',
'charged' => $amount,
'school_year' => $schoolYear,
'semester' => $semester,
'updated_by' => $actorId ?: null,
'created_at' => utc_now(),
'updated_at' => utc_now(),
]);
$created++;
$parentIds[] = $parentId;
}
$parentIds = array_values(array_unique($parentIds));
foreach ($parentIds as $parentId) {
$this->invoiceGeneration->generateInvoice((int) $parentId, $schoolYear);
}
return [
'created' => $created,
'parent_ids' => $parentIds,
];
}
public function updateParticipation(
int $parentId,
int $eventId,
array $participations,
string $schoolYear,
string $semester,
int $actorId
): array {
$event = Event::getEvent($eventId, $schoolYear);
if (!$event) {
return ['ok' => false, 'message' => 'Event not found.'];
}
$created = 0;
$updated = 0;
$deleted = 0;
foreach ($participations as $studentId => $value) {
$studentId = (int) $studentId;
if ($studentId <= 0) {
continue;
}
$existing = EventCharges::query()->where([
'parent_id' => $parentId,
'student_id' => $studentId,
'event_id' => $eventId,
'school_year' => $schoolYear,
'semester' => $semester,
])->first();
if ($value === 'no') {
if ($existing) {
$existing->delete();
$deleted++;
}
continue;
}
if ($existing) {
$existing->update([
'participation' => 'yes',
'charged' => $event->amount,
'updated_by' => $actorId,
'updated_at' => utc_now(),
]);
$updated++;
} else {
EventCharges::query()->create([
'parent_id' => $parentId,
'student_id' => $studentId,
'event_id' => $eventId,
'participation' => 'yes',
'charged' => $event->amount,
'school_year' => $schoolYear,
'semester' => $semester,
'created_by' => $actorId,
'updated_by' => $actorId,
'created_at' => utc_now(),
'updated_at' => utc_now(),
]);
$created++;
}
}
$this->invoiceGeneration->generateInvoice($parentId, $schoolYear);
return [
'ok' => true,
'created' => $created,
'updated' => $updated,
'deleted' => $deleted,
];
}
public function deleteChargesForEvent(int $eventId): array
{
$charges = EventCharges::query()
->where('event_id', $eventId)
->get()
->map(fn ($row) => (array) $row)
->all();
$parentIds = [];
foreach ($charges as $charge) {
EventCharges::query()->whereKey($charge['id'])->delete();
$parentIds[] = (int) ($charge['parent_id'] ?? 0);
}
return [
'parent_ids' => array_values(array_unique(array_filter($parentIds))),
];
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Services\Events;
use App\Models\Event;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class EventListService
{
public function list(array $filters): array
{
$perPage = (int) ($filters['per_page'] ?? 25);
$page = (int) ($filters['page'] ?? 1);
$sortBy = (string) ($filters['sort_by'] ?? 'created_at');
$sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
$activeOnly = $filters['active'] ?? null;
$allowedSorts = ['created_at', 'expiration_date', 'event_name'];
if (!in_array($sortBy, $allowedSorts, true)) {
$sortBy = 'created_at';
}
$query = Event::query();
if (!empty($filters['school_year'])) {
$query->where('school_year', (string) $filters['school_year']);
}
if (!empty($filters['semester'])) {
$query->where('semester', (string) $filters['semester']);
}
if (!empty($filters['q'])) {
$term = trim((string) $filters['q']);
$query->where('event_name', 'like', "%{$term}%");
}
if ($activeOnly !== null) {
$today = now()->toDateString();
if (filter_var($activeOnly, FILTER_VALIDATE_BOOLEAN)) {
$query->whereDate('expiration_date', '>=', $today);
} else {
$query->whereDate('expiration_date', '<', $today);
}
}
$paginator = $query
->orderBy($sortBy, $sortDir)
->paginate($perPage, ['*'], 'page', $page);
$activeCount = $this->activeCount($filters);
return [
'events' => $paginator,
'pagination' => $this->paginationPayload($paginator),
'active_count' => $activeCount,
];
}
public function find(int $eventId, ?string $schoolYear = null): ?Event
{
$query = Event::query()->whereKey($eventId);
if ($schoolYear !== null && $schoolYear !== '') {
$query->where('school_year', $schoolYear);
}
return $query->first();
}
private function activeCount(array $filters): int
{
$query = Event::query()->whereDate('expiration_date', '>=', now()->toDateString());
if (!empty($filters['school_year'])) {
$query->where('school_year', (string) $filters['school_year']);
}
if (!empty($filters['semester'])) {
$query->where('semester', (string) $filters['semester']);
}
return (int) $query->count();
}
private function paginationPayload(LengthAwarePaginator $paginator): array
{
return [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'total_pages' => $paginator->lastPage(),
];
}
}
@@ -0,0 +1,116 @@
<?php
namespace App\Services\Events;
use App\Models\Event;
use App\Services\Invoices\InvoiceGenerationService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
class EventManagementService
{
public function __construct(
private EventChargeService $charges,
private InvoiceGenerationService $invoiceGeneration
) {
}
public function create(array $payload, ?UploadedFile $flyer, int $actorId): array
{
return DB::transaction(function () use ($payload, $flyer, $actorId) {
$flyerPath = $this->storeFlyer($flyer);
$event = Event::query()->create([
'event_name' => $payload['event_name'],
'event_category' => $payload['event_category'],
'description' => $payload['description'] ?? null,
'amount' => $payload['amount'],
'flyer' => $flyerPath,
'expiration_date' => $payload['expiration_date'],
'semester' => $payload['semester'],
'school_year' => $payload['school_year'],
'created_by' => $actorId ?: null,
'created_at' => utc_now(),
'updated_at' => utc_now(),
]);
$seed = $this->charges->seedChargesForEvent(
$event,
(string) $payload['school_year'],
(string) $payload['semester'],
(float) $payload['amount'],
$actorId
);
return [
'ok' => true,
'event' => $event,
'charges_created' => $seed['created'] ?? 0,
];
});
}
public function update(int $eventId, array $payload, ?UploadedFile $flyer): array
{
$event = Event::query()->find($eventId);
if (!$event) {
return ['ok' => false, 'message' => 'Event not found.'];
}
$flyerPath = $event->flyer;
if ($flyer) {
$flyerPath = $this->storeFlyer($flyer) ?: $flyerPath;
}
$event->update([
'event_name' => $payload['event_name'] ?? $event->event_name,
'event_category' => $payload['event_category'] ?? $event->event_category,
'description' => $payload['description'] ?? $event->description,
'amount' => $payload['amount'] ?? $event->amount,
'flyer' => $flyerPath,
'expiration_date' => $payload['expiration_date'] ?? $event->expiration_date,
'semester' => $payload['semester'] ?? $event->semester,
'school_year' => $payload['school_year'] ?? $event->school_year,
'updated_at' => utc_now(),
]);
return ['ok' => true, 'event' => $event];
}
public function delete(int $eventId, int $actorId): array
{
$event = Event::query()->find($eventId);
if (!$event) {
return ['ok' => false, 'message' => 'Event not found.'];
}
return DB::transaction(function () use ($event) {
$charges = $this->charges->deleteChargesForEvent((int) $event->id);
$event->delete();
foreach ($charges['parent_ids'] ?? [] as $parentId) {
$this->invoiceGeneration->generateInvoice((int) $parentId, (string) $event->school_year);
}
return ['ok' => true];
});
}
private function storeFlyer(?UploadedFile $file): ?string
{
if (!$file || !$file->isValid()) {
return null;
}
$dir = public_path('uploads/event_flyers');
if (!File::exists($dir)) {
File::makeDirectory($dir, 0755, true);
}
$name = $file->hashName();
$file->move($dir, $name);
return 'event_flyers/' . $name;
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Services\Events;
use App\Models\EventCharges;
use App\Models\Student;
class EventStudentChargeService
{
public function listStudentsWithCharges(int $parentId, string $schoolYear, string $semester): array
{
$students = Student::query()
->where('parent_id', $parentId)
->get()
->map(fn ($row) => (array) $row)
->all();
$chargedIds = EventCharges::query()
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->distinct()
->pluck('student_id')
->map(fn ($id) => (int) $id)
->all();
$chargedMap = array_flip($chargedIds);
$data = [];
foreach ($students as $student) {
$data[] = [
'id' => (int) ($student['id'] ?? 0),
'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
'charged' => isset($chargedMap[(int) ($student['id'] ?? 0)]),
];
}
return $data;
}
}