029a09dc09
Auto Tag Develop / tag (push) Successful in 17s
## Objectif
Prévenir automatiquement les administrateurs, sur le **dernier jour ouvré précédant la fin d'un contrat**, qu'un salarié arrive au terme de son emploi.
## Fonctionnement
- Commande quotidienne `app:contract:end-notifications` (à brancher sur le crontab prod, ~6h ; option `--date=YYYY-MM-DD` pour test/rattrapage).
- Cible **la dernière période de contrat** d'un employé (un changement de contrat enchaîné, ex. CDD→CDI, ne notifie pas).
- Notifie sur le **dernier jour ouvré strictement avant** `endDate` (inclusif). Week-ends **et fériés** sautés → une fin de contrat le lundi est signalée dès le vendredi. Le Lundi de Pentecôte reste un jour ouvré (cohérent avec le reste de l'app).
- Une notification par admin : message « Fin de {nature} de {Nom} le {date} », catégorie `Contrat`, lien `/employees/{id}`, sans acteur.
- **Idempotent** : déduplication par `(recipient, category, target, message)`.
- Front : la cloche (déjà admin-only) affiche proprement les notifs sans acteur.
- **Aucune migration** (réutilise la table `notifications`).
## Architecture
Logique pure isolée et testée : `WorkingDayCalculator` (week-end + férié) + `ContractEndNotificationPlanner` (fenêtre + message). Persistance dans `ContractEndNotificationService`, exposée par `ContractEndNotificationCommand`. Méthodes repo `findLatestPeriodsForAllEmployees` + `existsForRecipientCategoryTargetMessage`.
## Tests & vérification
- 11 tests unitaires ajoutés ; suite complète verte (264 tests, 564 assertions).
- Vérif e2e manuelle : run du vendredi → 6 notifs/1 contrat finissant le lundi (saut de week-end OK), relance idempotente (0), contenu BDD correct.
## Documentation
`doc/contract-end-notifications.md`, `doc/functional-rules.md` (§15), doc in-app (`documentation-content.ts`), `CLAUDE.md`.
## ⚠️ Tâche infra
Ajouter la ligne crontab prod : `0 6 * * * … bin/console app:contract:end-notifications`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #35
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
48 lines
1.2 KiB
PHP
48 lines
1.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service\Notification;
|
|
|
|
use App\Service\PublicHolidayServiceInterface;
|
|
use DateTimeImmutable;
|
|
use Throwable;
|
|
|
|
final readonly class WorkingDayCalculator
|
|
{
|
|
public function __construct(
|
|
private PublicHolidayServiceInterface $holidays,
|
|
) {}
|
|
|
|
public function isWorkingDay(DateTimeImmutable $date): bool
|
|
{
|
|
$dayOfWeek = (int) $date->format('N'); // 1 (lundi) .. 7 (dimanche)
|
|
if ($dayOfWeek >= 6) {
|
|
return false;
|
|
}
|
|
|
|
return !$this->isPublicHoliday($date);
|
|
}
|
|
|
|
public function nextWorkingDay(DateTimeImmutable $date): DateTimeImmutable
|
|
{
|
|
$candidate = $date->modify('+1 day')->setTime(0, 0, 0);
|
|
while (!$this->isWorkingDay($candidate)) {
|
|
$candidate = $candidate->modify('+1 day');
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
|
|
private function isPublicHoliday(DateTimeImmutable $date): bool
|
|
{
|
|
try {
|
|
$holidays = $this->holidays->getHolidaysDayByYears('metropole', $date->format('Y'));
|
|
} catch (Throwable) {
|
|
return false;
|
|
}
|
|
|
|
return isset($holidays[$date->format('Y-m-d')]);
|
|
}
|
|
}
|