Drei Spuren, und welche Mail in welche gehoert
Die Trennung laeuft zwischen dringend und nicht dringend, nicht zwischen Massenversand und Einzelmail: eine Ausfallmeldung geht an alle UND eilt. Die sieben Mails, auf die jemand wartet, sind gesperrt statt nur voreingestellt — auch eine von Hand geschriebene Einstellung verschiebt sie nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>claude/nice-moser-521659
parent
2ab6bd22a3
commit
4db1957f06
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Mail;
|
||||||
|
|
||||||
|
use App\Mail\CloudResumedMail;
|
||||||
|
use App\Mail\CloudSuspendedMail;
|
||||||
|
use App\Mail\ContactRequestMail;
|
||||||
|
use App\Mail\DormantAccountWarningMail;
|
||||||
|
use App\Mail\DunningNoticeMail;
|
||||||
|
use App\Mail\InvoiceMail;
|
||||||
|
use App\Mail\MaintenanceAnnouncementMail;
|
||||||
|
use App\Mail\MaintenanceCancelledMail;
|
||||||
|
use App\Mail\NewDeviceSignInMail;
|
||||||
|
use App\Mail\OperatorMessageMail;
|
||||||
|
use App\Mail\OrderConfirmationMail;
|
||||||
|
use App\Mail\ResetPasswordMail;
|
||||||
|
use App\Mail\SecurityBlockMail;
|
||||||
|
use App\Mail\VerifyEmailMail;
|
||||||
|
use App\Support\Settings;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In welcher Spur eine Mail fährt.
|
||||||
|
*
|
||||||
|
* Drei Spuren, und die Trennung läuft nicht zwischen Massenversand und
|
||||||
|
* Einzelmail, sondern zwischen dringend und nicht dringend. Eine
|
||||||
|
* Ausfallmeldung geht an alle UND eilt — sie bekommt deshalb ein höheres
|
||||||
|
* Kontingent statt gar keines.
|
||||||
|
*
|
||||||
|
* Die Vorgaben stehen hier, die Änderung des Betreibers darüber in den
|
||||||
|
* Einstellungen. Eine unbekannte Klasse fällt in die ruhige Spur: eine zu
|
||||||
|
* langsam verschickte Mail ist ein kleinerer Fehler als ein ungedrosselter
|
||||||
|
* Schub, den niemand vorhergesehen hat.
|
||||||
|
*/
|
||||||
|
final class MailLane
|
||||||
|
{
|
||||||
|
public const DIRECT = 'mail-direkt';
|
||||||
|
|
||||||
|
public const URGENT = 'mail-wichtig';
|
||||||
|
|
||||||
|
public const CALM = 'mail-ruhig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die sieben, auf die gerade jemand wartet.
|
||||||
|
*
|
||||||
|
* Sie entstehen einzeln, weil ein einzelner Mensch geklickt hat, und
|
||||||
|
* können gar keinen Schub bilden. Drosseln nützt dort nichts und kostet
|
||||||
|
* einen Supportfall je verzögertem Kennwort — deshalb sind sie gesperrt
|
||||||
|
* und nicht nur voreingestellt.
|
||||||
|
*/
|
||||||
|
private const LOCKED = [
|
||||||
|
ResetPasswordMail::class,
|
||||||
|
VerifyEmailMail::class,
|
||||||
|
NewDeviceSignInMail::class,
|
||||||
|
SecurityBlockMail::class,
|
||||||
|
ContactRequestMail::class,
|
||||||
|
OrderConfirmationMail::class,
|
||||||
|
OperatorMessageMail::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var array<class-string, string> */
|
||||||
|
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<class-string, string>
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Mail\DunningNoticeMail;
|
||||||
|
use App\Mail\InvoiceMail;
|
||||||
|
use App\Mail\MaintenanceAnnouncementMail;
|
||||||
|
use App\Mail\ResetPasswordMail;
|
||||||
|
use App\Services\Mail\MailLane;
|
||||||
|
use App\Support\Settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Welche Mail in welcher Spur fährt.
|
||||||
|
*
|
||||||
|
* Die Vorgaben stehen im Code, die Änderung des Betreibers darüber. Gesperrt
|
||||||
|
* sind die Mails, auf die gerade ein Mensch wartet: ein Kennwort-Zurücksetzen,
|
||||||
|
* das zwanzig Minuten liegt, ist ein Supportfall und kein gespartes Ansehen.
|
||||||
|
*/
|
||||||
|
it('kennt die Vorgabe jeder Mailklasse', function () {
|
||||||
|
expect(MailLane::for(ResetPasswordMail::class))->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);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue