feat: CustomerPaymentMethod + prelevement auto avis dernier jour du mois

Entity CustomerPaymentMethod:
- customer, stripePaymentMethodId, type (sepa_debit/card)
- last4, brand, country, isDefault
- getDisplayLabel() pour affichage

Sauvegarde automatique du moyen de paiement:
- Contrat SEPA setup: cree CustomerPaymentMethod type SEPA
- Contrat CB premier paiement: webhook sauvegarde la carte
- Retire le default des anciens moyens de paiement

Commande cron app:advert:auto-payment:
- S'execute uniquement le dernier jour du mois
- Trouve les avis envoyes (state=send) avec client ayant un
  moyen de paiement par defaut
- Envoie un email d'annonce de prelevement au client
- Cree un PaymentIntent off_session avec le moyen de paiement
- Le webhook payment_intent.succeeded traite le paiement

Admin fiche client tab info:
- Affiche les moyens de paiement enregistres (type, last4, defaut)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-09 16:10:08 +02:00
parent f51f28fc0b
commit 6e5e389b7d
7 changed files with 472 additions and 0 deletions

View File

@@ -219,6 +219,33 @@
{% endif %}
</div>
{# Moyens de paiement #}
{% if paymentMethods|length > 0 %}
<div class="glass p-5 mt-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-3">Moyens de paiement enregistres</h2>
<div class="space-y-2">
{% for pm in paymentMethods %}
<div class="glass p-3 flex items-center justify-between">
<div class="flex items-center gap-3">
{% if pm.type == 'sepa_debit' %}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg>
{% endif %}
<div>
<p class="text-xs font-bold">{{ pm.displayLabel }}</p>
<p class="text-[10px] text-gray-400">Ajoute le {{ pm.createdAt|date('d/m/Y') }}</p>
</div>
</div>
{% if pm.isDefault %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[9px]">Par defaut</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{# Tab: Contacts #}
{% elseif tab == 'contacts' %}
<section class="glass p-6 mb-6">

View File

@@ -0,0 +1,43 @@
{% extends 'email/base.html.twig' %}
{% block content %}
<table width="600" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tr>
<td style="padding: 32px;">
{% set greeting = customer.raisonSociale ? 'Chez ' ~ customer.raisonSociale : 'Bonjour ' ~ customer.firstName %}
<h1 style="font-family: Arial, Helvetica, sans-serif; font-size: 22px; font-weight: 700; color: #111827; margin: 0 0 16px;">{{ greeting }},</h1>
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #374151; line-height: 22px; margin: 0 0 16px;">
Nous vous informons qu'un prelevement automatique sera effectue pour votre avis de paiement <strong>{{ advert.orderNumber.numOrder }}</strong>.
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 20px 0; border: 1px solid #e5e7eb;">
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb; width: 35%;">Avis</td>
<td style="padding: 10px 16px; font-family: monospace; font-size: 13px; font-weight: 700;">{{ advert.orderNumber.numOrder }}</td>
</tr>
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Montant</td>
<td style="padding: 10px 16px; font-family: monospace; font-size: 16px; font-weight: 700; color: #fabf04;">{{ amount|number_format(2, ',', ' ') }} &euro;</td>
</tr>
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Moyen de paiement</td>
<td style="padding: 10px 16px; font-size: 13px; font-weight: 700;">{{ methodLabel }}</td>
</tr>
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Date du prelevement</td>
<td style="padding: 10px 16px; font-size: 13px; font-weight: 700;">{{ "now"|date('d/m/Y') }}</td>
</tr>
</table>
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #374151; line-height: 22px; margin: 16px 0;">
Ce prelevement sera effectue automatiquement via votre moyen de paiement enregistre. Vous recevrez un email de confirmation une fois le paiement traite.
</p>
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: #9ca3af; margin: 16px 0 0;">
Pour toute question : <a href="mailto:client@e-cosplay.fr" style="color: #fabf04;">client@e-cosplay.fr</a>
</p>
</td>
</tr>
</table>
{% endblock %}