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' => $this->urls->webLoginUrl(), 'invoice_link' => $invoiceId ? $this->urls->spaParentInvoiceViewUrl($invoiceId) : $this->urls->spaParentInvoicesIndexUrl(), ]; $students = []; try { if (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(); } }