221 lines
8.1 KiB
PHP
221 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payments;
|
|
|
|
use App\Models\Configuration;
|
|
use App\Models\FamilyGuardian;
|
|
use App\Models\Invoice;
|
|
use App\Models\PaymentNotificationLog;
|
|
use App\Models\User;
|
|
use App\Services\EmailService;
|
|
use DateTimeImmutable;
|
|
use DateTimeZone;
|
|
|
|
class PaymentTestNotificationService
|
|
{
|
|
public function __construct(private EmailService $emailService)
|
|
{
|
|
}
|
|
|
|
public function send(string $email, string $type = 'no_payment'): array
|
|
{
|
|
$email = trim($email);
|
|
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
return ['ok' => false, 'message' => 'Invalid email address.'];
|
|
}
|
|
|
|
$type = in_array($type, ['no_payment', 'installment'], true) ? $type : 'no_payment';
|
|
|
|
$tzName = $this->resolveTimezone();
|
|
$tz = new DateTimeZone($tzName);
|
|
$now = new DateTimeImmutable('now', $tz);
|
|
|
|
$year = (int) $now->format('Y');
|
|
$month = (int) $now->format('n');
|
|
$schoolYear = (string) (Configuration::getConfig('school_year') ?? $year);
|
|
|
|
$user = User::query()->where('email', $email)->first();
|
|
$parentId = (int) ($user->id ?? 0);
|
|
|
|
$invoices = $parentId > 0
|
|
? Invoice::query()->where('parent_id', $parentId)->where('school_year', $schoolYear)->get()
|
|
: collect();
|
|
|
|
$totalBalance = 0.0;
|
|
$latestInvoiceId = null;
|
|
foreach ($invoices as $invoice) {
|
|
$totalBalance += (float) ($invoice->balance ?? 0);
|
|
if ($latestInvoiceId === null) {
|
|
$latestInvoiceId = (int) ($invoice->id ?? 0);
|
|
}
|
|
}
|
|
|
|
$ccEmail = $this->findSecondaryGuardianEmail($parentId, $email);
|
|
|
|
$parentName = $user ? trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) : '';
|
|
$monthYear = $now->format('F Y');
|
|
$subject = sprintf('Monthly Tuition Reminder — %s', $schoolYear);
|
|
$greeting = $parentName !== '' ? "Dear {$parentName}," : 'Dear Parent,';
|
|
$balanceFmt = '$' . number_format($totalBalance, 2);
|
|
$intro = ($type === 'no_payment')
|
|
? "We noticed there are outstanding tuition charges for {$schoolYear} but no payment has been recorded yet."
|
|
: "This is your monthly installment reminder for {$schoolYear}.";
|
|
|
|
$installmentInfo = $this->computeInstallmentSuggestion($totalBalance, $tzName);
|
|
|
|
$bodyHtml = <<<HTML
|
|
<div style="font-family:Arial,Helvetica,sans-serif; font-size:14px; color:#212529; line-height:1.5;">
|
|
<h2 style="margin:0 0 10px; font-size:18px;">Monthly Tuition Reminder (Test)</h2>
|
|
<p>{$greeting}</p>
|
|
<p>{$intro}</p>
|
|
<p><strong>Current Outstanding Balance:</strong> {$balanceFmt}</p>
|
|
<p>
|
|
As a friendly reminder, our tuition installments are due at the beginning of each month.
|
|
You can review your invoice and payment options by logging in to your parent portal.
|
|
</p>
|
|
<p>Thank you for your prompt attention.</p>
|
|
<p><em>Reminder Period:</em> {$monthYear}</p>
|
|
</div>
|
|
HTML;
|
|
|
|
$bodyHtml .= "<ul>"
|
|
. "<li><strong>Remaining installments:</strong> {$installmentInfo['remaining_installments']}</li>"
|
|
. "<li><strong>Suggested installment this month:</strong> {$installmentInfo['installment_due_fmt']}</li>"
|
|
. "<li><strong>Maximum installments available:</strong> {$installmentInfo['max_installments']}</li>"
|
|
. "</ul>";
|
|
|
|
if (view()->exists('emails._wrap_layout')) {
|
|
$body = view('emails._wrap_layout', [
|
|
'title' => 'Monthly Tuition Reminder',
|
|
'body_html' => $bodyHtml,
|
|
], ['saveData' => true]);
|
|
} else {
|
|
$body = $bodyHtml;
|
|
}
|
|
|
|
$ok = $this->emailService->send($email, $subject, (string) $body, 'finance');
|
|
if ($ok && $ccEmail && strcasecmp($ccEmail, $email) !== 0) {
|
|
$this->emailService->send($ccEmail, $subject, (string) $body, 'finance');
|
|
}
|
|
|
|
$existing = PaymentNotificationLog::query()
|
|
->where('parent_id', $parentId)
|
|
->where('period_year', $year)
|
|
->where('period_month', $month)
|
|
->where('type', $type)
|
|
->first();
|
|
|
|
$payload = [
|
|
'parent_id' => $parentId,
|
|
'invoice_id' => $latestInvoiceId,
|
|
'school_year' => $schoolYear,
|
|
'period_year' => $year,
|
|
'period_month' => $month,
|
|
'type' => $type,
|
|
'to_email' => $email,
|
|
'cc_email' => $ccEmail,
|
|
'head_fa_notified' => 0,
|
|
'subject' => $subject,
|
|
'body' => (string) $body,
|
|
'status' => $ok ? 'sent' : 'failed',
|
|
'error_message' => $ok ? null : 'Email send failed (see logs)',
|
|
'balance_snapshot' => $totalBalance,
|
|
'sent_at' => $now->format('Y-m-d H:i:s'),
|
|
];
|
|
|
|
if ($existing) {
|
|
$existing->fill($payload)->save();
|
|
$logId = (int) $existing->id;
|
|
} else {
|
|
$log = PaymentNotificationLog::query()->create($payload);
|
|
$logId = (int) $log->id;
|
|
}
|
|
|
|
return [
|
|
'ok' => $ok,
|
|
'log_id' => $logId,
|
|
'cc_email' => $ccEmail,
|
|
];
|
|
}
|
|
|
|
private function computeInstallmentSuggestion(float $balance, string $tzName): array
|
|
{
|
|
$installmentEndRaw = (string) (Configuration::getConfig('installment_date') ?? '');
|
|
$tz = new DateTimeZone($tzName);
|
|
$today = new DateTimeImmutable('today', $tz);
|
|
|
|
$end = null;
|
|
if ($installmentEndRaw !== '') {
|
|
try {
|
|
$end = new DateTimeImmutable($installmentEndRaw, $tz);
|
|
} catch (\Throwable $e) {
|
|
$end = null;
|
|
}
|
|
}
|
|
|
|
$remMonths = 0;
|
|
if ($end) {
|
|
$y = (int) $end->format('Y') - (int) $today->format('Y');
|
|
$m = (int) $end->format('n') - (int) $today->format('n');
|
|
$remMonths = $y * 12 + $m;
|
|
if ((int) $end->format('j') > (int) $today->format('j')) {
|
|
$remMonths += 1;
|
|
}
|
|
if ($remMonths < 0) {
|
|
$remMonths = 0;
|
|
}
|
|
}
|
|
|
|
$remainingInstallments = max(1, $remMonths);
|
|
$maxInstallments = max($remMonths ?: 0, $balance > 0 ? 2 : 0);
|
|
$installmentDue = $remainingInstallments > 0 ? ($balance / $remainingInstallments) : 0;
|
|
|
|
return [
|
|
'remaining_installments' => $remainingInstallments,
|
|
'max_installments' => $maxInstallments,
|
|
'installment_due' => round($installmentDue, 2),
|
|
'installment_due_fmt' => '$' . number_format($installmentDue, 2),
|
|
];
|
|
}
|
|
|
|
private function findSecondaryGuardianEmail(int $parentId, string $primaryEmail): ?string
|
|
{
|
|
if ($parentId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$row = FamilyGuardian::query()->where('user_id', $parentId)->first();
|
|
if (!$row || empty($row->family_id)) {
|
|
return null;
|
|
}
|
|
|
|
$others = FamilyGuardian::query()
|
|
->where('family_id', (int) $row->family_id)
|
|
->where('user_id', '!=', $parentId)
|
|
->where('receive_emails', 1)
|
|
->get();
|
|
|
|
foreach ($others as $guardian) {
|
|
$email = (string) (User::query()->where('id', (int) $guardian->user_id)->value('email') ?? '');
|
|
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) && strcasecmp($email, $primaryEmail) !== 0) {
|
|
return $email;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function resolveTimezone(): string
|
|
{
|
|
$schoolConfig = config('School');
|
|
if (is_object($schoolConfig) && isset($schoolConfig->attendance['timezone'])) {
|
|
return (string) $schoolConfig->attendance['timezone'];
|
|
}
|
|
if (is_array($schoolConfig) && isset($schoolConfig['attendance']['timezone'])) {
|
|
return (string) $schoolConfig['attendance']['timezone'];
|
|
}
|
|
|
|
return (string) (config('app.timezone') ?: 'UTC');
|
|
}
|
|
}
|