fix: correctly apply due dates for contract and invoice upload reminders
This commit is contained in:
@@ -4,7 +4,7 @@ namespace App\Service\Assignment;
|
||||
|
||||
use App\Entity\Assignment;
|
||||
use App\Model\TimelineItem;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Carbon\CarbonPeriodImmutable;
|
||||
|
||||
/**
|
||||
* Service for processing assignments into timeline visualization data.
|
||||
@@ -110,13 +110,13 @@ class TimelineService
|
||||
* Returns a period from 6 weeks ago to 6 months in the future,
|
||||
* providing context for both recent and upcoming assignments.
|
||||
*
|
||||
* @return CarbonPeriod The active time window for timeline display
|
||||
* @return CarbonPeriodImmutable The active time window for timeline display
|
||||
*/
|
||||
public function getActiveWindow(): CarbonPeriod
|
||||
public function getActiveWindow(): CarbonPeriodImmutable
|
||||
{
|
||||
$dateFrom = (new \DateTimeImmutable())->modify('-6 weeks');
|
||||
$dateTo = (new \DateTimeImmutable())->modify('+6 months');
|
||||
|
||||
return CarbonPeriod::create($dateFrom, $dateTo);
|
||||
return CarbonPeriodImmutable::create($dateFrom, $dateTo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ namespace App\Service\Common;
|
||||
|
||||
use App\Model\AbstractFilterDto;
|
||||
use App\Model\TimelineFilterDto;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Carbon\CarbonPeriodImmutable;
|
||||
|
||||
class TimelineFilterHandler extends AbstractFilterHandler
|
||||
{
|
||||
protected string $namespace = 'filter:timeline';
|
||||
protected string $modelClass = TimelineFilterDto::class;
|
||||
|
||||
protected ?CarbonPeriod $period = null;
|
||||
protected ?CarbonPeriodImmutable $period = null;
|
||||
|
||||
public function setPeriod(CarbonPeriod $period): void
|
||||
public function setPeriod(CarbonPeriodImmutable $period): void
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class TimelineFilterHandler extends AbstractFilterHandler
|
||||
$filterDto->setHotels($data['hotels']);
|
||||
}
|
||||
|
||||
$filterDto->setPeriod(new CarbonPeriod($periodFrom, $periodTo));
|
||||
$filterDto->setPeriod(new CarbonPeriodImmutable($periodFrom, $periodTo));
|
||||
|
||||
return $filterDto;
|
||||
}
|
||||
|
||||
@@ -5,61 +5,35 @@ namespace App\Service\Cron;
|
||||
use App\Config\EmailTextKey;
|
||||
use App\Email\Mailer;
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\Upload;
|
||||
use App\Repository\DispositionRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* The nightly reminders about documents a teamer still owes us.
|
||||
*
|
||||
* Both rules pick out exactly one day, which is what makes them idempotent: the cron can run
|
||||
* twice without mailing anyone twice, and nothing has to record what was already sent.
|
||||
*/
|
||||
class UploadReminderService
|
||||
{
|
||||
/**
|
||||
* How long before the invoice deadline the reminder goes out.
|
||||
*/
|
||||
private const INVOICE_REMINDER_LEAD_DAYS = 3;
|
||||
|
||||
public function __construct(
|
||||
private readonly DispositionRepository $dispositionRepository,
|
||||
private readonly Mailer $mailer,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly int $contractUploadDeadlineDays,
|
||||
private readonly int $invoiceUploadDeadlineDays,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reminds about a missing contract on the last day of the upload period, counted from the
|
||||
* creation of the disposition.
|
||||
*/
|
||||
public function sendContractUploadReminders(): string
|
||||
{
|
||||
// Contracts have to be uploaded before assignments' begin date. Reminder is
|
||||
// sent 7 and 14 days after disposition has been created.
|
||||
$contractDueDateFirst = (new \DateTimeImmutable())->modify('-7 days');
|
||||
$contractDueDateSecond = (new \DateTimeImmutable())->modify('-14 days');
|
||||
|
||||
// Find dispositions of assignments with due contract upload
|
||||
$qb = $this->dispositionRepository->createQueryBuilder('disposition');
|
||||
|
||||
$dispositions = $qb
|
||||
->select('disposition', 'assignment', 'destination')
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->leftJoin('disposition.documents', 'document', Join::WITH, 'document.type = :documentType')
|
||||
->innerJoin('disposition.teamer', 'teamer')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->isNull('document'),
|
||||
// deleted teamers are excluded here rather than when sending, so that the
|
||||
// count reported below stays truthful
|
||||
$qb->expr()->isNull('teamer.deletedAt'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('document.createdAt', ':contractDueDateFirst'),
|
||||
$qb->expr()->eq('document.createdAt', ':contractDueDateSecond'),
|
||||
)
|
||||
))
|
||||
->setParameters(new ArrayCollection([
|
||||
new Parameter('documentType', Upload::TYPE_CONTRACT),
|
||||
new Parameter('contractDueDateFirst', $contractDueDateFirst),
|
||||
new Parameter('contractDueDateSecond', $contractDueDateSecond),
|
||||
]))
|
||||
->getQuery()
|
||||
->getResult()
|
||||
$dispositions = $this
|
||||
->dispositionRepository
|
||||
->findDispositionsForContractReminder($this->contractUploadDeadlineDays)
|
||||
;
|
||||
|
||||
if (0 === $count = count($dispositions)) {
|
||||
@@ -70,12 +44,8 @@ class UploadReminderService
|
||||
}
|
||||
|
||||
foreach ($dispositions as $disposition) {
|
||||
$teamer = $disposition->getTeamer();
|
||||
|
||||
$this->mailer->createAndSendText(EmailTextKey::REMINDER_CONTRACT_UPLOAD, [
|
||||
$this->send(EmailTextKey::REMINDER_CONTRACT_UPLOAD, $disposition, [
|
||||
'destination' => (string) $disposition->getAssignment()->getDestination(),
|
||||
], [
|
||||
'to' => $teamer->getCommunication()->getEmail(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -85,65 +55,58 @@ class UploadReminderService
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reminds about a missing invoice twice: the day after the assignment ended, and again on
|
||||
* the last day of the upload period. The second mail has its own wording, because by then
|
||||
* there is no time left to promise.
|
||||
*/
|
||||
public function sendInvoiceUploadReminders(): string
|
||||
{
|
||||
// Invoices have to be uploaded until invoiceUploadDeadlineDays after end of
|
||||
// assignment. Reminder is sent three days before end of this period, so the
|
||||
// offset follows the deadline the mail itself quotes.
|
||||
$invoiceDueDate = (new \DateTimeImmutable())
|
||||
->modify(sprintf('-%d days', $this->invoiceUploadDeadlineDays - self::INVOICE_REMINDER_LEAD_DAYS))
|
||||
;
|
||||
$today = new \DateTimeImmutable('today');
|
||||
|
||||
// Find dispositions of assignments with due invoice upload
|
||||
$qb = $this->dispositionRepository->createQueryBuilder('disposition');
|
||||
$count = $this->sendInvoiceUploadRemindersForEndDate(
|
||||
$today->modify('-1 day'),
|
||||
EmailTextKey::REMINDER_INVOICE_UPLOAD
|
||||
);
|
||||
|
||||
$dispositions = $qb
|
||||
->select('disposition', 'assignment', 'destination')
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->leftJoin('disposition.documents', 'document', Join::WITH, 'document.type = :documentType')
|
||||
->innerJoin('disposition.teamer', 'teamer')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->isNull('document'),
|
||||
$qb->expr()->isNull('teamer.deletedAt'),
|
||||
$qb->expr()->orX(
|
||||
// Due date can be determined by assignment's date which potentially overrides destination date
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->eq('assignment.dateTo', ':dateTo')
|
||||
),
|
||||
$qb->expr()->eq('destination.dateTo', ':dateTo'),
|
||||
)
|
||||
))
|
||||
->setParameters(new ArrayCollection([
|
||||
new Parameter('documentType', Upload::TYPE_INVOICE),
|
||||
new Parameter('dateTo', $invoiceDueDate),
|
||||
]))
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
$count += $this->sendInvoiceUploadRemindersForEndDate(
|
||||
$today->modify(sprintf('-%d days', $this->invoiceUploadDeadlineDays)),
|
||||
EmailTextKey::REMINDER_INVOICE_UPLOAD_FINAL
|
||||
);
|
||||
|
||||
if (0 === $count = count($dispositions)) {
|
||||
$message = 'No due invoice upload reminders to be sent to teamers';
|
||||
$this->logger->info($message);
|
||||
$message = 0 === $count
|
||||
? 'No due invoice upload reminders to be sent to teamers'
|
||||
: 'Sent '.$count.' due invoice upload reminders to teamers';
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
foreach ($dispositions as $disposition) {
|
||||
$teamer = $disposition->getTeamer();
|
||||
|
||||
$this->mailer->createAndSendText(EmailTextKey::REMINDER_INVOICE_UPLOAD, [
|
||||
'destination' => (string) $disposition->getAssignment()->getDestination(),
|
||||
'invoiceUploadDeadlineDays' => $this->invoiceUploadDeadlineDays,
|
||||
], [
|
||||
'to' => $teamer->getCommunication()->getEmail(),
|
||||
]);
|
||||
}
|
||||
|
||||
$message = 'Sent '.$count.' due invoice upload reminders to teamers';
|
||||
$this->logger->info($message);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
private function sendInvoiceUploadRemindersForEndDate(\DateTimeImmutable $endDate, EmailTextKey $key): int
|
||||
{
|
||||
$dispositions = $this
|
||||
->dispositionRepository
|
||||
->findDispositionsForInvoiceReminder($endDate)
|
||||
;
|
||||
|
||||
foreach ($dispositions as $disposition) {
|
||||
$this->send($key, $disposition, [
|
||||
'destination' => (string) $disposition->getAssignment()->getDestination(),
|
||||
'invoiceUploadDeadlineDays' => $this->invoiceUploadDeadlineDays,
|
||||
]);
|
||||
}
|
||||
|
||||
return count($dispositions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|int|null> $placeholders
|
||||
*/
|
||||
private function send(EmailTextKey $key, Disposition $disposition, array $placeholders): void
|
||||
{
|
||||
$this->mailer->createAndSendText($key, $placeholders, [
|
||||
'to' => $disposition->getTeamer()->getCommunication()->getEmail(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Service\Teamer;
|
||||
|
||||
use App\Entity\Availability;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
class AvailabilityProcessor
|
||||
{
|
||||
@@ -28,7 +28,7 @@ class AvailabilityProcessor
|
||||
/** @var Availability $record */
|
||||
$dateFrom = $record->getDateFrom();
|
||||
$year = $dateFrom->format('Y');
|
||||
$month = (new Carbon($dateFrom))->locale('de_DE')->monthName;
|
||||
$month = (new CarbonImmutable($dateFrom))->locale('de_DE')->monthName;
|
||||
|
||||
if (false === isset($availabilities[$year])) {
|
||||
$availabilities[$year] = [];
|
||||
|
||||
Reference in New Issue
Block a user