```
✨ feat(Contrats): Ajoute la gestion de l'état de la caution (restituée/encaissée).
```
This commit is contained in:
32
migrations/Version20260123115335.php
Normal file
32
migrations/Version20260123115335.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?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 Version20260123115335 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE contrats ADD caution_state VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE contrats DROP caution_state');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace App\Controller\Dashboard;
|
|||||||
use App\Entity\Contrats;
|
use App\Entity\Contrats;
|
||||||
use App\Entity\ContratsLine;
|
use App\Entity\ContratsLine;
|
||||||
use App\Entity\ContratsOption;
|
use App\Entity\ContratsOption;
|
||||||
|
use App\Entity\ContratsPayments;
|
||||||
use App\Entity\Devis;
|
use App\Entity\Devis;
|
||||||
use App\Event\Signature\ContratEvent;
|
use App\Event\Signature\ContratEvent;
|
||||||
use App\Form\Type\ContratsType;
|
use App\Form\Type\ContratsType;
|
||||||
@@ -79,6 +80,7 @@ class ContratsController extends AbstractController
|
|||||||
EntityManagerInterface $entityManager,
|
EntityManagerInterface $entityManager,
|
||||||
Request $request,
|
Request $request,
|
||||||
Client $client,
|
Client $client,
|
||||||
|
\App\Service\Stripe\Client $stripeClient,
|
||||||
DevisRepository $devisRepository,
|
DevisRepository $devisRepository,
|
||||||
AppLogger $appLogger,
|
AppLogger $appLogger,
|
||||||
EventDispatcherInterface $eventDispatcher,
|
EventDispatcherInterface $eventDispatcher,
|
||||||
@@ -128,6 +130,40 @@ class ContratsController extends AbstractController
|
|||||||
|
|
||||||
$solde = $totalHt - $dejaPaye;
|
$solde = $totalHt - $dejaPaye;
|
||||||
|
|
||||||
|
|
||||||
|
if($request->query->has('act') && $request->query->get('act') === 'cautionCapture') {
|
||||||
|
$amount = $request->query->get('amountToCapture');
|
||||||
|
$paiementCaution = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||||
|
'contrat' => $contrat,
|
||||||
|
'type' => 'caution',
|
||||||
|
]);
|
||||||
|
$result = $stripeClient->capture($paiementCaution->getPaymentId());
|
||||||
|
if($result['state']) {
|
||||||
|
$contrat->setCautionState("recover");
|
||||||
|
$entityManager->persist($contrat);
|
||||||
|
$entityManager->flush();
|
||||||
|
$this->addFlash("success","Caution restitué");
|
||||||
|
} else {
|
||||||
|
$this->addFlash("error",$result['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($request->query->has('act') && $request->query->get('act') === 'cautionRelease') {
|
||||||
|
|
||||||
|
$paiementCaution = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||||
|
'contrat' => $contrat,
|
||||||
|
'type' => 'caution',
|
||||||
|
]);
|
||||||
|
$result = $stripeClient->cancelPayment($paiementCaution->getPaymentId());
|
||||||
|
if($result['state']) {
|
||||||
|
$contrat->setCautionState("restitue");
|
||||||
|
$entityManager->persist($contrat);
|
||||||
|
$entityManager->flush();
|
||||||
|
$this->addFlash("success","Caution restitué");
|
||||||
|
} else {
|
||||||
|
$this->addFlash("error",$result['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $this->render('dashboard/contrats/view.twig', [
|
return $this->render('dashboard/contrats/view.twig', [
|
||||||
'contrat' => $contrat,
|
'contrat' => $contrat,
|
||||||
'days' => $days,
|
'days' => $days,
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ class Contrats
|
|||||||
#[ORM\OneToOne(mappedBy: 'contrat', cascade: ['persist', 'remove'])]
|
#[ORM\OneToOne(mappedBy: 'contrat', cascade: ['persist', 'remove'])]
|
||||||
private ?Facture $facture = null;
|
private ?Facture $facture = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $cautionState = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->contratsPayments = new ArrayCollection();
|
$this->contratsPayments = new ArrayCollection();
|
||||||
@@ -815,6 +818,18 @@ class Contrats
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCautionState(): ?string
|
||||||
|
{
|
||||||
|
return $this->cautionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCautionState(?string $cautionState): static
|
||||||
|
{
|
||||||
|
$this->cautionState = $cautionState;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -572,4 +572,36 @@ class Client
|
|||||||
|
|
||||||
return $newSession;
|
return $newSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function cancelPayment(?string $getPaymentId)
|
||||||
|
{
|
||||||
|
$session = $this->client->checkout->sessions->retrieve($getPaymentId);
|
||||||
|
$paymentIntent = $this->client->paymentIntents->retrieve($session->payment_intent);
|
||||||
|
if($paymentIntent->status == "requires_capture") {
|
||||||
|
$this->client->paymentIntents->cancel($paymentIntent->id);
|
||||||
|
return [
|
||||||
|
'state' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'state' => false,
|
||||||
|
'message' => 'Impossible d\'annuler la caution'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function capture(?string $getPaymentId)
|
||||||
|
{
|
||||||
|
$session = $this->client->checkout->sessions->retrieve($getPaymentId);
|
||||||
|
$paymentIntent = $this->client->paymentIntents->retrieve($session->payment_intent);
|
||||||
|
if($paymentIntent->status == "requires_capture") {
|
||||||
|
$this->client->paymentIntents->capture($paymentIntent->id);
|
||||||
|
return [
|
||||||
|
'state' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'state' => false,
|
||||||
|
'message' => 'Impossible de récupérer la caution'
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,36 @@
|
|||||||
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
Télécharger Contrat PDF
|
Télécharger Devis PDF
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if contrat.signed %}
|
||||||
|
<a href="{{ vich_uploader_asset(contrat,'devisSignFile') }}"
|
||||||
|
download
|
||||||
|
class="flex items-center gap-2 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 backdrop-blur-md rounded-xl text-white text-xs font-black uppercase italic transition-all group">
|
||||||
|
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Télécharger Contrat Signée PDF
|
||||||
|
</a>
|
||||||
|
<a href="{{ vich_uploader_asset(contrat,'devisAuditFile') }}"
|
||||||
|
download
|
||||||
|
class="flex items-center gap-2 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 backdrop-blur-md rounded-xl text-white text-xs font-black uppercase italic transition-all group">
|
||||||
|
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Télécharger Audit Signée PDF
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ vich_uploader_asset(contrat,'devisFile') }}"
|
||||||
|
download
|
||||||
|
class="flex items-center gap-2 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 backdrop-blur-md rounded-xl text-white text-xs font-black uppercase italic transition-all group">
|
||||||
|
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Télécharger Contrat PDF
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -64,54 +91,67 @@
|
|||||||
{# 3. CAUTION (MODE ADMINISTRATION) #}
|
{# 3. CAUTION (MODE ADMINISTRATION) #}
|
||||||
<div class="relative overflow-hidden bg-white/5 border border-white/10 backdrop-blur-xl p-6 rounded-[2rem]">
|
<div class="relative overflow-hidden bg-white/5 border border-white/10 backdrop-blur-xl p-6 rounded-[2rem]">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
{# Icône dynamique : Pulse si non déposée, fixe si OK #}
|
||||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center {{ cautionOk ? 'bg-purple-500/20 text-purple-400' : 'bg-rose-500/20 text-rose-500 animate-pulse' }}">
|
<div class="w-10 h-10 rounded-xl flex items-center justify-center {{ cautionOk ? 'bg-purple-500/20 text-purple-400' : 'bg-rose-500/20 text-rose-500 animate-pulse' }}">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-slate-500 text-[9px] font-black uppercase tracking-widest">Garantie (Caution)</p>
|
<p class="text-slate-500 text-[9px] font-black uppercase tracking-widest">Garantie (Caution)</p>
|
||||||
|
|
||||||
{% if not cautionOk %}
|
{% if not cautionOk %}
|
||||||
{# Affichage simple du statut sans lien #}
|
{# ÉTAT 1 : ATTENTE DE DÉPÔT #}
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<p class="text-rose-500 font-black italic uppercase text-sm leading-tight">
|
<p class="text-rose-500 font-black italic uppercase text-sm leading-tight">Non Déposée</p>
|
||||||
Non Déposée
|
|
||||||
</p>
|
|
||||||
<p class="text-[9px] text-slate-500 uppercase font-bold italic">Attendu : {{ totalCaution|number_format(2, ',', ' ') }}€</p>
|
<p class="text-[9px] text-slate-500 uppercase font-bold italic">Attendu : {{ totalCaution|number_format(2, ',', ' ') }}€</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{# État : Caution déposée - Actions d'administration #}
|
{# ÉTAT 2 : CAUTION DÉPOSÉE #}
|
||||||
<div class="space-y-3 mt-2">
|
{% if contrat.cautionState == null %}
|
||||||
<p class="text-white font-bold italic uppercase text-[10px] mb-2 opacity-70">Empreinte active</p>
|
{# ACTIONS DISPONIBLES #}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="space-y-3 mt-2">
|
||||||
{# ACTION LIBÉRER #}
|
<p class="text-white font-bold italic uppercase text-[10px] mb-2 opacity-70">Empreinte active</p>
|
||||||
<a href="{{ path('app_crm_contrats_view', {id: contrat.id, act: 'cautionRelease'}) }}"
|
<div class="flex flex-col gap-2">
|
||||||
data-turbo="false"
|
{# LIBÉRER #}
|
||||||
onclick="return confirm('Confirmer la libération de la caution ?')"
|
<a href="{{ path('app_crm_contrats_view', {id: contrat.id, act: 'cautionRelease'}) }}"
|
||||||
class="w-full py-1.5 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 rounded-lg text-emerald-400 text-[9px] font-black uppercase italic text-center transition-all">
|
data-turbo="false"
|
||||||
Libérer la caution
|
onclick="return confirm('Confirmer la libération de la caution ?')"
|
||||||
</a>
|
class="w-full py-1.5 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 rounded-lg text-emerald-400 text-[9px] font-black uppercase italic text-center transition-all">
|
||||||
|
Libérer la caution
|
||||||
|
</a>
|
||||||
|
|
||||||
{# ACTION ENCAISSER #}
|
{# ENCAISSER #}
|
||||||
<form action="{{ path('app_crm_contrats_view', {id: contrat.id}) }}" method="GET" data-turbo="false" class="space-y-1">
|
<form action="{{ path('app_crm_contrats_view', {id: contrat.id}) }}" method="GET" data-turbo="false" class="space-y-1">
|
||||||
<input type="hidden" name="act" value="cautionCapture">
|
<input type="hidden" name="act" value="cautionCapture">
|
||||||
<div class="flex items-center bg-rose-500/5 rounded-lg border border-rose-500/20 px-2 py-1">
|
<div class="flex items-center bg-rose-500/5 rounded-lg border border-rose-500/20 px-2 py-1">
|
||||||
<input type="number"
|
<input type="number" name="amountToCapture" step="0.01" max="{{ totalCaution }}" value="{{ totalCaution }}"
|
||||||
name="amountToCapture"
|
class="bg-transparent border-none text-rose-500 text-[10px] font-black w-14 p-0 focus:ring-0">
|
||||||
step="0.01"
|
<button type="submit" onclick="return confirm('Confirmer l\'encaissement ?')"
|
||||||
max="{{ totalCaution }}"
|
class="ml-auto text-rose-500 text-[9px] font-black uppercase italic hover:text-white transition-colors">
|
||||||
value="{{ totalCaution }}"
|
Encaisser
|
||||||
class="bg-transparent border-none text-rose-500 text-[10px] font-black w-14 p-0 focus:ring-0">
|
</button>
|
||||||
<button type="submit"
|
</div>
|
||||||
onclick="return confirm('Confirmer l\'encaissement partiel ou total ?')"
|
</form>
|
||||||
class="ml-auto text-rose-500 text-[9px] font-black uppercase italic hover:text-white transition-colors">
|
</div>
|
||||||
Encaisser
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
|
{# ÉTAT 3 : ARCHIVÉ (RESTITUÉ OU RÉCUPÉRÉ) #}
|
||||||
|
<div class="mt-2">
|
||||||
|
{% if contrat.cautionState == 'restitue' %}
|
||||||
|
<span class="px-3 py-1 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-[10px] font-black uppercase italic rounded-full">
|
||||||
|
Restituée
|
||||||
|
</span>
|
||||||
|
{% elseif contrat.cautionState == 'recover' %}
|
||||||
|
<span class="px-3 py-1 bg-amber-500/10 border border-amber-500/20 text-amber-400 text-[10px] font-black uppercase italic rounded-full">
|
||||||
|
Encaissée
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-white font-bold text-xs uppercase opacity-50">{{ contrat.cautionState }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user