```
✨ 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:
34
migrations/Version20260206150000.php
Normal file
34
migrations/Version20260206150000.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
migrations/Version20260206160000.php
Normal file
34
migrations/Version20260206160000.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user