feat: complete email infrastructure with queue, templates, logging, and API
Adds the full transactional email system:
- Redis queue (QUEUE_CONNECTION=redis), SES config in .env.example
- 3 migrations: organisation_email_settings, organisation_email_templates, email_logs
- EmailTemplateType and EmailLogStatus enums with Dutch defaults
- EmailService as central entry point for all email sending
- SendTransactionalEmail queued job with retries and idempotency
- TransactionalMail mailable with responsive HTML + plain text templates
- Organisation-level branding (colors, logo, footer, reply-to)
- Per-type template overrides with {variable} substitution
- Email log with filtering by status, type, date range, recipient
- Preview and send-test endpoints for template management
- API endpoints: email-settings, email-templates (CRUD), email-logs (read-only)
- Integrated into existing flows: invitations, password reset, email
verification, registration approval/rejection
- 37 new tests across 4 test files, all existing tests updated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
api/app/Enums/EmailLogStatus.php
Normal file
12
api/app/Enums/EmailLogStatus.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum EmailLogStatus: string
|
||||
{
|
||||
case QUEUED = 'queued';
|
||||
case SENT = 'sent';
|
||||
case FAILED = 'failed';
|
||||
}
|
||||
75
api/app/Enums/EmailTemplateType.php
Normal file
75
api/app/Enums/EmailTemplateType.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum EmailTemplateType: string
|
||||
{
|
||||
case INVITATION = 'invitation';
|
||||
case PASSWORD_RESET = 'password_reset';
|
||||
case EMAIL_VERIFICATION = 'email_verification';
|
||||
case REGISTRATION_APPROVED = 'registration_approved';
|
||||
case REGISTRATION_REJECTED = 'registration_rejected';
|
||||
case SHIFT_ASSIGNMENT = 'shift_assignment';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::INVITATION => 'Uitnodiging',
|
||||
self::PASSWORD_RESET => 'Wachtwoord resetten',
|
||||
self::EMAIL_VERIFICATION => 'E-mailadres verificatie',
|
||||
self::REGISTRATION_APPROVED => 'Registratie goedgekeurd',
|
||||
self::REGISTRATION_REJECTED => 'Registratie afgewezen',
|
||||
self::SHIFT_ASSIGNMENT => 'Diensttoewijzing',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Default template content per type (Dutch).
|
||||
* Used when no organisation-specific override exists.
|
||||
*
|
||||
* @return array{subject: string, heading: string, body_text: string, button_text: string|null}
|
||||
*/
|
||||
public function defaults(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::INVITATION => [
|
||||
'subject' => 'Je bent uitgenodigd voor {organisation_name}',
|
||||
'heading' => 'Welkom bij {organisation_name}!',
|
||||
'body_text' => 'Je bent uitgenodigd om lid te worden van {organisation_name} op Crewli. Klik op de knop hieronder om je uitnodiging te accepteren en een account aan te maken.',
|
||||
'button_text' => 'Uitnodiging accepteren',
|
||||
],
|
||||
self::PASSWORD_RESET => [
|
||||
'subject' => 'Wachtwoord resetten',
|
||||
'heading' => 'Wachtwoord resetten',
|
||||
'body_text' => 'Je hebt een verzoek ingediend om je wachtwoord te resetten. Klik op de knop hieronder om een nieuw wachtwoord in te stellen. Deze link is 60 minuten geldig.',
|
||||
'button_text' => 'Wachtwoord resetten',
|
||||
],
|
||||
self::EMAIL_VERIFICATION => [
|
||||
'subject' => 'Bevestig je nieuwe e-mailadres',
|
||||
'heading' => 'E-mailadres bevestigen',
|
||||
'body_text' => 'Klik op de knop hieronder om je nieuwe e-mailadres te bevestigen. Als je dit niet hebt aangevraagd, kun je deze email negeren.',
|
||||
'button_text' => 'E-mailadres bevestigen',
|
||||
],
|
||||
self::REGISTRATION_APPROVED => [
|
||||
'subject' => 'Je registratie voor {event_name} is goedgekeurd!',
|
||||
'heading' => 'Welkom aan boord!',
|
||||
'body_text' => 'Goed nieuws! Je registratie als vrijwilliger voor {event_name} is goedgekeurd. Je kunt nu inloggen op het portaal om je diensten te bekijken en te claimen.',
|
||||
'button_text' => 'Ga naar het portaal',
|
||||
],
|
||||
self::REGISTRATION_REJECTED => [
|
||||
'subject' => 'Update over je registratie voor {event_name}',
|
||||
'heading' => 'Helaas...',
|
||||
'body_text' => 'Bedankt voor je interesse om als vrijwilliger mee te helpen bij {event_name}. Helaas kunnen we je registratie op dit moment niet goedkeuren. Neem contact op met de organisatie als je vragen hebt.',
|
||||
'button_text' => null,
|
||||
],
|
||||
self::SHIFT_ASSIGNMENT => [
|
||||
'subject' => 'Je bent ingedeeld voor een dienst bij {event_name}',
|
||||
'heading' => 'Nieuwe diensttoewijzing',
|
||||
'body_text' => 'Je bent ingedeeld voor de volgende dienst: {shift_title} op {shift_date} van {shift_start} tot {shift_end} bij {section_name}. Log in op het portaal voor meer details.',
|
||||
'button_text' => 'Bekijk je diensten',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user