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>feat/versandtakt
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