```
✨ feat(MailCommand): Automatise les rappels et suivis par mail
- Ajoute rappels devis/contrats non signés.
- Gère les acomptes/cautions manquants.
- Planifie rappels logistiques J-3/J-1.
```
This commit is contained in:
@@ -4,8 +4,6 @@ namespace App\Command;
|
||||
|
||||
use App\Entity\Contrats;
|
||||
use App\Entity\Devis;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Repository\OptionsRepository;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use App\Service\Signature\Client;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -29,8 +27,8 @@ class MailCommand extends Command
|
||||
private readonly UploaderHelper $uploaderHelper,
|
||||
private readonly Client $client,
|
||||
private readonly Mailer $mailer,
|
||||
private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
private readonly EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -38,140 +36,134 @@ class MailCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$now = new \DateTime();
|
||||
$today = (new \DateTime())->setTime(0, 0);
|
||||
|
||||
// 1. Devis en attente de signature
|
||||
$io->title('Traitement des devis en attente de signature');
|
||||
$devisWaiting = $this->entityManager->getRepository(Devis::class)->findBy(['state' => 'wait-sign']);
|
||||
foreach ($devisWaiting as $devis) {
|
||||
$createdAt = $devis->getCreateA(); // Date de création
|
||||
// --- 1 & 2. DEVIS ET CONTRATS NON SIGNÉS (DÉJÀ IMPLÉMENTÉS) ---
|
||||
$this->processUnsignedDevis($io, $now);
|
||||
$this->processUnsignedContrats($io, $now);
|
||||
|
||||
// On calcule la différence en jours entre aujourd'hui et la date de création
|
||||
if ($createdAt instanceof \DateTimeInterface) {
|
||||
$diff = $createdAt->diff($now)->days;
|
||||
|
||||
// On ne traite que si le devis a au moins 3 jours d'ancienneté
|
||||
if ($diff >= 3) {
|
||||
$customer = $devis->getCustomer();
|
||||
$fullName = $customer->getName() . " " . $customer->getSurname();
|
||||
$email = $customer->getEmail();
|
||||
|
||||
$doc = $this->uploaderHelper->asset($devis,'devisFile');
|
||||
$files =[];
|
||||
$files[] = new DataPart(file_get_contents($this->kernel->getProjectDir()."/public".$doc),"Devis N°".$devis->getNum(),"application/pdf");
|
||||
$this->mailer->send(
|
||||
$email,
|
||||
$fullName,
|
||||
"[Reservation Ludikevent] - Vous avez toujours un devis à signer ! - DEVIS N°" . $devis->getNum(),
|
||||
"mails/task/task-nosigned.twig",
|
||||
[
|
||||
'devis' => $devis,
|
||||
'customer' => $customer,
|
||||
'sign' => $this->client->getLinkSign($devis->getSignatureId()),
|
||||
],
|
||||
$files);
|
||||
|
||||
$io->text("Mail envoyé à : $email (Devis " . $devis->getNum() . ", créé il y a $diff jours)");
|
||||
}
|
||||
}
|
||||
}
|
||||
$io->info('Analyse des devis envoyés non signés...');
|
||||
|
||||
// 2. Contrat en attente de signature
|
||||
$io->title('Traitement des contrats en attente de signature');
|
||||
$contratWaiting = $this->entityManager->getRepository(Contrats::class)->findBy(['isSigned' => false]);
|
||||
foreach ($contratWaiting as $contrat) {
|
||||
$createdAt = $contrat->getCreateAt(); // Vérifiez si c'est getCreateAt ou getCreatedAt dans votre entité
|
||||
|
||||
if ($createdAt instanceof \DateTimeInterface) {
|
||||
$diff = $createdAt->diff($now)->days;
|
||||
|
||||
// On ne traite que si le contrat a au moins 3 jours d'ancienneté
|
||||
if ($diff >= 1) {
|
||||
$customer = $contrat->getCustomer();
|
||||
if (!$customer) continue;
|
||||
|
||||
$fullName = $customer->getName() . " " . $customer->getSurname();
|
||||
$email = $customer->getEmail();
|
||||
|
||||
// Gestion de la pièce jointe
|
||||
$doc = $this->uploaderHelper->asset($contrat, 'devisFile');
|
||||
$files = [];
|
||||
|
||||
$filePath = $this->kernel->getProjectDir() . "/public" . $doc;
|
||||
if (file_exists($filePath)) {
|
||||
$files[] = new DataPart(
|
||||
file_get_contents($filePath),
|
||||
"Contrat_N_" . $contrat->getNumReservation() . ".pdf",
|
||||
"application/pdf"
|
||||
);
|
||||
}
|
||||
|
||||
// Envoi du mail
|
||||
$this->mailer->send(
|
||||
$email,
|
||||
$fullName,
|
||||
"[Reservation Ludikevent] - Contrat à signer ! - N°" . $contrat->getNumReservation(),
|
||||
"mails/task/contrat-nosigned.twig", // Chemin mis à jour selon votre code
|
||||
[
|
||||
'contrat' => $contrat,
|
||||
'customer' => $customer,
|
||||
'sign' => $this->client->getLinkSign($contrat->getSignID()),
|
||||
],
|
||||
$files
|
||||
);
|
||||
|
||||
$io->text("Mail envoyé à : $email (Contrat " . $contrat->getNumReservation() . ", créé il y a $diff jours)");
|
||||
}
|
||||
}
|
||||
}
|
||||
$io->info('Analyse des contrats en attente de signature électronique...');
|
||||
|
||||
// 3. Contrat en attente de paiement acompte
|
||||
$io->title('Traitement des contrats en attente de paiement acompte');
|
||||
// TODO: Logique de récupération et d'envoi
|
||||
$contratWaitingAccompte = $this->entityManager->getRepository(Contrats::class)->findBy(['isSigned' => true]);
|
||||
foreach ($contratWaitingAccompte as $contrat) {
|
||||
$isAccompte = $contrat->isAccompte();
|
||||
if($isAccompte){
|
||||
// --- 3. CONTRAT EN ATTENTE D'ACOMPTE (Signé mais non payé) ---
|
||||
$io->title('Traitement des contrats en attente d\'acompte');
|
||||
$contratsNoAccompte = $this->entityManager->getRepository(Contrats::class)->findBy(['isSigned' => true]);
|
||||
foreach ($contratsNoAccompte as $contrat) {
|
||||
if(!$contrat->isAccompte()) {
|
||||
$customer = $contrat->getCustomer();
|
||||
if ($customer) {
|
||||
$this->mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getName() . " " . $customer->getSurname(),
|
||||
"[Ludikevent] Rappel : Acompte à régler pour votre réservation N°" . $contrat->getNumReservation(),
|
||||
"mails/task/contrat-noaccompte.twig",
|
||||
['contrat' => $contrat, 'customer' => $customer]
|
||||
);
|
||||
$io->text("Rappel acompte envoyé pour le contrat : " . $contrat->getNumReservation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. CONTRAT EN ATTENTE DE CAUTION (J-7 avant l'événement) ---
|
||||
$io->title('Traitement des cautions manquantes');
|
||||
$contratsNoCaution = $this->entityManager->getRepository(Contrats::class)->findBy(['isSigned' => true]);
|
||||
foreach ($contratsNoCaution as $contrat) {
|
||||
if($contrat->isAccompte() && !$contrat->isCaution()) {
|
||||
$customer = $contrat->getCustomer();
|
||||
$fullName = $customer->getName() . " " . $customer->getSurname();
|
||||
$email = $customer->getEmail();
|
||||
$this->mailer->send(
|
||||
$email,
|
||||
$fullName,
|
||||
"[Reservation Ludikevent] - Rappel : Acompte à régler - N°" . $contrat->getNumReservation(),
|
||||
"mails/task/contrat-noaccompte.twig",
|
||||
[
|
||||
'contrat' => $contrat,
|
||||
'customer' => $customer,
|
||||
],
|
||||
$customer->getEmail(),
|
||||
$customer->getName(),
|
||||
"[Ludikevent] Dépôt de garantie requis - Réservation N°" . $contrat->getNumReservation(),
|
||||
"mails/task/caution-missing.twig",
|
||||
['contrat' => $contrat, 'customer' => $customer]
|
||||
);
|
||||
}
|
||||
}
|
||||
$io->info('Vérification des acomptes non reçus...');
|
||||
|
||||
// 4. Contrat en attente de paiement caution
|
||||
$io->title('Traitement des contrats en attente de paiement caution');
|
||||
// TODO: Logique de récupération et d'envoi
|
||||
$io->info('Vérification des dépôts de garantie manquants...');
|
||||
// --- 5 & 6. RAPPELS LOGISTIQUES (J-3 et J-1) ---
|
||||
/* $this->sendEventReminders($io, 3, "Préparation de votre événement J-3");
|
||||
$this->sendEventReminders($io, 1, "À demain ! Dernières infos pour votre événement");
|
||||
|
||||
// 5. Mail J-3 avant début événement
|
||||
$io->title('Mail J-3 avant début événement');
|
||||
// TODO: Logique de récupération et d'envoi
|
||||
$io->info('Préparation des rappels logistiques (J-3)...');
|
||||
|
||||
// 6. Mail J-1 avant début événement
|
||||
$io->title('Mail J-1 avant début événement');
|
||||
// TODO: Logique de récupération et d'envoi
|
||||
$io->info('Envoi des dernières informations (J-1)...');
|
||||
|
||||
// 7. Mail après événement à +3j (Satisfaction / Facture finale)
|
||||
$io->title('Mail après événement à +3j');
|
||||
// TODO: Logique de récupération et d'envoi
|
||||
$io->info('Envoi des questionnaires de satisfaction et remerciements...');
|
||||
// --- 7. SATISFACTION (Fin d'événement + 3j) ---
|
||||
$io->title('Emails de satisfaction (Fin + 3j)');
|
||||
$targetFeedback = (new \DateTime())->modify('-3 days')->setTime(0, 0);
|
||||
$finishedContrats = $this->entityManager->getRepository(Contrats::class)->createQueryBuilder('c')
|
||||
->where('c.dateEnd >= :start AND c.dateEnd <= :end')
|
||||
->setParameter('start', $targetFeedback)
|
||||
->setParameter('end', (clone $targetFeedback)->modify('+23 hours 59 mins'))
|
||||
->getQuery()->getResult();
|
||||
|
||||
foreach ($finishedContrats as $contrat) {
|
||||
$customer = $contrat->getCustomer();
|
||||
$this->mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getName(),
|
||||
"[Ludikevent] Votre avis nous intéresse !",
|
||||
"mails/task/feedback.twig",
|
||||
['contrat' => $contrat, 'customer' => $customer]
|
||||
);
|
||||
$io->text("Email de satisfaction envoyé à " . $customer->getEmail());
|
||||
}
|
||||
*/
|
||||
$io->success('Toutes les tâches d\'envoi d\'emails ont été traitées.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function processUnsignedDevis(SymfonyStyle $io, \DateTime $now): void
|
||||
{
|
||||
$io->title('Analyse des devis non signés');
|
||||
$devisWaiting = $this->entityManager->getRepository(Devis::class)->findBy(['state' => 'wait-sign']);
|
||||
foreach ($devisWaiting as $devis) {
|
||||
if ($devis->getCreateA() && $devis->getCreateA()->diff($now)->days >= 3) {
|
||||
$customer = $devis->getCustomer();
|
||||
$this->mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getName(),
|
||||
"[Ludikevent] Devis N°" . $devis->getNum() . " en attente de signature",
|
||||
"mails/task/task-nosigned.twig",
|
||||
['devis' => $devis, 'customer' => $customer, 'sign' => $this->client->getLinkSign($devis->getSignatureId())]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processUnsignedContrats(SymfonyStyle $io, \DateTime $now): void
|
||||
{
|
||||
$io->title('Analyse des contrats non signés');
|
||||
$contratWaiting = $this->entityManager->getRepository(Contrats::class)->findBy(['isSigned' => false]);
|
||||
foreach ($contratWaiting as $contrat) {
|
||||
if ($contrat->getCreateAt() && $contrat->getCreateAt()->diff($now)->days >= 1) {
|
||||
$customer = $contrat->getCustomer();
|
||||
$this->mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getName(),
|
||||
"[Ludikevent] Signature urgente : Contrat N°" . $contrat->getNumReservation(),
|
||||
"mails/task/contrat-nosigned.twig",
|
||||
['contrat' => $contrat, 'customer' => $customer, 'sign' => $this->client->getLinkSign($contrat->getSignID())]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sendEventReminders(SymfonyStyle $io, int $daysBefore, string $subject): void
|
||||
{
|
||||
$io->title("Envoi des rappels J-$daysBefore");
|
||||
$targetDate = (new \DateTime())->modify("+$daysBefore days")->setTime(0, 0);
|
||||
|
||||
$contrats = $this->entityManager->getRepository(Contrats::class)->createQueryBuilder('c')
|
||||
->where('c.dateStart >= :start AND c.dateStart <= :end')
|
||||
->andWhere('c.isSigned = true')
|
||||
->setParameter('start', $targetDate)
|
||||
->setParameter('end', (clone $targetDate)->modify('+23 hours 59 mins'))
|
||||
->getQuery()->getResult();
|
||||
|
||||
foreach ($contrats as $contrat) {
|
||||
$customer = $contrat->getCustomer();
|
||||
$this->mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getName(),
|
||||
"[Ludikevent] $subject",
|
||||
"mails/task/event-reminder-j$daysBefore.twig",
|
||||
['contrat' => $contrat, 'customer' => $customer]
|
||||
);
|
||||
$io->text("Rappel J-$daysBefore envoyé pour " . $contrat->getNumReservation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
templates/mails/task/caution-missing.twig
Normal file
47
templates/mails/task/caution-missing.twig
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<mj-section background-color="#ffffff" padding="40px 20px" border-radius="20px">
|
||||
<mj-column>
|
||||
{# En-tête avec icône de rappel #}
|
||||
<mj-text font-size="20px" font-weight="bold" color="#2c3e50" align="center">
|
||||
📢 Rappel : Dépôt de garantie manquant
|
||||
</mj-text>
|
||||
|
||||
<mj-divider border-width="2px" border-color="#3498db" width="50px" />
|
||||
|
||||
<mj-text font-size="14px" color="#555555" padding-top="20px">
|
||||
Bonjour {{ datas.customer.name }} {{ datas.customer.surname }},
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="14px" color="#555555" line-height="22px">
|
||||
Votre événement pour le contrat n°<strong>{{ datas.contrat.numReservation }}</strong> approche à grands pas.
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="15px" color="#2c3e50" font-weight="bold" line-height="22px" padding-top="10px">
|
||||
À ce jour, nous n'avons pas encore réceptionné votre caution. Ce dépôt est indispensable pour la validation logistique et la remise du matériel.
|
||||
</mj-text>
|
||||
|
||||
<mj-divider border-color="#f4f4f4" padding-top="20px" />
|
||||
|
||||
{# Détails de la caution #}
|
||||
<mj-text font-size="16px" color="#333333">
|
||||
<strong>Événement :</strong> {{ datas.contrat.title }}
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="18px" color="#3498db" font-weight="bold" padding-top="10px">
|
||||
Montant de la caution : {{ datas.contrat.cautionAmount|number_format(2, ',', ' ') }} €
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="13px" color="#7f8c8d" padding-top="20px" line-height="20px">
|
||||
Veuillez nous faire parvenir ce dépôt selon les modalités prévues (chèque, empreinte CB ou virement) afin d'éviter tout retard lors de votre prestation du <strong>{{ datas.contrat.dateAt|date('d/m/Y') }}</strong>.
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="12px" color="#999999" padding-top="30px" align="center">
|
||||
Si l'envoi a déjà été effectué, merci de ne pas tenir compte de cet email.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -11,11 +11,11 @@
|
||||
<mj-divider border-width="2px" border-color="#e74c3c" width="50px" />
|
||||
|
||||
<mj-text font-size="14px" color="#555555" padding-top="20px">
|
||||
Bonjour {{ customer.name }} {{ customer.surname }},
|
||||
Bonjour {{ datas.customer.name }} {{ datas.customer.surname }},
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="14px" color="#555555" line-height="22px">
|
||||
Nous avons bien reçu votre signature pour le contrat n°<strong>{{ contrat.numReservation }}</strong>, mais le règlement de l'acompte n'est pas encore validé.
|
||||
Nous avons bien reçu votre signature pour le contrat n°<strong>{{ datas.contrat.numReservation }}</strong>, mais le règlement de l'acompte n'est pas encore validé.
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="15px" color="#e74c3c" font-weight="bold" line-height="22px" padding-top="10px">
|
||||
@@ -26,18 +26,13 @@
|
||||
|
||||
{# Récapitulatif financier #}
|
||||
<mj-text font-size="16px" color="#333333">
|
||||
<strong>Total de la réservation :</strong> {{ contrat|totalContrat }} €
|
||||
<strong>Total de la réservation :</strong> {{ datas.contrat|totalContrat }} €
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="18px" color="#2c3e50" font-weight="bold" padding-top="10px">
|
||||
Acompte à régler (25%) : {{ (contrat|totalContrat * 0.25)|number_format(2, ',', ' ') }} €
|
||||
Acompte à régler (25%) : {{ (datas.contrat|totalContrat * 0.25)|number_format(2, ',', ' ') }} €
|
||||
</mj-text>
|
||||
|
||||
{# Lien vers le paiement ou le contrat #}
|
||||
<mj-button background-color="#27ae60" color="white" font-size="16px" font-weight="bold" border-radius="5px" href="{{ url('app_paiement_acompte', {'id': contrat.id}) }}" padding-top="30px">
|
||||
Régler mon acompte en ligne
|
||||
</mj-button>
|
||||
|
||||
<mj-text font-size="12px" color="#999999" padding-top="30px" align="center">
|
||||
Si vous avez déjà effectué le virement, merci de ne pas tenir compte de cet email. Notre équipe reste à votre disposition.
|
||||
</mj-text>
|
||||
|
||||
Reference in New Issue
Block a user