```
✨ feat(contrats): Ajoute gestion manuelle des paiements et états de caution
Ajoute la gestion manuelle des paiements (accompte, solde, caution) et permet la libération/encaissement de caution avec envoi de mail.
```
This commit is contained in:
@@ -11,7 +11,6 @@ use App\Entity\Product;
|
||||
use App\Event\Signature\ContratEvent;
|
||||
use App\Form\Type\ContratsType;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\AccountRepository;
|
||||
use App\Repository\DevisRepository;
|
||||
use App\Repository\ContratsRepository;
|
||||
use App\Service\Mailer\Mailer;
|
||||
@@ -19,7 +18,6 @@ use App\Service\Pdf\ContratPdfService;
|
||||
use App\Service\Pdf\PlPdf;
|
||||
use App\Service\Signature\Client;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -32,7 +30,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
class ContratsController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* Liste des contrats
|
||||
* Liste des contrats avec pagination et renvoi de mail
|
||||
*/
|
||||
#[Route(path: '/crm/contrats', name: 'app_crm_contrats', options: ['sitemap' => false], methods: ['GET'])]
|
||||
public function contrats(
|
||||
@@ -43,51 +41,42 @@ class ContratsController extends AbstractController
|
||||
Request $request
|
||||
): Response {
|
||||
|
||||
// --- ACTION D'ENVOI PAR EMAIL ---
|
||||
if ($request->query->has('idSend')) {
|
||||
$contrat = $contratsRepository->find($request->query->get('idSend'));
|
||||
|
||||
if (!$contrat) {
|
||||
$this->addFlash("danger", "Contrat introuvable.");
|
||||
return $this->redirectToRoute('app_crm_contrats');
|
||||
}
|
||||
|
||||
// Déclenchement de l'événement (ton Subscriber s'occupe de l'envoi du mail)
|
||||
$event = new ContratEvent($contrat);
|
||||
$eventDispatcher->dispatch($event);
|
||||
|
||||
$this->addFlash("success", "Le contrat a bien été envoyé à " . $contrat->getCustomer()->getEmail());
|
||||
$appLogger->record('RESEND', "Renvoi du contrat N°" . $contrat->getNumReservation() . " effectué");
|
||||
$appLogger->record('RESEND', "Renvoi du contrat N°" . $contrat->getNumReservation());
|
||||
|
||||
return $this->redirectToRoute('app_crm_contrats');
|
||||
}
|
||||
|
||||
// --- LOG DE CONSULTATION ---
|
||||
$appLogger->record('VIEW', 'Consultation de la liste des contrats');
|
||||
|
||||
// --- AFFICHAGE DE LA LISTE ---
|
||||
$query = $contratsRepository->findBy([], ['createAt' => 'DESC']);
|
||||
$pagination = $paginator->paginate(
|
||||
$query,
|
||||
$request->query->getInt('page', 1),
|
||||
10
|
||||
);
|
||||
$pagination = $paginator->paginate($query, $request->query->getInt('page', 1), 10);
|
||||
|
||||
return $this->render('dashboard/contrats/list.twig', [
|
||||
'contrats' => $pagination,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue détaillée et gestion des paiements/actions
|
||||
*/
|
||||
#[Route(path: '/crm/contrats/view/{id}', name: 'app_crm_contrats_view', options: ['sitemap' => false], methods: ['GET', 'POST'])]
|
||||
public function contratsView(
|
||||
?Contrats $contrat,
|
||||
EntityManagerInterface $entityManager,
|
||||
Request $request,
|
||||
Client $client,
|
||||
\App\Service\Stripe\Client $stripeClient,
|
||||
DevisRepository $devisRepository,
|
||||
AppLogger $appLogger,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
KernelInterface $kernel,
|
||||
Mailer $mailer,
|
||||
): Response {
|
||||
@@ -95,222 +84,121 @@ class ContratsController extends AbstractController
|
||||
throw $this->createNotFoundException('Contrat non trouvé.');
|
||||
}
|
||||
|
||||
// --- CALCULS DES MONTANTS ---
|
||||
$totalHt = 0;
|
||||
$totalCaution = 0;
|
||||
$days = $contrat->getDateAt()->diff($contrat->getEndAt())->days + 1;
|
||||
|
||||
// 1. Calcul de la durée en jours
|
||||
$dateStart = $contrat->getDateAt();
|
||||
$dateEnd = $contrat->getEndAt();
|
||||
|
||||
// On ajoute +1 pour inclure le jour de début et de fin
|
||||
$interval = $dateStart->diff($dateEnd);
|
||||
$days = $interval->days + 1;
|
||||
|
||||
// 2. Calcul des lignes avec tarif dégressif
|
||||
foreach ($contrat->getContratsLines() as $line) {
|
||||
// Premier jour
|
||||
$priceLine = $line->getPrice1DayHt();
|
||||
|
||||
// Jours supplémentaires
|
||||
if ($days > 1) {
|
||||
$priceLine += ($line->getPriceSupDayHt() * ($days - 1));
|
||||
}
|
||||
|
||||
$priceLine = $line->getPrice1DayHt() + ($line->getPriceSupDayHt() * ($days - 1));
|
||||
$totalHt += $priceLine;
|
||||
$totalCaution += $line->getCaution();
|
||||
}
|
||||
|
||||
// 3. Ajout des options (forfaitaires)
|
||||
foreach ($contrat->getContratsOptions() as $option) {
|
||||
$totalHt += $option->getPrice();
|
||||
}
|
||||
|
||||
// 4. Calcul du solde (Total - paiements déjà effectués)
|
||||
$dejaPaye = 0;
|
||||
foreach ($contrat->getContratsPayments() as $payment) {
|
||||
if ($payment->getState() === 'complete' && $payment->getType() !== 'caution') {
|
||||
$dejaPaye += $payment->getAmount();
|
||||
foreach ($contrat->getContratsPayments() as $p) {
|
||||
if ($p->getState() === 'complete' && $p->getType() !== 'caution') {
|
||||
$dejaPaye += $p->getAmount();
|
||||
}
|
||||
}
|
||||
|
||||
$solde = $totalHt - $dejaPaye;
|
||||
$customer = $contrat->getCustomer();
|
||||
$customerName = $customer->getSurname() . ' ' . $customer->getName();
|
||||
|
||||
|
||||
if($request->query->has('type') && $request->query->get('type') === 'accompte') {
|
||||
$paiementAccompte = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'contrat' => $contrat,
|
||||
'type' => 'accompte',
|
||||
]);
|
||||
if(!$paiementAccompte) {
|
||||
$paiementAccompte = new ContratsPayments();
|
||||
$paiementAccompte->setContrat($contrat);
|
||||
$paiementAccompte->setType('accompte');
|
||||
}
|
||||
$paiementAccompte->setValidateAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setPaymentAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setAmount( $totalHt * 0.25);
|
||||
$paiementAccompte->setState("complete");
|
||||
$paiementAccompte->setPaymentId("");
|
||||
$paiementAccompte->setCard([
|
||||
'type' => 'manuel'
|
||||
]);
|
||||
$pdf = new PlPdf($kernel, $paiementAccompte, $contrat);
|
||||
$pdf->generate();
|
||||
$content = $pdf->Output('S');
|
||||
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
|
||||
file_put_contents($tmpSigned, $content);
|
||||
|
||||
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
|
||||
$paiementAccompte->setPaymentFile(new UploadedFile($tmpSigned, "confirmed-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
|
||||
$entityManager->persist($paiementAccompte);
|
||||
$entityManager->flush();
|
||||
$data = $client->autoSignConfirmedPayment($paiementAccompte);
|
||||
// 1. Gestion du PDF SIGNÉ
|
||||
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
|
||||
$signedContent = file_get_contents($data);
|
||||
file_put_contents($tmpSigned, $signedContent);
|
||||
|
||||
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
|
||||
$paiementAccompte->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
|
||||
$entityManager->persist($paiementAccompte);
|
||||
$entityManager->flush();
|
||||
$customer = $contrat->getCustomer();
|
||||
$subjectCustomer = "[Ludikevent] Confirmation de votre acompte - #" . $contrat->getNumReservation();
|
||||
$mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getSurname() . ' ' . $customer->getName(),
|
||||
$subjectCustomer,
|
||||
"mails/customer/accompte_confirmation.twig",
|
||||
[
|
||||
'contrat' => $contrat,
|
||||
'payment' => $paiementAccompte,
|
||||
'customer' => $customer,
|
||||
'reservationLink' => "https://reservation.ludikevent.fr" . $this->generateUrl('gestion_contrat_view', ['num' => $contrat->getNumReservation()])
|
||||
]
|
||||
);
|
||||
$appLogger->record('PAYMENT','Validation accompte manuel pour contrat #' . $contrat->getNumReservation());
|
||||
$this->addFlash("success","Validation accompte effectuée");
|
||||
return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]);
|
||||
}
|
||||
if($request->query->has('type') && $request->query->get('type') === 'caution') {
|
||||
$paiementAccompte = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
// --- TRAITEMENT : ACTIONS SUR CAUTION (Libérer / Encaisser) ---
|
||||
if ($request->query->has('action')) {
|
||||
$action = $request->query->get('action');
|
||||
$paymentCaution = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'contrat' => $contrat,
|
||||
'type' => 'caution',
|
||||
]);
|
||||
if(!$paiementAccompte) {
|
||||
$paiementAccompte = new ContratsPayments();
|
||||
$paiementAccompte->setContrat($contrat);
|
||||
$paiementAccompte->setType('caution');
|
||||
|
||||
if ($paymentCaution) {
|
||||
if ($action === 'liberer') {
|
||||
$paymentCaution->setState("release");
|
||||
$subject = "[Ludikevent] Votre caution a été libérée - #" . $contrat->getNumReservation();
|
||||
$template = "mails/customer/caution_release.twig";
|
||||
$logAction = "Libération caution";
|
||||
} else {
|
||||
$paymentCaution->setState("recup");
|
||||
$subject = "[Ludikevent] Votre caution a été encaissée - #" . $contrat->getNumReservation();
|
||||
$template = "mails/customer/caution_encaissement.twig";
|
||||
$logAction = "Encaissement caution";
|
||||
}
|
||||
|
||||
$entityManager->flush();
|
||||
$mailer->send($customer->getEmail(), $customerName, $subject, $template, [
|
||||
'datas' => [
|
||||
'contrat' => $contrat,
|
||||
'payment' => $paymentCaution,
|
||||
'customer' => $customer,
|
||||
'reservationLink' => "https://reservation.ludikevent.fr" . $this->generateUrl('gestion_contrat_view', ['num' => $contrat->getNumReservation()])
|
||||
]
|
||||
]);
|
||||
|
||||
$appLogger->record('PAYMENT', "$logAction pour #" . $contrat->getNumReservation());
|
||||
$this->addFlash("success", "Action effectuée avec succès.");
|
||||
return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]);
|
||||
}
|
||||
$paiementAccompte->setValidateAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setPaymentAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setAmount( $totalCaution);
|
||||
$paiementAccompte->setState("complete");
|
||||
$paiementAccompte->setPaymentId("");
|
||||
$paiementAccompte->setCard([
|
||||
'type' => 'manuel'
|
||||
]);
|
||||
$pdf = new PlPdf($kernel, $paiementAccompte, $contrat);
|
||||
$pdf->generate();
|
||||
$content = $pdf->Output('S');
|
||||
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
|
||||
file_put_contents($tmpSigned, $content);
|
||||
|
||||
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
|
||||
$paiementAccompte->setPaymentFile(new UploadedFile($tmpSigned, "confirmed-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
|
||||
$entityManager->persist($paiementAccompte);
|
||||
$entityManager->flush();
|
||||
$data = $client->autoSignConfirmedPayment($paiementAccompte);
|
||||
// 1. Gestion du PDF SIGNÉ
|
||||
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
|
||||
$signedContent = file_get_contents($data);
|
||||
file_put_contents($tmpSigned, $signedContent);
|
||||
|
||||
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
|
||||
$paiementAccompte->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
|
||||
$entityManager->persist($paiementAccompte);
|
||||
$entityManager->flush();
|
||||
$customer = $contrat->getCustomer();
|
||||
$subjectCustomer = "[Ludikevent] Confirmation de votre caution - #" . $contrat->getNumReservation();
|
||||
$mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getSurname() . ' ' . $customer->getName(),
|
||||
$subjectCustomer,
|
||||
"mails/customer/accompte_confirmation.twig",
|
||||
[
|
||||
'contrat' => $contrat,
|
||||
'payment' => $paiementAccompte,
|
||||
'customer' => $customer,
|
||||
'reservationLink' => "https://reservation.ludikevent.fr" . $this->generateUrl('gestion_contrat_view', ['num' => $contrat->getNumReservation()])
|
||||
]
|
||||
);
|
||||
$appLogger->record('PAYMENT','Validation caution manuel pour contrat #' . $contrat->getNumReservation());
|
||||
$this->addFlash("success","Validation caution effectuée");
|
||||
return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]);
|
||||
}
|
||||
if($request->query->has('type') && $request->query->get('type') === 'solde') {
|
||||
$paiementAccompte = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
|
||||
// --- TRAITEMENT : ENREGISTREMENT PAIEMENT MANUEL ---
|
||||
if ($request->query->has('type')) {
|
||||
$type = $request->query->get('type');
|
||||
$amount = match($type) {
|
||||
'accompte' => $totalHt * 0.25,
|
||||
'caution' => $totalCaution,
|
||||
'solde' => $solde,
|
||||
default => 0
|
||||
};
|
||||
|
||||
$payment = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'contrat' => $contrat,
|
||||
'type' => 'solde',
|
||||
]);
|
||||
if(!$paiementAccompte) {
|
||||
$paiementAccompte = new ContratsPayments();
|
||||
$paiementAccompte->setContrat($contrat);
|
||||
$paiementAccompte->setType('solde');
|
||||
}
|
||||
$paiementAccompte->setValidateAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setPaymentAt(new \DateTimeImmutable());
|
||||
$paiementAccompte->setAmount( $totalHt);
|
||||
$paiementAccompte->setState("complete");
|
||||
$paiementAccompte->setPaymentId("");
|
||||
$paiementAccompte->setCard([
|
||||
'type' => 'manuel'
|
||||
]);
|
||||
$pdf = new PlPdf($kernel, $paiementAccompte, $contrat);
|
||||
'type' => $type,
|
||||
]) ?? new ContratsPayments();
|
||||
|
||||
$payment->setContrat($contrat);
|
||||
$payment->setType($type);
|
||||
$payment->setAmount($amount);
|
||||
$payment->setState("complete");
|
||||
$payment->setPaymentAt(new \DateTimeImmutable());
|
||||
$payment->setValidateAt(new \DateTimeImmutable());
|
||||
$payment->setUpdateAt(new \DateTimeImmutable());
|
||||
$payment->setCard(['type' => 'manuel']);
|
||||
$payment->setPaymentId("MANUAL-" . uniqid());
|
||||
|
||||
// Génération PDF et signature
|
||||
$pdf = new PlPdf($kernel, $payment, $contrat);
|
||||
$pdf->generate();
|
||||
$content = $pdf->Output('S');
|
||||
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
|
||||
file_put_contents($tmpSigned, $content);
|
||||
$tmpPath = sys_get_temp_dir() . '/pay_' . uniqid() . '.pdf';
|
||||
file_put_contents($tmpPath, $pdf->Output('S'));
|
||||
|
||||
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
|
||||
$paiementAccompte->setPaymentFile(new UploadedFile($tmpSigned, "confirmed-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
|
||||
$entityManager->persist($paiementAccompte);
|
||||
$payment->setPaymentFile(new UploadedFile($tmpPath, "recu-" . $type . ".pdf", "application/pdf", null, true));
|
||||
$entityManager->persist($payment);
|
||||
$entityManager->flush();
|
||||
$data = $client->autoSignConfirmedPayment($paiementAccompte);
|
||||
// 1. Gestion du PDF SIGNÉ
|
||||
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
|
||||
$signedContent = file_get_contents($data);
|
||||
file_put_contents($tmpSigned, $signedContent);
|
||||
|
||||
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
|
||||
$paiementAccompte->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
|
||||
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
|
||||
$entityManager->persist($paiementAccompte);
|
||||
$signedUrl = $client->autoSignConfirmedPayment($payment);
|
||||
$tmpSigned = sys_get_temp_dir() . '/signed_' . uniqid() . '.pdf';
|
||||
file_put_contents($tmpSigned, file_get_contents($signedUrl));
|
||||
$payment->setPaymentSignedFile(new UploadedFile($tmpSigned, "sign-" . $type . ".pdf", "application/pdf", null, true));
|
||||
|
||||
$entityManager->flush();
|
||||
$customer = $contrat->getCustomer();
|
||||
$subjectCustomer = "[Ludikevent] Votre réservation est désormais soldée - #" . $contrat->getNumReservation();
|
||||
$mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getSurname() . ' ' . $customer->getName(),
|
||||
$subjectCustomer,
|
||||
"mails/customer/accompte_confirmation.twig",
|
||||
[
|
||||
|
||||
$mailer->send($customer->getEmail(), $customerName, "[Ludikevent] Confirmation de paiement - " . $type, "mails/customer/accompte_confirmation.twig", [
|
||||
'datas' => [
|
||||
'contrat' => $contrat,
|
||||
'payment' => $paiementAccompte,
|
||||
'payment' => $payment,
|
||||
'customer' => $customer,
|
||||
'reservationLink' => "https://reservation.ludikevent.fr" . $this->generateUrl('gestion_contrat_view', ['num' => $contrat->getNumReservation()])
|
||||
]
|
||||
);
|
||||
$appLogger->record('PAYMENT','Validation solde manuel pour contrat #' . $contrat->getNumReservation());
|
||||
$this->addFlash("success","Validation solde effectuée");
|
||||
]);
|
||||
|
||||
$appLogger->record('PAYMENT', "Validation manuelle $type pour #" . $contrat->getNumReservation());
|
||||
$this->addFlash("success", "Paiement $type enregistré.");
|
||||
return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]);
|
||||
}
|
||||
|
||||
@@ -319,14 +207,13 @@ class ContratsController extends AbstractController
|
||||
'days' => $days,
|
||||
'solde' => $solde,
|
||||
'totalHT' => $totalHt,
|
||||
'signedNumber' => $contrat->getSignID(),
|
||||
'arrhes' => $totalHt * 0.25,
|
||||
'totalCaution' => $totalCaution,
|
||||
'arrhes' => $totalHt * 0.25,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Création d'un contrat à partir d'un devis
|
||||
* Création d'un contrat
|
||||
*/
|
||||
#[Route(path: '/crm/contrats/add', name: 'app_crm_contrats_create', options: ['sitemap' => false], methods: ['GET', 'POST'])]
|
||||
public function contratsAdd(
|
||||
@@ -335,143 +222,70 @@ class ContratsController extends AbstractController
|
||||
Client $client,
|
||||
DevisRepository $devisRepository,
|
||||
AppLogger $appLogger,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
KernelInterface $kernel,
|
||||
): Response {
|
||||
$devis = $devisRepository->find($request->get('idDevis', 0));
|
||||
|
||||
$c = new Contrats();
|
||||
$lines = [['id' => 0, 'name' => '', 'priceHt1Day' => 0, 'priceHtSupDay' => 0, 'caution' => 0]];
|
||||
$options = [['id' => 0, 'name' => '', 'priceHt' => 0,'details'=>'']];
|
||||
|
||||
// Logique de pré-remplissage via Devis...
|
||||
if ($devis instanceof Devis) {
|
||||
$c->setDateAt($devis->getStartAt());
|
||||
$c->setEndAt($devis->getEndAt());
|
||||
$c->setCustomer($devis->getCustomer());
|
||||
$c->setDevis($devis);
|
||||
|
||||
|
||||
// Mapping adresse de l'événement
|
||||
if ($devis->getAddressShip()) {
|
||||
$c->setAddressEvent($devis->getAddressShip()->getAddress());
|
||||
$c->setAddress2Event($devis->getAddressShip()->getAddress2());
|
||||
$c->setAddress3Event($devis->getAddressShip()->getAddress3());
|
||||
$c->setZipCodeEvent($devis->getAddressShip()->getZipcode());
|
||||
$c->setTownEvent($devis->getAddressShip()->getCity());
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
$options = [];
|
||||
|
||||
foreach ($devis->getDevisLines() as $line) {
|
||||
$p = $entityManager->getRepository(Product::class)->findOneBy(['name'=>$line->getProduct()]);
|
||||
|
||||
$lines[] = [
|
||||
'id' => $line->getId(),
|
||||
'name' =>$p->getName() . " - " . $p->getRef(),
|
||||
'priceHt1Day' => $line->getPriceHt(),
|
||||
'priceHtSupDay' => $line->getPriceHtSup(),
|
||||
'caution' => $p->getCaution(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($devis->getDevisOptions() as $line) {
|
||||
$options[] = [
|
||||
'id' => $line->getId(),
|
||||
'name' => $line->getOption(),
|
||||
'details' => $line->getDetails(),
|
||||
'priceHt' => $line->getPriceHt(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$form = $this->createForm(ContratsType::class, $c);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
|
||||
// Récupération sécurisée des données de lignes et d'options
|
||||
$postData = $request->request->all();
|
||||
|
||||
if (isset($postData['lines'])) {
|
||||
foreach ($postData['lines'] as $line) {
|
||||
$vc = new ContratsLine();
|
||||
$vc->setContrat($c);
|
||||
$vc->setType("");
|
||||
$vc->setName($line['name']);
|
||||
$vc->setPrice1DayHt($line['priceHt1Day']);
|
||||
$vc->setPriceSupDayHt($line['priceHtSupDay']);
|
||||
$vc->setCaution($line['caution']);
|
||||
$entityManager->persist($vc);
|
||||
}
|
||||
foreach ($postData['lines'] ?? [] as $line) {
|
||||
$vc = (new ContratsLine())->setContrat($c)->setName($line['name'])->setPrice1DayHt($line['priceHt1Day'])->setPriceSupDayHt($line['priceHtSupDay'])->setCaution($line['caution']);
|
||||
$entityManager->persist($vc);
|
||||
}
|
||||
|
||||
if (isset($postData['options'])) {
|
||||
foreach ($postData['options'] as $line) {
|
||||
$vc = new ContratsOption();
|
||||
$vc->setContrat($c);
|
||||
$vc->setName($line['name']);
|
||||
$vc->setDetails($line['details']);
|
||||
$vc->setPrice($line['priceHt']);
|
||||
$entityManager->persist($vc);
|
||||
}
|
||||
foreach ($postData['options'] ?? [] as $opt) {
|
||||
$vo = (new ContratsOption())->setContrat($c)->setName($opt['name'])->setDetails($opt['details'])->setPrice($opt['priceHt']);
|
||||
$entityManager->persist($vo);
|
||||
}
|
||||
|
||||
// Génération des données de réservation
|
||||
$reservationNumber = $this->generateReservationNumber();
|
||||
$c->setNumReservation($reservationNumber);
|
||||
$c->setIsSigned(false);
|
||||
$c->setNumReservation($this->generateReservationNumber());
|
||||
$c->setCreateAt(new \DateTimeImmutable());
|
||||
|
||||
$contrateService = new ContratPdfService($kernel,$c,true);
|
||||
$contentDocuseal = $contrateService->generate();
|
||||
$tmpPathDocuseal = sys_get_temp_dir() . '/docuseal_' . uniqid() . '.pdf';
|
||||
file_put_contents($tmpPathDocuseal, $contentDocuseal);
|
||||
$fileDocuseal = new UploadedFile($tmpPathDocuseal, 'dc_' . $devis->getNum() . '.pdf', 'application/pdf', 0, true);
|
||||
$c->setDevisDocuSealFile($fileDocuseal);
|
||||
// PDFs
|
||||
foreach ([true, false] as $isDocuseal) {
|
||||
$service = new ContratPdfService($kernel, $c, $isDocuseal);
|
||||
$tmp = sys_get_temp_dir() . '/' . uniqid() . '.pdf';
|
||||
file_put_contents($tmp, $service->generate());
|
||||
$file = new UploadedFile($tmp, 'doc.pdf', 'application/pdf', null, true);
|
||||
$isDocuseal ? $c->setDevisDocuSealFile($file) : $c->setDevisFile($file);
|
||||
}
|
||||
|
||||
|
||||
$contrateService = new ContratPdfService($kernel,$c,false);
|
||||
$contentDevis = $contrateService->generate();
|
||||
$tmpPathDevis = sys_get_temp_dir() . '/devis_' . uniqid() . '.pdf';
|
||||
file_put_contents($tmpPathDevis, $contentDevis);
|
||||
|
||||
$fileDevis = new UploadedFile($tmpPathDevis, 'devis_' . $devis->getNum() . '.pdf', 'application/pdf', 0, true);
|
||||
$c->setDevisFile($fileDevis);
|
||||
$entityManager->persist($c);
|
||||
$entityManager->flush();
|
||||
$client->createSubmissionContrat($c);
|
||||
|
||||
// Flash & Logs
|
||||
$this->addFlash('success', "Le contrat $reservationNumber a été généré avec succès.");
|
||||
$appLogger->record('CREATE', "Génération contrat : $reservationNumber pour le client " . $c->getCustomer()->getName());
|
||||
|
||||
$appLogger->record('CREATE', "Contrat généré : " . $c->getNumReservation());
|
||||
return $this->redirectToRoute('app_crm_contrats');
|
||||
}
|
||||
|
||||
$appLogger->record('VIEW', 'Consultation page création contrat');
|
||||
|
||||
return $this->render('dashboard/contrats/add.twig', [
|
||||
'devis' => $devis,
|
||||
'form' => $form->createView(),
|
||||
'lines' => $lines,
|
||||
'options' => $options,
|
||||
'lines' => $lines ?? [],
|
||||
'options' => $options ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un numéro de réservation sécurisé
|
||||
*/
|
||||
private function generateReservationNumber(): string
|
||||
{
|
||||
$prefix = 'RESERV-' . date('Ymd');
|
||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$randomString = '';
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$randomString .= $alphabet[random_int(0, strlen($alphabet) - 1)];
|
||||
}
|
||||
|
||||
return $prefix . '-' . $randomString;
|
||||
return 'RESERV-' . date('Ymd') . '-' . substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,18 @@ class StripeExtension extends AbstractExtension
|
||||
|
||||
public function contratPaymentPay(Contrats $contrat,string $type): bool
|
||||
{
|
||||
if($type == "caution_free"){
|
||||
$pl = $this->em->getRepository(ContratsPayments::class)->findOneBy(['type'=>'caution','contrat'=>$contrat]);
|
||||
if($pl instanceof ContratsPayments) {
|
||||
return $pl->getState() == "release";
|
||||
}
|
||||
}
|
||||
if($type == "caution_recup"){
|
||||
$pl = $this->em->getRepository(ContratsPayments::class)->findOneBy(['type'=>'caution','contrat'=>$contrat]);
|
||||
if($pl instanceof ContratsPayments) {
|
||||
return $pl->getState() == "recup";
|
||||
}
|
||||
}
|
||||
$pl = $this->em->getRepository(ContratsPayments::class)->findOneBy(['type'=>$type,'contrat'=>$contrat]);
|
||||
if($pl instanceof ContratsPayments) {
|
||||
return $pl->getState() == "complete";
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
{# Définition des états de paiement #}
|
||||
{% set acompteOk = contratPaymentPay(contrat, 'accompte') %}
|
||||
{% set cautionOk = contratPaymentPay(contrat, 'caution') %}
|
||||
|
||||
{% set soldeOk = (solde <= 0.05) %}
|
||||
|
||||
<div class="space-y-8 pb-20">
|
||||
@@ -149,22 +150,46 @@
|
||||
<div>
|
||||
<span class="block text-[9px] font-black uppercase tracking-widest {{ cautionOk ? 'text-emerald-500' : 'text-rose-500' }}">Caution</span>
|
||||
<div class="flex flex-col gap-2 mt-3">
|
||||
{% if not cautionOk %}
|
||||
{% set cautionRelase = contratPaymentPay(contrat, 'caution_free') %}
|
||||
{% set cautionEncaisser = contratPaymentPay(contrat, 'caution_recup') %}
|
||||
|
||||
{# 1. ON TESTE D'ABORD LES ÉTATS FINAUX #}
|
||||
{% if cautionRelase %}
|
||||
<div class="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
|
||||
<svg class="w-3 h-3 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-[9px] font-black text-emerald-500 uppercase italic">Caution Libérée</span>
|
||||
</div>
|
||||
|
||||
{% elseif cautionEncaisser %}
|
||||
<div class="flex items-center gap-1.5 px-3 py-1.5 bg-amber-500/10 border border-amber-500/20 rounded-lg">
|
||||
<svg class="w-3 h-3 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<span class="text-[9px] font-black text-amber-500 uppercase italic">Caution Encaissée</span>
|
||||
</div>
|
||||
|
||||
{# 2. SI PAS DE STATUT FINAL, ON REGARDE SI ELLE EST AU MOINS REÇUE #}
|
||||
{% elseif cautionOk %}
|
||||
<div class="flex gap-2">
|
||||
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, action: 'encaisser'}) }}"
|
||||
onclick="return confirm('CONFIRMATION : Voulez-vous vraiment ENCAISSER la caution ?')"
|
||||
class="px-3 py-1.5 bg-amber-500/20 hover:bg-amber-500/40 border border-amber-500/30 rounded-lg text-[9px] font-black text-amber-500 uppercase transition-all shadow-lg shadow-amber-900/10">
|
||||
Encaisser
|
||||
</a>
|
||||
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, action: 'liberer'}) }}"
|
||||
class="px-3 py-1.5 bg-emerald-500/20 hover:bg-emerald-500/40 border border-emerald-500/30 rounded-lg text-[9px] font-black text-emerald-400 uppercase transition-all shadow-lg shadow-emerald-900/10">
|
||||
Libérer
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# 3. SINON, C'EST QU'ELLE N'EST PAS ENCORE ENREGISTRÉE #}
|
||||
{% else %}
|
||||
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'caution'}) }}"
|
||||
class="px-4 py-1.5 bg-rose-500/20 hover:bg-rose-500/30 border border-rose-500/30 rounded-lg text-[10px] font-black text-rose-400 uppercase transition-all">
|
||||
Marquer reçue
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="flex gap-2">
|
||||
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, action: 'encaisser'}) }}"
|
||||
class="px-3 py-1.5 bg-amber-500/20 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg text-[9px] font-black text-amber-500 uppercase transition-all">
|
||||
Encaisser
|
||||
</a>
|
||||
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, action: 'liberer'}) }}"
|
||||
class="px-3 py-1.5 bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/30 rounded-lg text-[9px] font-black text-emerald-400 uppercase transition-all">
|
||||
Libérer
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
58
templates/mails/customer/caution_encaissement.twig
Normal file
58
templates/mails/customer/caution_encaissement.twig
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<mj-section padding="20px">
|
||||
<mj-column background-color="#ffffff" border-radius="24px" padding="30px" css-class="shadow">
|
||||
|
||||
{# Icône d'alerte / prélèvement #}
|
||||
<mj-image src="https://cdn-icons-png.flaticon.com/512/595/595067.png" width="60px" padding-bottom="20px" />
|
||||
|
||||
<mj-text align="center" font-size="28px" font-weight="900" color="#0f172a" text-transform="uppercase" font-style="italic" padding-bottom="0px">
|
||||
Caution <span color="#e11d48">Encaissée</span>
|
||||
</mj-text>
|
||||
|
||||
<mj-text align="center" font-size="10px" font-weight="800" color="#64748b" letter-spacing="2px" text-transform="uppercase" padding-top="10px">
|
||||
Réservation #{{ datas.contrat.numReservation }}
|
||||
</mj-text>
|
||||
|
||||
<mj-divider border-width="1px" border-color="#f1f5f9" padding="30px 0" />
|
||||
|
||||
<mj-text font-size="15px" color="#334155" line-height="1.5">
|
||||
Bonjour <strong>{{ datas.customer.surname }}</strong>,
|
||||
<br /><br />
|
||||
Nous vous informons que suite au retour de votre location et après expertise de notre équipe, nous avons procédé à l'**encaissement de votre caution**.
|
||||
<br /><br />
|
||||
Cette décision fait suite au constat suivant :
|
||||
<p style="background-color: #fff1f2; border-left: 4px solid #e11d48; padding: 15px; color: #9f1239; font-style: italic;">
|
||||
{{ datas.reason|default('Non-respect des conditions générales de location ou dégradation du matériel constaté lors du retour.') }}
|
||||
</p>
|
||||
</mj-text>
|
||||
|
||||
<mj-wrapper background-color="#f8fafc" border-radius="16px" padding="20px">
|
||||
<mj-section padding="0">
|
||||
<mj-column width="50%">
|
||||
<mj-text font-size="10px" font-weight="800" color="#94a3b8" text-transform="uppercase">Montant prélevé</mj-text>
|
||||
<mj-text font-size="22px" font-weight="900" font-style="italic" color="#e11d48" padding-top="5px">
|
||||
{{ datas.payment.amount|number_format(2, ',', ' ') }}€
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="50%">
|
||||
<mj-text font-size="10px" font-weight="800" color="#94a3b8" text-transform="uppercase" align="right">Date d'effet</mj-text>
|
||||
<mj-text font-size="14px" font-weight="700" color="#475569" align="right" padding-top="10px">
|
||||
{{ "now"|date('d/m/Y') }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-text font-size="13px" color="#64748b" padding-top="20px">
|
||||
Vous recevrez prochainement par email une facture détaillée justifiant les frais de remise en état ou les pénalités appliquées.
|
||||
</mj-text>
|
||||
|
||||
<mj-button background-color="#e11d48" color="#ffffff" border-radius="12px" font-weight="800" text-transform="uppercase" font-size="12px" padding-top="30px" href="{{ datas.contactLink }}">
|
||||
Contacter le support
|
||||
</mj-button>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
55
templates/mails/customer/caution_release.twig
Normal file
55
templates/mails/customer/caution_release.twig
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<mj-section padding="20px">
|
||||
<mj-column background-color="#ffffff" border-radius="24px" padding="30px" css-class="shadow">
|
||||
|
||||
{# Icône de succès / déverrouillage #}
|
||||
<mj-image src="https://cdn-icons-png.flaticon.com/512/7124/7124533.png" width="60px" padding-bottom="20px" />
|
||||
|
||||
<mj-text align="center" font-size="28px" font-weight="900" color="#0f172a" text-transform="uppercase" font-style="italic" padding-bottom="0px">
|
||||
Caution <span color="#16a34a">Libérée</span>
|
||||
</mj-text>
|
||||
|
||||
<mj-text align="center" font-size="10px" font-weight="800" color="#64748b" letter-spacing="2px" text-transform="uppercase" padding-top="10px">
|
||||
Réservation #{{ datas.contrat.numReservation }}
|
||||
</mj-text>
|
||||
|
||||
<mj-divider border-width="1px" border-color="#f1f5f9" padding="30px 0" />
|
||||
|
||||
<mj-text font-size="15px" color="#334155" line-height="1.5">
|
||||
Bonjour <strong>{{ datas.customer.surname }}</strong>,
|
||||
<br /><br />
|
||||
Bonne nouvelle ! Suite à la vérification du matériel de votre réservation, nous vous confirmons que votre <strong>caution a été intégralement libérée</strong>.
|
||||
<br /><br />
|
||||
L'empreinte bancaire qui avait été effectuée est désormais annulée. Votre plafond bancaire sera mis à jour automatiquement par votre établissement bancaire sous un délai habituel de 24h à 48h.
|
||||
</mj-text>
|
||||
|
||||
<mj-wrapper background-color="#f0fdf4" border-radius="16px" padding="20px">
|
||||
<mj-section padding="0">
|
||||
<mj-column width="50%">
|
||||
<mj-text font-size="10px" font-weight="800" color="#16a34a" text-transform="uppercase">Statut final</mj-text>
|
||||
<mj-text font-size="20px" font-weight="900" font-style="italic" color="#16a34a" padding-top="5px">
|
||||
ANNULÉE
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="50%">
|
||||
<mj-text font-size="10px" font-weight="800" color="#94a3b8" text-transform="uppercase" align="right">Montant libéré</mj-text>
|
||||
<mj-text font-size="20px" font-weight="900" color="#475569" align="right" padding-top="5px">
|
||||
{{ datas.payment.amount|number_format(2, ',', ' ') }}€
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-text font-size="12px" color="#94a3b8" align="center" padding-top="20px" line-height="1.4">
|
||||
<em>Note : Puisqu'il s'agissait d'une pré-autorisation, aucune transaction de "remboursement" n'apparaîtra sur votre relevé, la ligne de débit initial disparaîtra simplement ou passera en statut annulé.</em>
|
||||
</mj-text>
|
||||
|
||||
<mj-button background-color="#0f172a" color="#ffffff" border-radius="12px" font-weight="800" text-transform="uppercase" font-size="12px" padding-top="30px" href="{{ datas.reservationLink }}">
|
||||
Voir ma réservation
|
||||
</mj-button>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user