feat: let admins notify all users about updated access codes

This commit is contained in:
fromme committed 2026-10-15 10:49:09 +02:00
1 parent a957e0d662
commit d68b6b156c
16 files changed
+458 -59

No files matched your search

+5 -51
View File
@@ -4,14 +4,11 @@ declare(strict_types=1);
namespace App\Service;
use App\Email\Mailer;
use App\Entity\Booking;
use App\Enum\ResourceType;
use App\Model\SeriesResult;
use App\Repository\ResourceTypeSettingsRepository;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
/**
* Tells whoever an administrator named that something was booked.
@@ -22,25 +19,14 @@ use Symfony\Component\Mime\Address;
* would otherwise never notify anybody.
*
* **Nothing here may break a booking.** It runs after the transaction has committed, so a
* failure cannot roll anything back, and a transport that is down is logged rather than thrown:
* the booking is made, and the person who made it should not be shown an error about somebody
* else's mail server.
*
* Every recipient gets their own mail, addressed only to them. Nobody sees who else is on the
* list, and a single bad address loses its own mail rather than everybody's.
*
* Each mail is multipart: the MyE&P layout for clients that render HTML, and a hand-written text
* part for those that do not. The text part is written rather than generated because there is no
* html-to-markdown converter installed, and three facts and a name read better as plain lines
* than as a flattened table anyway.
* failure cannot roll anything back -- and Mailer never throws, so the person who booked is not
* shown an error about somebody else's mail server.
*/
final readonly class BookingNotifier
{
public function __construct(
private MailerInterface $mailer,
private Mailer $mailer,
private ResourceTypeSettingsRepository $settings,
private LoggerInterface $logger,
private string $sender,
) {
}
@@ -80,42 +66,10 @@ final readonly class BookingNotifier
}
/**
* @param string $template the template pair's basename; both .html.twig and
* .txt.twig of it are rendered
* @param array<string, mixed> $context
*/
private function send(ResourceType $type, string $subject, string $template, array $context): void
{
$recipients = $this->settings->recipientsFor($type);
if ([] === $recipients) {
return;
}
foreach ($recipients as $recipient) {
// A fresh message per recipient: ->to() on a reused one appends rather than replaces,
// which would put the whole list back on everybody's copy.
$email = (new TemplatedEmail())
->from(new Address($this->sender, 'E&P Platz'))
->to($recipient)
->subject($subject)
->htmlTemplate($template.'.html.twig')
->textTemplate($template.'.txt.twig')
->context($context);
try {
$this->mailer->send($email);
} catch (\Throwable $e) {
// Caught per recipient, so one unreachable address does not cost the others theirs.
// \Throwable rather than TransportExceptionInterface because the templates are
// rendered inside send(): a typo in one of them would otherwise surface as a failed
// booking, which is exactly what this class promises never to do.
$this->logger->error('Booking notification could not be sent.', [
'subject' => $subject,
'recipient' => $recipient,
'exception' => $e,
]);
}
}
$this->mailer->send($this->settings->recipientsFor($type), $subject, $template, $context);
}
}