190 lines
6.6 KiB
PHP
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');
|
|
}
|
|
}
|