Files
alrahma_sunday_school_api/app/Services/Payments/PaymentTestNotificationService.php
T
2026-06-08 23:30:22 -04:00

213 lines
7.8 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 App\Support\MailHtml;
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>';
$body = MailHtml::document('Monthly Tuition Reminder', $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');
}
}