feat: E-Flex - sauvegarde PaymentMethod CB + echeances auto (1ere +2j, suivantes tous les 2 mois)

- Checkout Session avec setup_future_usage=off_session + customer Stripe
  pour sauvegarder la carte et permettre les prelevements futurs
- Webhook payment_intent.succeeded stocke le PaymentMethod sur EFlex
  si pas deja configure (permet cron auto ensuite)
- 1ere echeance = creation +2 jours (pas de champ startDate)
- Echeances suivantes = tous les 2 mois apres la 1ere
- Retrait du champ date 1ere echeance du formulaire (automatique)
- Info dans le formulaire: "La 1ere echeance sera due 2 jours apres
  la signature. Les suivantes tous les 2 mois."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-09 07:54:01 +02:00
parent bbb9ad318e
commit 1c5e099598
4 changed files with 32 additions and 11 deletions

View File

@@ -42,10 +42,8 @@ class EFlexController extends AbstractController
$description = trim($request->request->getString('description')); $description = trim($request->request->getString('description'));
$totalAmount = $request->request->getString('totalAmount'); $totalAmount = $request->request->getString('totalAmount');
$nbEcheances = $request->request->getInt('nbEcheances'); $nbEcheances = $request->request->getInt('nbEcheances');
$startDate = $request->request->getString('startDate');
$paymentMethod = $request->request->getString('paymentMethod', EFlex::METHOD_SEPA);
if ('' === $description || $nbEcheances < 2 || $nbEcheances > 36 || '' === $startDate) { if ('' === $description || $nbEcheances < 2 || $nbEcheances > 36) {
$this->addFlash('error', 'Donnees invalides. Minimum 2 echeances, maximum 36.'); $this->addFlash('error', 'Donnees invalides. Minimum 2 echeances, maximum 36.');
return $this->redirectToRoute('app_admin_clients_show', ['id' => $customerId, 'tab' => 'esyflex']); return $this->redirectToRoute('app_admin_clients_show', ['id' => $customerId, 'tab' => 'esyflex']);
@@ -55,12 +53,12 @@ class EFlexController extends AbstractController
$monthlyAmount = round($totalFloat / $nbEcheances, 2); $monthlyAmount = round($totalFloat / $nbEcheances, 2);
$eflex = new EFlex($customer, $description, number_format($totalFloat, 2, '.', '')); $eflex = new EFlex($customer, $description, number_format($totalFloat, 2, '.', ''));
$eflex->setPaymentMethod($paymentMethod);
$start = new \DateTimeImmutable($startDate); // 1ere echeance = maintenant +2 jours, les suivantes tous les 2 mois
$firstDate = (new \DateTimeImmutable())->modify('+2 days');
for ($i = 1; $i <= $nbEcheances; ++$i) { for ($i = 1; $i <= $nbEcheances; ++$i) {
$scheduledAt = $start->modify('+'.($i - 1).' months'); $scheduledAt = 1 === $i ? $firstDate : $firstDate->modify('+'.($i - 1) * 2 .' months');
$amount = $i === $nbEcheances $amount = $i === $nbEcheances
? number_format($totalFloat - ($monthlyAmount * ($nbEcheances - 1)), 2, '.', '') ? number_format($totalFloat - ($monthlyAmount * ($nbEcheances - 1)), 2, '.', '')
: number_format($monthlyAmount, 2, '.', ''); : number_format($monthlyAmount, 2, '.', '');

View File

@@ -313,13 +313,28 @@ class EFlexProcessController extends AbstractController
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
\Stripe\Stripe::setApiKey($stripeSk); \Stripe\Stripe::setApiKey($stripeSk);
$customer = $eflex->getCustomer();
// Creer le customer Stripe si besoin
$stripeCustomerId = $eflex->getStripeCustomerId() ?? $customer->getStripeCustomerId();
if (null === $stripeCustomerId) {
$stripeCustomer = \Stripe\Customer::create([
'email' => $customer->getEmail(),
'name' => $customer->getFullName(),
]);
$stripeCustomerId = $stripeCustomer->id;
$customer->setStripeCustomerId($stripeCustomerId);
$eflex->setStripeCustomerId($stripeCustomerId);
$this->em->flush();
}
$successUrl = $this->generateUrl('app_eflex_process', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL); $successUrl = $this->generateUrl('app_eflex_process', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL);
$cancelUrl = $successUrl; $cancelUrl = $successUrl;
$checkoutSession = \Stripe\Checkout\Session::create([ $checkoutSession = \Stripe\Checkout\Session::create([
'mode' => 'payment', 'mode' => 'payment',
'payment_method_types' => ['card'], 'payment_method_types' => ['card'],
'customer_email' => $eflex->getCustomer()->getEmail(), 'customer' => $stripeCustomerId,
'line_items' => [[ 'line_items' => [[
'price_data' => [ 'price_data' => [
'currency' => 'eur', 'currency' => 'eur',
@@ -331,6 +346,7 @@ class EFlexProcessController extends AbstractController
'quantity' => 1, 'quantity' => 1,
]], ]],
'payment_intent_data' => [ 'payment_intent_data' => [
'setup_future_usage' => 'off_session',
'metadata' => [ 'metadata' => [
'eflex_id' => (string) $eflex->getId(), 'eflex_id' => (string) $eflex->getId(),
'eflex_line_id' => (string) $line->getId(), 'eflex_line_id' => (string) $line->getId(),

View File

@@ -685,6 +685,16 @@ class WebhookStripeController extends AbstractController
$line->setPaidAt(new \DateTimeImmutable()); $line->setPaidAt(new \DateTimeImmutable());
$line->setStripePaymentIntentId($paymentIntent->id); $line->setStripePaymentIntentId($paymentIntent->id);
$line->setPaidMethod('stripe'); $line->setPaidMethod('stripe');
// Sauvegarder le PaymentMethod pour les prelevements futurs (si pas deja fait)
if (null === $eflex->getStripePaymentMethodId()) {
$pmId = $paymentIntent->payment_method ?? null;
if (null !== $pmId) {
$eflex->setStripePaymentMethodId((string) $pmId);
$eflex->setState(\App\Entity\EFlex::STATE_ACTIVE);
}
}
$this->em->flush(); $this->em->flush();
if ($eflex->getNbPaid() >= $eflex->getNbLines()) { if ($eflex->getNbPaid() >= $eflex->getNbLines()) {

View File

@@ -1284,11 +1284,8 @@
<label for="eflex-nbEcheances" class="block text-[9px] font-bold uppercase tracking-wider text-gray-400 mb-1">Nombre d'echeances *</label> <label for="eflex-nbEcheances" class="block text-[9px] font-bold uppercase tracking-wider text-gray-400 mb-1">Nombre d'echeances *</label>
<input type="number" id="eflex-nbEcheances" name="nbEcheances" min="2" max="36" value="3" required class="input-glass w-full px-3 py-2 text-xs font-bold"> <input type="number" id="eflex-nbEcheances" name="nbEcheances" min="2" max="36" value="3" required class="input-glass w-full px-3 py-2 text-xs font-bold">
</div> </div>
<div>
<label for="eflex-startDate" class="block text-[9px] font-bold uppercase tracking-wider text-gray-400 mb-1">Date 1ere echeance *</label>
<input type="date" id="eflex-startDate" name="startDate" required class="input-glass w-full px-3 py-2 text-xs font-bold">
</div>
</div> </div>
<p class="text-[10px] text-gray-400 mb-4">La 1ere echeance sera due 2 jours apres la signature. Les suivantes tous les 2 mois.</p>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button type="button" data-modal-close="modal-eflex" class="px-4 py-2 glass font-bold uppercase text-[10px] tracking-widest">Annuler</button> <button type="button" data-modal-close="modal-eflex" class="px-4 py-2 glass font-bold uppercase text-[10px] tracking-widest">Annuler</button>
<button type="submit" class="btn-gold px-4 py-2 font-bold uppercase text-[10px] tracking-wider text-gray-900">Creer</button> <button type="submit" class="btn-gold px-4 py-2 font-bold uppercase text-[10px] tracking-wider text-gray-900">Creer</button>