Files
myep-team/src/Service/Cron/SeasonTaskReminderService.php
T

190 lines
6.6 KiB
PHP

<?php
namespace App\Service\Cron;
use App\Config\EmailTextKey;
use App\Config\SeasonCalendar;
use App\Email\Mailer;
use App\Entity\SeasonAssignmentTask;
use App\Repository\SeasonAssignmentTaskRepository;
use App\Repository\UserRepository;
use App\Service\Season\SeasonAssignmentLabeler;
use App\Service\Season\SeasonTaskDeadlineResolver;
use Psr\Log\LoggerInterface;
/**
* The two reminders around the season checklist: one to the office when a deadline is reached,
* one to the teamer while they still owe us something.
*
* Both are idempotent without recording anything, like UploadReminderService: the office summary
* picks out exactly one day per deadline, and the teamer digest picks out exactly one weekday.
*
* Only "done" stops either of them. An arriving file does not, and neither does the "uploaded"
* tick, which is a note for the office and gates nothing. Neither does doneTeamer on the
* assignment: that flag is informative only.
*/
class SeasonTaskReminderService
{
private const DIGEST_WEEKDAY = 1;
public function __construct(
private readonly SeasonAssignmentTaskRepository $seasonAssignmentTaskRepository,
private readonly SeasonTaskDeadlineResolver $deadlineResolver,
private readonly SeasonAssignmentLabeler $labeler,
private readonly SeasonCalendar $seasonCalendar,
private readonly UserRepository $userRepository,
private readonly Mailer $mailer,
private readonly LoggerInterface $logger,
) {
}
/**
* One mail to the office listing every entry whose deadline is reached today.
*
* Frist 2 defaults to Frist 1, so the two normally fall on the same day. A row is reported
* once in that case and reads as Frist 1: a second, identical line would be noise, and the
* office only pushes the second deadline out deliberately.
*/
public function sendDeadlineSummary(?\DateTimeImmutable $today = null): string
{
$today = ($today ?? new \DateTimeImmutable('today'))->setTime(0, 0);
$violations = [];
foreach ($this->seasonAssignmentTaskRepository->findOpen() as $task) {
if (null === $level = $this->levelDueOn($task, $today)) {
continue;
}
$teamer = (string) $task->getSeasonAssignment()?->getTeamer();
$violations[$teamer][] = [
'task' => $task,
'label' => $task->getTask()?->label() ?? $task->getTaskKey(),
'level' => $level,
];
}
if (0 === $count = count($violations)) {
$message = 'No season task deadlines reached today';
$this->logger->info($message);
return $message;
}
// the two roles that carry ROLE_ADMINISTRATIVE: the repository matches roles as they are
// stored on the user, and nobody is ever stored with the hierarchy-granted one
foreach ($this->userRepository->getUsersByRoles(['ROLE_TEAM_ADMIN', 'ROLE_MANAGER']) as $user) {
if (true === $user->isMuteNotifications()) {
continue;
}
$this->mailer->createAndSendEmail([
'violations' => $violations,
], [
'to' => $user->getEmail(),
'subject' => 'Fristen für Saisonunterlagen erreicht',
'template' => 'email/season_deadlines.html.twig',
]);
}
$message = 'Sent the season task deadline summary for '.$count.' teamers';
$this->logger->info($message);
return $message;
}
/**
* One mail per teamer, every Monday, listing what they still have to send us. It repeats
* until an administrative user ticks the entry off; it is not tied to a deadline at all.
*/
public function sendTeamerDigest(?\DateTimeImmutable $today = null): string
{
$today = ($today ?? new \DateTimeImmutable('today'))->setTime(0, 0);
if (self::DIGEST_WEEKDAY !== (int) $today->format('N')) {
$message = 'Season task digests are only sent on mondays';
$this->logger->info($message);
return $message;
}
// Scoped to the season being prepared, so a teamer is never chased about a season that
// is long over.
[$windowStart, $windowEnd] = $this->seasonCalendar->windowFor(
$this->seasonCalendar->currentOrUpcomingSeasonYear($today)
);
$open = $this->seasonAssignmentTaskRepository->findOpen(
$windowStart->toDateTimeImmutable(),
$windowEnd->toDateTimeImmutable()
);
$perTeamer = [];
foreach ($open as $task) {
$seasonAssignment = $task->getSeasonAssignment();
if (null === $seasonAssignment) {
continue;
}
// office-internal entries never reach the teamer
if (true !== $task->getTask()?->requiresReturn()) {
continue;
}
$perTeamer[$seasonAssignment->getId()]['assignment'] = $seasonAssignment;
$perTeamer[$seasonAssignment->getId()]['labels'][] = $task->getTask()->label();
}
$count = 0;
foreach ($perTeamer as $entry) {
$teamer = $entry['assignment']->getTeamer();
$email = $teamer?->getCommunication()?->getEmail();
if (null === $email) {
continue;
}
$this->mailer->createAndSendText(EmailTextKey::REMINDER_SEASON_TASKS, [
// the placeholder keeps its name: admins can have edited this text in the
// database, and renaming it would blank it out in those stored versions
'destination' => $this->labeler->label($entry['assignment']),
'tasks' => implode(', ', $entry['labels']),
], [
'to' => $email,
]);
++$count;
}
$message = 'Sent '.$count.' season task digests to teamers';
$this->logger->info($message);
return $message;
}
/**
* Which deadline of this entry falls on $day, or null for none. Frist 1 wins when both do.
*/
private function levelDueOn(SeasonAssignmentTask $task, \DateTimeImmutable $day): ?int
{
if ($this->isSameDay($this->deadlineResolver->resolveFirst($task), $day)) {
return 1;
}
if ($this->isSameDay($this->deadlineResolver->resolveSecond($task), $day)) {
return 2;
}
return null;
}
private function isSameDay(?\DateTimeImmutable $deadline, \DateTimeImmutable $day): bool
{
return null !== $deadline && $deadline->format('Y-m-d') === $day->format('Y-m-d');
}
}