Files
alrahma_sunday_school/app/Services/RegistrationOpeningEmailService.php
T
root 608aca79b8
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Failing after 1m19s
fix enrollment, invoice, payment and financila aid
2026-08-24 21:20:58 -04:00

314 lines
12 KiB
PHP

<?php
namespace App\Services;
use CodeIgniter\Database\BaseConnection;
/**
* @deprecated Use EnrollmentRegistrationEmailService instead. This legacy sender is retained for reference only.
*/
class RegistrationOpeningEmailService
{
public const TEMPLATE_KEY = 'registration_opening';
private BaseConnection $db;
private EmailService $emailService;
public function __construct(?BaseConnection $db = null, ?EmailService $emailService = null)
{
$this->db = $db ?? \Config\Database::connect();
$this->emailService = $emailService ?? new EmailService();
}
public function sendForDate(\DateTimeInterface $date, bool $force = false, ?string $testEmail = null, bool $dryRun = false): array
{
$summary = [
'school_years' => 0,
'recipients' => 0,
'sent' => 0,
'failed' => 0,
'skipped' => 0,
'dry_run' => $dryRun,
'messages' => [],
];
foreach ($this->registrationYearsForDate($date, $force) as $schoolYear) {
$summary['school_years']++;
$recipients = $testEmail !== null
? [[
'user_id' => null,
'family_id' => null,
'email' => $testEmail,
'name' => 'Parent',
]]
: $this->parentEmailRecipients();
if ($recipients === []) {
$summary['messages'][] = sprintf('No parent emails found for %s.', $schoolYear['name']);
continue;
}
[$subject, $body] = $this->composeEmail($schoolYear);
if ($testEmail !== null) {
$subject = '[TEST] ' . $subject;
}
foreach ($recipients as $recipient) {
if ($testEmail === null && ! $dryRun && $this->alreadySentForRecipient($schoolYear, $recipient['email'])) {
$summary['skipped']++;
continue;
}
$summary['recipients']++;
$personalizedBody = $this->replaceTokens($body, $schoolYear, $recipient);
$personalizedSubject = $this->replaceTokens($subject, $schoolYear, $recipient);
$sent = $dryRun || $this->emailService->send($recipient['email'], $personalizedSubject, $personalizedBody, 'general');
$sent ? $summary['sent']++ : $summary['failed']++;
if (! $dryRun) {
$this->logAttempt($schoolYear, $recipient, $personalizedSubject, $personalizedBody, $sent);
}
}
}
return $summary;
}
public function registrationYearsForDate(\DateTimeInterface $date, bool $force = false): array
{
$builder = $this->db->table('school_years')
->where('registration_starts_on IS NOT NULL');
if (! $force) {
$builder->where('registration_starts_on', $date->format('Y-m-d'));
}
return $builder->orderBy('registration_starts_on', 'ASC')
->get()
->getResultArray();
}
public function parentEmailRecipients(): array
{
$recipients = [];
$hasFamilyGuardians = $this->db->tableExists('family_guardians');
if ($this->db->tableExists('roles') && $this->db->tableExists('user_roles')) {
$builder = $this->db->table('users u')
->select($hasFamilyGuardians ? 'u.id AS user_id, fg.family_id, u.firstname, u.lastname, u.email' : 'u.id AS user_id, NULL AS family_id, u.firstname, u.lastname, u.email')
->join('user_roles ur', 'ur.user_id = u.id', 'inner')
->join('roles r', 'r.id = ur.role_id', 'inner')
->where('LOWER(r.name)', 'parent')
->where('u.email IS NOT NULL')
->where('u.email !=', '');
if ($hasFamilyGuardians) {
$builder->join('family_guardians fg', 'fg.user_id = u.id', 'left')
->groupStart()
->where('fg.id IS NULL')
->orWhere('fg.receive_emails', 1)
->groupEnd();
}
if ($this->hasField('user_roles', 'deleted_at')) {
$builder->where('ur.deleted_at', null);
}
foreach ($builder->get()->getResultArray() as $row) {
$this->addRecipient($recipients, $row);
}
}
if ($hasFamilyGuardians) {
$guardianBuilder = $this->db->table('family_guardians fg')
->select('u.id AS user_id, fg.family_id, u.firstname, u.lastname, u.email')
->join('users u', 'u.id = fg.user_id', 'inner')
->where('fg.receive_emails', 1)
->where('u.email IS NOT NULL')
->where('u.email !=', '');
foreach ($guardianBuilder->get()->getResultArray() as $row) {
$this->addRecipient($recipients, $row);
}
}
uasort($recipients, static fn(array $a, array $b): int => strcasecmp($a['email'], $b['email']));
return array_values($recipients);
}
public function alreadySentForSchoolYear(array $schoolYear): bool
{
$schoolYearId = (int) ($schoolYear['id'] ?? 0);
$schoolYearName = (string) ($schoolYear['name'] ?? '');
$builder = $this->db->table('communication_logs')
->where('template_key', self::TEMPLATE_KEY)
->where('status', 'sent')
->groupStart();
if ($schoolYearId > 0) {
$builder->like('metadata', '"school_year_id":' . $schoolYearId);
}
if ($schoolYearName !== '') {
if ($schoolYearId > 0) {
$builder->orLike('metadata', '"school_year":"' . $this->escapeJsonLikeValue($schoolYearName) . '"');
} else {
$builder->like('metadata', '"school_year":"' . $this->escapeJsonLikeValue($schoolYearName) . '"');
}
}
$builder->groupEnd();
return $builder->countAllResults() > 0;
}
public function alreadySentForRecipient(array $schoolYear, string $email): bool
{
$schoolYearId = (int) ($schoolYear['id'] ?? 0);
$schoolYearName = (string) ($schoolYear['name'] ?? '');
$builder = $this->db->table('communication_logs')
->where('template_key', self::TEMPLATE_KEY)
->where('status', 'sent')
->like('recipients', '"' . strtolower(trim($email)) . '"')
->groupStart();
if ($schoolYearId > 0) {
$builder->like('metadata', '"school_year_id":' . $schoolYearId);
}
if ($schoolYearName !== '') {
if ($schoolYearId > 0) {
$builder->orLike('metadata', '"school_year":"' . $this->escapeJsonLikeValue($schoolYearName) . '"');
} else {
$builder->like('metadata', '"school_year":"' . $this->escapeJsonLikeValue($schoolYearName) . '"');
}
}
$builder->groupEnd();
return $builder->countAllResults() > 0;
}
private function addRecipient(array &$recipients, array $row): void
{
$email = strtolower(trim((string) ($row['email'] ?? '')));
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
return;
}
$name = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? ''));
$recipients[$email] = [
'user_id' => isset($row['user_id']) ? (int) $row['user_id'] : null,
'family_id' => isset($row['family_id']) ? (int) $row['family_id'] : null,
'email' => $email,
'name' => $name !== '' ? $name : 'Parent',
];
}
private function composeEmail(array $schoolYear): array
{
$template = $this->loadTemplate();
$subject = $template['subject'] ?? 'Registration is now open for {{school_year}}';
$bodyHtml = $template['body'] ?? $this->defaultBody();
$bodyHtml = $this->replaceTokens($bodyHtml, $schoolYear, ['name' => 'Parent', 'email' => '']);
$body = view('emails/_wrap_layout', [
'title' => $this->replaceTokens($subject, $schoolYear, ['name' => 'Parent', 'email' => '']),
'body_html' => $bodyHtml,
], ['saveData' => true]);
return [$subject, $body];
}
private function loadTemplate(): ?array
{
if (! $this->db->tableExists('email_templates')) {
return null;
}
$fields = $this->db->getFieldNames('email_templates');
$keyField = in_array('code', $fields, true) ? 'code' : 'template_key';
$bodyField = in_array('body_html', $fields, true) ? 'body_html' : 'body';
$row = $this->db->table('email_templates')
->select('subject, ' . $bodyField . ' AS body')
->where($keyField, self::TEMPLATE_KEY)
->where('is_active', 1)
->get()
->getRowArray();
return $row ?: null;
}
private function replaceTokens(string $text, array $schoolYear, array $recipient): string
{
$registrationEnds = trim((string) ($schoolYear['registration_ends_on'] ?? ''));
$deadline = $registrationEnds !== '' ? $registrationEnds : 'the posted deadline';
return strtr($text, [
'{{name}}' => (string) ($recipient['name'] ?? 'Parent'),
'{{parent_name}}' => (string) ($recipient['name'] ?? 'Parent'),
'{{school_year}}' => (string) ($schoolYear['name'] ?? ''),
'{{registration_starts_on}}' => (string) ($schoolYear['registration_starts_on'] ?? ''),
'{{registration_ends_on}}' => $registrationEnds,
'{{registration_deadline}}' => $deadline,
'{{registration_url}}' => site_url('user'),
'{{login_url}}' => site_url('login'),
'{{school_name}}' => 'Al Rahma Sunday School',
]);
}
private function defaultBody(): string
{
return <<<'HTML'
<p>Dear {{name}},</p>
<p>Registration is now open for the {{school_year}} school year.</p>
<p>Please complete your registration through the school portal. The registration deadline is {{registration_deadline}}.</p>
<p><a href="{{registration_url}}">Start registration</a></p>
<p>If you already have an account, you can also log in here: <a href="{{login_url}}">{{login_url}}</a></p>
<p>Regards,<br>{{school_name}}</p>
HTML;
}
private function logAttempt(array $schoolYear, array $recipient, string $subject, string $body, bool $sent): void
{
$this->db->table('communication_logs')->insert([
'student_id' => 0,
'family_id' => $recipient['family_id'] ?: null,
'student_name' => 'All Families',
'template_key' => self::TEMPLATE_KEY,
'subject' => $subject,
'body' => $body,
'recipients' => json_encode([$recipient['email']]),
'status' => $sent ? 'sent' : 'failed',
'error_message' => $sent ? null : 'Email send failed',
'sent_by' => null,
'metadata' => json_encode([
'campaign' => self::TEMPLATE_KEY,
'school_year_id' => (int) ($schoolYear['id'] ?? 0),
'school_year' => (string) ($schoolYear['name'] ?? ''),
'registration_starts_on' => $schoolYear['registration_starts_on'] ?? null,
'registration_ends_on' => $schoolYear['registration_ends_on'] ?? null,
'recipient_user_id' => $recipient['user_id'],
]),
]);
}
private function hasField(string $table, string $field): bool
{
return in_array($field, $this->db->getFieldNames($table), true);
}
private function escapeJsonLikeValue(string $value): string
{
return trim(json_encode($value), '"');
}
}