feat: all-season assignments
This commit is contained in:
1 parent
675cec1b69
commit
cf876ed224
119 files changed
+6194
-294
No files matched your search
@@ -0,0 +1,189 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user