diff --git a/app/Services/Mail/MailLane.php b/app/Services/Mail/MailLane.php new file mode 100644 index 0000000..08c849d --- /dev/null +++ b/app/Services/Mail/MailLane.php @@ -0,0 +1,117 @@ + */ + private const DEFAULTS = [ + MaintenanceAnnouncementMail::class => self::URGENT, + MaintenanceCancelledMail::class => self::URGENT, + CloudSuspendedMail::class => self::URGENT, + CloudResumedMail::class => self::URGENT, + InvoiceMail::class => self::CALM, + DunningNoticeMail::class => self::CALM, + DormantAccountWarningMail::class => self::CALM, + ]; + + /** Die Schlange, in die diese Mailklasse gehört. */ + public static function for(string $mailableClass): string + { + if (self::isLocked($mailableClass)) { + return self::DIRECT; + } + + $stored = (array) Settings::get('mail.lanes', []); + $lane = $stored[$mailableClass] ?? self::DEFAULTS[$mailableClass] ?? self::CALM; + + return in_array($lane, [self::DIRECT, self::URGENT, self::CALM], true) ? $lane : self::CALM; + } + + public static function isLocked(string $mailableClass): bool + { + return in_array($mailableClass, self::LOCKED, true); + } + + /** Verschiebt eine Mailklasse. Wirft, wenn sie gesperrt ist. */ + public static function assign(string $mailableClass, string $lane): void + { + if (self::isLocked($mailableClass)) { + throw new RuntimeException( + "{$mailableClass} fährt immer direkt: darauf wartet gerade jemand." + ); + } + + $stored = (array) Settings::get('mail.lanes', []); + $stored[$mailableClass] = $lane; + Settings::set('mail.lanes', $stored); + } + + /** + * Jede bekannte Mailklasse mit ihrer Spur, für die Konsole. + * + * @return array + */ + public static function all(): array + { + $classes = array_merge(self::LOCKED, array_keys(self::DEFAULTS)); + + return collect($classes) + ->mapWithKeys(fn (string $class) => [$class => self::for($class)]) + ->all(); + } +} diff --git a/tests/Feature/Mail/MailLaneTest.php b/tests/Feature/Mail/MailLaneTest.php new file mode 100644 index 0000000..c95130a --- /dev/null +++ b/tests/Feature/Mail/MailLaneTest.php @@ -0,0 +1,50 @@ +toBe(MailLane::DIRECT) + ->and(MailLane::for(MaintenanceAnnouncementMail::class))->toBe(MailLane::URGENT) + ->and(MailLane::for(InvoiceMail::class))->toBe(MailLane::CALM) + ->and(MailLane::for(DunningNoticeMail::class))->toBe(MailLane::CALM); +}); + +it('schickt eine unbekannte Mailklasse in die ruhige Spur', function () { + expect(MailLane::for('App\\Mail\\GibtEsNichtMail'))->toBe(MailLane::CALM); +}); + +it('lässt den Betreiber eine Mail verschieben', function () { + MailLane::assign(InvoiceMail::class, MailLane::URGENT); + + expect(MailLane::for(InvoiceMail::class))->toBe(MailLane::URGENT); +}); + +it('sperrt die Mails, auf die jemand wartet', function () { + expect(MailLane::isLocked(ResetPasswordMail::class))->toBeTrue() + ->and(MailLane::isLocked(InvoiceMail::class))->toBeFalse(); + + expect(fn () => MailLane::assign(ResetPasswordMail::class, MailLane::CALM)) + ->toThrow(RuntimeException::class); + + expect(MailLane::for(ResetPasswordMail::class))->toBe(MailLane::DIRECT); +}); + +it('lässt eine gespeicherte Zuordnung eine gesperrte Klasse nicht überschreiben', function () { + // Nicht über assign(), sondern direkt in die Einstellung geschrieben — so + // sähe es aus, wenn jemand an der Prüfung vorbei schreibt. + Settings::set('mail.lanes', [ResetPasswordMail::class => MailLane::CALM]); + + expect(MailLane::for(ResetPasswordMail::class))->toBe(MailLane::DIRECT); +});