feat: notify configured recipients when something is booked
This commit is contained in:
1 parent
be37fc81f6
commit
d287cb2600
23 files changed
+1014
-1
No files matched your search
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
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.
|
||||
*
|
||||
* Recipients are configured per ResourceType rather than per resource or per group, because that
|
||||
* is the level the admin screen groups by and the only one every resource is guaranteed to have:
|
||||
* a resource's group is optional, and a kind that is not split into groups -- rooms, cars --
|
||||
* 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.
|
||||
*/
|
||||
final readonly class BookingNotifier
|
||||
{
|
||||
public function __construct(
|
||||
private MailerInterface $mailer,
|
||||
private ResourceTypeSettingsRepository $settings,
|
||||
private LoggerInterface $logger,
|
||||
private string $sender,
|
||||
) {
|
||||
}
|
||||
|
||||
public function bookingCreated(Booking $booking): void
|
||||
{
|
||||
$resource = $booking->getResource();
|
||||
|
||||
$this->send(
|
||||
$resource->getType(),
|
||||
\sprintf('Neue Buchung: %s am %s', $resource->getLabel(), $booking->getDate()->format('d.m.Y')),
|
||||
'email/booking_created',
|
||||
['booking' => $booking],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One mail for a whole series, not one per occurrence.
|
||||
*
|
||||
* A year of weekly bookings is the same act and would otherwise arrive as fifty-two mails in
|
||||
* one second -- the same reason AuditLogger records a series once rather than per day.
|
||||
*/
|
||||
public function seriesCreated(SeriesResult $result): void
|
||||
{
|
||||
if ($result->isCompletelyBlocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$series = $result->series;
|
||||
$resource = $series->getResource();
|
||||
|
||||
$this->send(
|
||||
$resource->getType(),
|
||||
\sprintf('Neue Serienbuchung: %s ab %s', $resource->getLabel(), $series->getStartDate()->format('d.m.Y')),
|
||||
'email/series_created',
|
||||
['result' => $result, 'series' => $series],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user