feat(contrats): Améliore la gestion des contrats et des paiements.

- Rend le champ details non obligatoire dans add.twig
- Ajoute une valeur par défaut pour isSigned et type dans les entités.
- Corrige l'ajout des lignes et options au contrat.
- Ajoute la création automatique du client Stripe.
```
This commit is contained in:
Serreau Jovann
2026-02-06 11:06:38 +01:00
parent a6e5d5f4a8
commit 7ff3538bcd
8 changed files with 114 additions and 34 deletions

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260206150000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Set default value for is_signed in contrats table';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE contrats ALTER is_signed SET DEFAULT false');
$this->addSql('UPDATE contrats SET is_signed = false WHERE is_signed IS NULL');
$this->addSql('ALTER TABLE contrats ALTER is_signed SET NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE contrats ALTER is_signed DROP DEFAULT');
$this->addSql('ALTER TABLE contrats ALTER is_signed DROP NOT NULL');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260206160000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Set default value for type in contrats_line table';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql("ALTER TABLE contrats_line ALTER type SET DEFAULT 'product'");
$this->addSql("UPDATE contrats_line SET type = 'product' WHERE type IS NULL");
$this->addSql("ALTER TABLE contrats_line ALTER type SET NOT NULL");
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE contrats_line ALTER type DROP DEFAULT');
$this->addSql('ALTER TABLE contrats_line ALTER type DROP NOT NULL');
}
}

View File

@@ -166,7 +166,7 @@ class ContratController extends AbstractController
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true"; $tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
$totals = $this->calculateContractTotals($contrat, $tvaEnabled); $totals = $this->calculateContractTotals($contrat, $tvaEnabled);
$totalDays = $totals['days']; $totalDays = $totals['days'];
$totalHT = $totals['totalHT']; $totalHT = $totals['totalHT'];
$totalTTC = $totals['totalTTC']; $totalTTC = $totals['totalTTC'];
@@ -205,11 +205,11 @@ class ContratController extends AbstractController
if ($request->query->has('act') && $request->query->get('act') === 'accomptePay') { if ($request->query->has('act') && $request->query->get('act') === 'accomptePay') {
$response = $this->handlePayment( $response = $this->handlePayment(
$contrat, $contrat,
'accompte', 'accompte',
$arrhes, $arrhes,
$entityManager, $entityManager,
$stripeClient, $stripeClient,
false false
); );
if ($response) return $response; if ($response) return $response;
@@ -222,15 +222,15 @@ class ContratController extends AbstractController
$type = $isSoldeTotal ? 'solde' : 'solde_partiel'; $type = $isSoldeTotal ? 'solde' : 'solde_partiel';
$response = $this->handlePayment( $response = $this->handlePayment(
$contrat, $contrat,
$type, $type,
$finalAmount, $finalAmount,
$entityManager, $entityManager,
$stripeClient, $stripeClient,
$isSoldeTotal $isSoldeTotal
); );
if ($response) return $response; if ($response) return $response;
// Fallback // Fallback
$this->addFlash('error', 'Impossible de générer le lien de paiement.'); $this->addFlash('error', 'Impossible de générer le lien de paiement.');
return new RedirectResponse($request->headers->get('referer')); return new RedirectResponse($request->headers->get('referer'));
@@ -291,7 +291,8 @@ class ContratController extends AbstractController
EntityManagerInterface $em, EntityManagerInterface $em,
UserPasswordHasherInterface $hasher, UserPasswordHasherInterface $hasher,
CustomerRepository $customerRepository, CustomerRepository $customerRepository,
Security $security // Injection du service Security Security $security, // Injection du service Security
\App\Service\Stripe\Client $stripeClient
): Response { ): Response {
$session = $request->getSession(); $session = $request->getSession();
$customer = $session->get('config_customer_id') ? $customerRepository->find($session->get('config_customer_id')) : null; $customer = $session->get('config_customer_id') ? $customerRepository->find($session->get('config_customer_id')) : null;
@@ -315,9 +316,17 @@ class ContratController extends AbstractController
$customer->setVerificationCode(null); $customer->setVerificationCode(null);
$customer->setVerificationCodeExpiresAt(null); $customer->setVerificationCodeExpiresAt(null);
// Création Stripe automatique
try {
$stripeClient->createCustomer($customer);
} catch (\Exception $e) {
// Log l'erreur mais ne bloque pas le process, on pourra le refaire plus tard
// $logger->error('Erreur création Stripe auto : ' . $e->getMessage());
}
$em->flush(); $em->flush();
$security->login($customer, 'form_login', 'main'); $security->login($customer, \App\Security\CustomerAuthenticator::class, 'main');
return $this->render('reservation/contrat/finish_activate.twig', [ return $this->render('reservation/contrat/finish_activate.twig', [
'customer' => $customer, 'customer' => $customer,
@@ -421,13 +430,13 @@ class ContratController extends AbstractController
foreach ($contrat->getContratsLines() as $line) { foreach ($contrat->getContratsLines() as $line) {
$linePriceHT = $line->getPrice1DayHt(); $linePriceHT = $line->getPrice1DayHt();
if ($totalDays > 1) { if ($totalDays > 1) {
$linePriceHT += (($line->getPriceSupDayHt()) * ($totalDays - 1)); $linePriceHT += (($line->getPriceSupDayHt()) * ($totalDays - 1));
} }
$totalHT += $linePriceHT; $totalHT += $linePriceHT;
if ($tvaEnabled) { if ($tvaEnabled) {
// Calculation matches original logic: (Price1Day * 1.2) + (PriceSup * 1.2 * days) // Calculation matches original logic: (Price1Day * 1.2) + (PriceSup * 1.2 * days)
$linePriceTTC = $line->getPrice1DayHt() * 1.20; $linePriceTTC = $line->getPrice1DayHt() * 1.20;
@@ -479,7 +488,7 @@ class ContratController extends AbstractController
'state' => ['complete', 'created'], 'state' => ['complete', 'created'],
'type' => $type 'type' => $type
]; ];
// For solde/solde_partiel, we only check for 'created' to allow new attempts if previous failed/expired // For solde/solde_partiel, we only check for 'created' to allow new attempts if previous failed/expired
// but typically 'solde' payments are unique or sequential. // but typically 'solde' payments are unique or sequential.
if ($type === 'solde' || $type === 'solde_partiel') { if ($type === 'solde' || $type === 'solde_partiel') {
@@ -493,6 +502,18 @@ class ContratController extends AbstractController
$existingPayment = $em->getRepository(ContratsPayments::class)->findOneBy($criteria); $existingPayment = $em->getRepository(ContratsPayments::class)->findOneBy($criteria);
if (!$existingPayment) { if (!$existingPayment) {
// Check if customer exists in Stripe
$customer = $contrat->getCustomer();
if (!$customer->getCustomerId()) {
try {
$stripeClient->createCustomer($customer);
$em->flush();
} catch (\Exception $e) {
// Log error or handle it, maybe return null to show error
// For now we continue, assuming createPayment might fail gracefully or we just try
}
}
// Create new payment intent // Create new payment intent
if ($type === 'accompte') { if ($type === 'accompte') {
$result = $stripeClient->createPaymentAccompte($amount, $contrat); $result = $stripeClient->createPaymentAccompte($amount, $contrat);

View File

@@ -331,15 +331,16 @@ class ContratsController extends AbstractController
->setPriceSupDayHt($line['priceHtSupDay']) ->setPriceSupDayHt($line['priceHtSupDay'])
->setCaution($line['caution']); ->setCaution($line['caution']);
$this->em->persist($vc); $this->em->persist($vc);
$contrat->addContratsLine($vc);
} }
foreach ($postData['options'] ?? [] as $opt) { foreach ($postData['options'] ?? [] as $opt) {
$vo = (new ContratsOption()) $vo = (new ContratsOption())
->setContrat($contrat)
->setName($opt['name']) ->setName($opt['name'])
->setDetails($opt['details']) ->setDetails($opt['details'])
->setPrice($opt['priceHt']); ->setPrice($opt['priceHt']);
$this->em->persist($vo); $this->em->persist($vo);
$contrat->addContratsOption($vo);
} }
$contrat->setNumReservation($this->generateReservationNumber()); $contrat->setNumReservation($this->generateReservationNumber());

View File

@@ -68,8 +68,8 @@ class Contrats
#[ORM\Column(type: Types::TEXT,nullable: true)] #[ORM\Column(type: Types::TEXT,nullable: true)]
private ?string $notes = null; private ?string $notes = null;
#[ORM\Column] #[ORM\Column(options: ['default' => false])]
private ?bool $isSigned = null; private ?bool $isSigned = false;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $signID = null; private ?string $signID = null;

View File

@@ -28,8 +28,8 @@ class ContratsLine
#[ORM\Column] #[ORM\Column]
private ?float $caution = null; private ?float $caution = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255, options: ['default' => 'product'])]
private ?string $type = null; private ?string $type = 'product';
public function getId(): ?int public function getId(): ?int
{ {

View File

@@ -237,7 +237,7 @@
</div> </div>
<div class="lg:col-span-2"> <div class="lg:col-span-2">
<label class="text-[9px] font-black text-slate-300 uppercase tracking-widest ml-1 mb-2 block">Détails</label> <label class="text-[9px] font-black text-slate-300 uppercase tracking-widest ml-1 mb-2 block">Détails</label>
<input type="text" name="options[{{ key }}][details]" value="{{ line.details }}" required class="w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-purple-500/20 focus:border-purple-500 transition-all py-3 px-5 text-sm font-mono"> <input type="text" name="options[{{ key }}][details]" value="{{ line.details }}" class="w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-purple-500/20 focus:border-purple-500 transition-all py-3 px-5 text-sm font-mono">
</div> </div>
{# 2. PRIX 1J #} {# 2. PRIX 1J #}
<div class="lg:col-span-3"> <div class="lg:col-span-3">

View File

@@ -89,16 +89,6 @@
{% endif %} {% endif %}
</div> </div>
{# CAUTION #}
<div class="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-xl border border-gray-100">
<span class="text-[10px] font-bold text-gray-400 uppercase">Caution</span>
{% if contratPaymentPay(contrat, 'caution') %}
<span class="text-[10px] font-black text-green-600 bg-green-100 px-2 py-0.5 rounded-lg uppercase tracking-tight">Réceptionnée</span>
{% else %}
<span class="text-[10px] font-black text-red-500 bg-red-100 px-2 py-0.5 rounded-lg uppercase tracking-tight">Manquante</span>
{% endif %}
</div>
{# SOLDE #} {# SOLDE #}
<div class="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-xl border border-gray-100"> <div class="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-xl border border-gray-100">
<span class="text-[10px] font-bold text-gray-400 uppercase">Solde</span> <span class="text-[10px] font-bold text-gray-400 uppercase">Solde</span>