feat(contrat/view): Ajoute l'attribut data-turbo="false" au lien d'acompte.
🐛 fix(RedirecListener): Exclut les webhooks de la redirection ngrok.
 feat(Webhooks): Crée des ProductReserve lors du paiement d'un acompte.
 feat(ContratPdfService): Remplace le code-barres par un QR code.
 feat(ContratController): Gère le paiement de l'acompte via Stripe.
```
This commit is contained in:
Serreau Jovann
2026-01-29 13:47:33 +01:00
parent e5252b2932
commit 85afa1b31b
5 changed files with 63 additions and 52 deletions

View File

@@ -15,6 +15,7 @@ use App\Repository\ContratsRepository;
use App\Repository\CustomerAddressRepository;
use App\Repository\CustomerRepository;
use App\Service\Mailer\Mailer;
use App\Service\Pdf\ContratPdfService;
use App\Service\Pdf\PlPdf;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
@@ -227,7 +228,6 @@ class ContratController extends AbstractController
if ($request->query->has('act') && $request->query->get('act') === 'accomptePay') {
// On vérifie s'il n'y a pas déjà un acompte payé ou en cours
$existingPayment = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
'contrat' => $contrat,
'type' => 'accompte',
@@ -400,7 +400,6 @@ class ContratController extends AbstractController
return $this->file($path, 'confirmation-paiement.pdf', ResponseHeaderBag::DISPOSITION_INLINE);
}
}
return $this->render('reservation/contrat/view.twig', [
'contrat' => $contrat,
'days' => $totalDays,

View File

@@ -45,28 +45,42 @@ class Webhooks extends AbstractController
$pl->setValidateAt(new \DateTimeImmutable('now'));
$pl->setCard($client->paymentMethod($session['payment_intent']));
$contrat = $pl->getContrat();
$customer = $contrat->getCustomer();
// Gestion des réservations produits (si pas déjà géré par un devis)
if (!$contrat->getDevis() instanceof Devis) {
$contrat = $pl->getContrat();
if($pl->getType() == "accompte") {
foreach ($contrat->getContratsLines() as $line) {
$product = $productRepository->findOneBy(['name' => $line->getName()]);
if ($product instanceof Product) {
$pres = new ProductReserve();
$pres->setProduct($product); // Corrigé ici
$pres->setStartAt($contrat->getDateAt());
$pres->setEndAt($contrat->getEndAt());
$pres->setContrat($contrat);
$entityManager->persist($pres);
}
}
} else {
foreach ($contrat->getDevis()->getProductReserve() as $productReserve) {
$productReserve->setContrat($contrat);
$entityManager->persist($productReserve);
$p = $productRepository->findOneBy(['name' => $line->getProduct()]);
$pr = new ProductReserve();
$pr->setContrat($contrat);
$pr->setProduct($p);
$pr->setStartAt($contrat->getDateAt());
$pr->setEndAt($contrat->getEndAt());
$pr->setCustomer($contrat->getCustomer());
$pr->setDevis($contrat->getDevis());
$entityManager->persist($pr);
$entityManager->flush();
}
}
$contrat = $pl->getContrat();
if($pl->getType() == "accompte") {
foreach ($contrat->getContratsLines() as $line) {
$r = explode(" - ",$line->getName());
if(isset($r[1])) {
$p = $productRepository->findOneBy(['ref' => $r[1]]);
$pr = new ProductReserve();
$pr->setContrat($contrat);
$pr->setProduct($p);
$pr->setStartAt($contrat->getDateAt());
$pr->setEndAt($contrat->getEndAt());
$pr->setCustomer($contrat->getCustomer());
$entityManager->persist($pr);
}
}
}
$customer = $contrat->getCustomer();
$pdf = new PlPdf($kernel, $pl, $contrat);
$pdf->generate();

View File

@@ -26,7 +26,7 @@ class RedirecListener
// --- Logique Spécifique DEV / NGROK ---
// Si on est en dev et que le host contient "ngrok-free.app" (ou "ngrok")
if ($this->isDev && str_contains($host, 'ngrok')) {
if ($this->isDev && str_contains($host, 'ngrok') && !str_contains($pathInfo,'webhooks')) {
// On redirige vers le domaine local esyweb.local en conservant le chemin et les paramètres
$newUrl = "https://esyweb.local" . $pathInfo;
$queryString = $request->getQueryString();

View File

@@ -3,6 +3,12 @@
namespace App\Service\Pdf;
use App\Entity\Contrats;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\Label\LabelAlignment;
use Endroid\QrCode\RoundBlockSizeMode;
use Endroid\QrCode\Writer\PngWriter;
use Fpdf\Fpdf;
use Symfony\Component\HttpKernel\KernelInterface;
@@ -28,38 +34,30 @@ class ContratPdfService extends Fpdf
/**
* Génère un code-barres Code 39
*/
private function Code39($xpos, $ypos, $code, $baseline = 0.5, $height = 5)
private function insertQRCode(float $x, float $y, string $data, int $size = 22): void
{
$wide = $baseline * 2.25;
$narrow = $baseline;
$gap = $narrow;
$builder = new Builder(
writer: new PngWriter(),
writerOptions: [],
validateResult: false,
data: $data,
encoding: new Encoding('UTF-8'),
errorCorrectionLevel: ErrorCorrectionLevel::High,
size: 300,
margin: 0,
roundBlockSizeMode: RoundBlockSizeMode::Margin,
);
$barChar = [
'0' => 'nnnwwnwnn', '1' => 'wnnwnnnnw', '2' => 'nnwwnnnnw', '3' => 'wnwwnnnnn',
'4' => 'nnnwwnnnw', '5' => 'wnnwwnnnn', '6' => 'nnwwnwnnn', '7' => 'nnnwnnwnw',
'8' => 'wnnwnnwnn', '9' => 'nnwwnnwnn', 'A' => 'wnnnnwnnw', 'B' => 'nnwnnwnnw',
'C' => 'wnwnnwnnn', 'D' => 'nnnnwwnnw', 'E' => 'wnnnwwnnn', 'F' => 'nnwnwwnnn',
'G' => 'nnnnnwwnw', 'H' => 'wnnnnwwnn', 'I' => 'nnwnnwwnn', 'J' => 'nnnnwwwnn',
'K' => 'wnnnnnnww', 'L' => 'nnwnnnnww', 'M' => 'wnwnnnnwn', 'N' => 'nnnnwnnww',
'O' => 'wnnnwnnwn', 'P' => 'nnwnwnnwn', 'Q' => 'nnnnnnwww', 'R' => 'wnnnnnwwn',
'S' => 'nnwnnnwwn', 'T' => 'nnnnwnwwn', 'U' => 'wwnnnnnnw', 'V' => 'nwwnnnnnw',
'W' => 'wwwnnnnnn', 'X' => 'nwnnwnnnw', 'Y' => 'wwnnwnnnn', 'Z' => 'nwwnwnnnn',
'-' => 'nwnnnnwnw', '.' => 'wwnnnnwnn', ' ' => 'nwwnnnwnn', '*' => 'nwnnwnwnn',
'$' => 'nwnwnwnnn', '/' => 'nwnwnnnwn', '+' => 'nwnnnwnwn', '%' => 'nnnwnwnwn'
];
$result = $builder->build();
$this->SetFillColor(0);
$code = '*' . strtoupper($code) . '*';
for ($i = 0; $i < strlen($code); $i++) {
$char = $code[$i];
if (!isset($barChar[$char])) continue;
$seq = $barChar[$char];
for ($bar = 0; $bar < 9; $bar++) {
if ($seq[$bar] == 'n') $lineWidth = $narrow; else $lineWidth = $wide;
if ($bar % 2 == 0) $this->Rect($xpos, $ypos, $lineWidth, $height, 'F');
$xpos += $lineWidth;
}
$xpos += $gap;
// Utilisation d'un fichier temporaire pour FPDF
$tmpFile = tempnam(sys_get_temp_dir(), 'qr_');
$result->saveToFile($tmpFile);
$this->Image($tmpFile, $x, $y, $size, $size, 'PNG');
if (file_exists($tmpFile)) {
unlink($tmpFile);
}
}
@@ -113,7 +111,7 @@ class ContratPdfService extends Fpdf
// Paramètres : X=160 (à droite), Y=42, Code=Numéro, Largeur barre=0.3, Hauteur=8
$this->Code39(155, 41, $this->contrats->getNumReservation(), 0.3, 8);
$this->insertQRCode(175, 10, $this->contrats->getNumReservation(), 22);
$this->Ln(10); // Retour à la ligne après le titre et le barcode

View File

@@ -264,7 +264,7 @@
<div class="p-6 text-center">
<p class="text-3xl font-black text-slate-900 italic mb-4">{{ arrhes|number_format(2, ',', ' ') }}€</p>
{% if contrat.signed %}
<a href="{{ path('gestion_contrat_view', {'num': contrat.numReservation,'act':'accomptePay'}) }}" class="block w-full bg-slate-900 text-white py-4 rounded-xl font-black uppercase text-xs hover:bg-blue-600 transition-all shadow-md">Régler l'acompte</a>
<a data-turbo="false" href="{{ path('gestion_contrat_view', {'num': contrat.numReservation,'act':'accomptePay'}) }}" class="block w-full bg-slate-900 text-white py-4 rounded-xl font-black uppercase text-xs hover:bg-blue-600 transition-all shadow-md">Régler l'acompte</a>
{% else %}
<div class="p-3 bg-amber-50 rounded-xl border border-amber-100">
<span class="text-[9px] text-amber-600 font-black uppercase tracking-tighter">Attente de signature du contrat</span>