fix: correctly apply due dates for contract and invoice upload reminders
This commit is contained in:
@@ -5,8 +5,12 @@ namespace App\Repository;
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\Teamer;
|
||||
use App\Entity\Upload;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\Query\Expr\Orx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
@@ -481,9 +485,129 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
}, $results);
|
||||
}
|
||||
|
||||
public function findOverdueContracts(): array
|
||||
/**
|
||||
* Dispositions to remind about a missing contract, on the day the upload period runs out.
|
||||
*
|
||||
* Anchored on the creation of the disposition, which is what the upload period counts
|
||||
* from. The range is a half-open day rather than an equality: disposition.createdAt is a
|
||||
* timestamp, and the cron runs at 01:00, so "=" would only ever match a disposition that
|
||||
* happened to be created at that exact second.
|
||||
*
|
||||
* @param int $deadlineDays %contract_upload_deadline_days%
|
||||
*/
|
||||
public function findDispositionsForContractReminder(int $deadlineDays): array
|
||||
{
|
||||
$dueDate = (new \DateTimeImmutable())->modify('-7 days');
|
||||
return $this->getContractReminderQuery($deadlineDays)->getResult();
|
||||
}
|
||||
|
||||
public function getContractReminderQuery(int $deadlineDays): Query
|
||||
{
|
||||
$dayStart = (new \DateTimeImmutable('today'))->modify(sprintf('-%d days', $deadlineDays));
|
||||
|
||||
$qb = $this->createQueryBuilder('disposition');
|
||||
|
||||
return $qb
|
||||
->select('disposition', 'assignment', 'destination', 'teamer')
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
// restricted to the contract, so an unrelated upload does not suppress the reminder
|
||||
->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 the caller reports stays truthful
|
||||
$qb->expr()->isNull('teamer.deletedAt'),
|
||||
$qb->expr()->eq('disposition.status', ':dispositionStatus'),
|
||||
$qb->expr()->notIn('assignment.status', ':assignmentStatus'),
|
||||
$qb->expr()->gte('disposition.createdAt', ':dayStart'),
|
||||
$qb->expr()->lt('disposition.createdAt', ':dayEnd'),
|
||||
))
|
||||
->setParameter('documentType', Upload::TYPE_CONTRACT)
|
||||
->setParameter('dispositionStatus', Disposition::STATUS_NEW)
|
||||
->setParameter('assignmentStatus', [Assignment::STATUS_CALLED_OFF, Assignment::STATUS_DELETED])
|
||||
->setParameter('dayStart', $dayStart)
|
||||
->setParameter('dayEnd', $dayStart->modify('+1 day'))
|
||||
->getQuery()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispositions to remind about a missing invoice, for assignments ending on one given day.
|
||||
*
|
||||
* Both sends - the day after the assignment ends and again on the last day of the upload
|
||||
* period - come through here with a different $endDate, which is what keeps each of them
|
||||
* a one-shot without needing a record of what has already been sent.
|
||||
*
|
||||
* Anchored on the end date rather than Disposition::STATUS_ENDED on purpose: there is no
|
||||
* column recording when a disposition ended, and DispositionStatusService runs after the
|
||||
* reminders in CronCommand, so a status-based rule would always be one run late.
|
||||
*/
|
||||
public function findDispositionsForInvoiceReminder(\DateTimeImmutable $endDate): array
|
||||
{
|
||||
return $this->getInvoiceReminderQuery($endDate)->getResult();
|
||||
}
|
||||
|
||||
public function getInvoiceReminderQuery(\DateTimeImmutable $endDate): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder('disposition');
|
||||
|
||||
return $qb
|
||||
->select('disposition', 'assignment', 'destination', 'teamer')
|
||||
->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()->notIn('disposition.status', ':dispositionStatus'),
|
||||
$qb->expr()->notIn('assignment.status', ':assignmentStatus'),
|
||||
$this->effectiveDateToEquals($qb, ':endDate'),
|
||||
))
|
||||
->setParameter('documentType', Upload::TYPE_INVOICE)
|
||||
->setParameter('dispositionStatus', [Disposition::STATUS_CALLED_OFF, Disposition::STATUS_COMPLETED])
|
||||
->setParameter('assignmentStatus', [Assignment::STATUS_CALLED_OFF, Assignment::STATUS_DELETED])
|
||||
// bound as a date string, not as a DateTimeImmutable: both columns are DATE, and
|
||||
// Doctrine would otherwise bind 'Y-m-d H:i:s', which a DATE never equals
|
||||
->setParameter('endDate', $endDate->format('Y-m-d'))
|
||||
->getQuery()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the assignment's end date against $parameter, falling back to the destination's
|
||||
* when the assignment does not override it.
|
||||
*
|
||||
* The fallback is guarded on both sides - without the isNull() on the second branch an
|
||||
* assignment that moves the end date into the future would still match on the
|
||||
* destination's date. Kept in one place because the per-assignment date override is due to
|
||||
* be removed (docs/remove-assignment-date-override.md), and this then collapses to a plain
|
||||
* comparison on destination.dateTo.
|
||||
*/
|
||||
private function effectiveDateToEquals(QueryBuilder $qb, string $parameter): Orx
|
||||
{
|
||||
return $qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->eq('assignment.dateTo', $parameter)
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->eq('destination.dateTo', $parameter)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispositions whose contract upload period has run out, for the admin dashboard.
|
||||
*
|
||||
* @param int $deadlineDays %contract_upload_deadline_days%, so this list marks a
|
||||
* disposition overdue on the same day the reminder mail goes out
|
||||
*/
|
||||
public function findOverdueContracts(int $deadlineDays): array
|
||||
{
|
||||
$dueDate = (new \DateTimeImmutable())->modify(sprintf('-%d days', $deadlineDays));
|
||||
|
||||
$qb = $this->createQueryBuilder('disposition');
|
||||
|
||||
@@ -492,7 +616,9 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
->innerJoin('assignment.jobProfile', 'job_profile')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->leftJoin('disposition.documents', 'document')
|
||||
// the join is restricted to the contract, so that an unrelated upload - a
|
||||
// driver's licence, say - does not hide a disposition from this list
|
||||
->leftJoin('disposition.documents', 'document', Join::WITH, 'document.type = :documentType')
|
||||
->innerJoin('disposition.teamer', 'teamer')
|
||||
->innerJoin('teamer.user', 'user')
|
||||
->where($qb->expr()->andX(
|
||||
@@ -500,6 +626,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
$qb->expr()->eq('disposition.status', ':status'),
|
||||
$qb->expr()->isNull('document')
|
||||
))
|
||||
->setParameter('documentType', Upload::TYPE_CONTRACT)
|
||||
->setParameter('dueDate', $dueDate)
|
||||
->setParameter('status', Disposition::STATUS_NEW)
|
||||
->orderBy('disposition.createdAt', 'ASC')
|
||||
|
||||
Reference in New Issue
Block a user