Add stock management, order notifications, webhooks, expiration cron, and billet type validation
- Decrement billet quantity after purchase in BilletOrderService::generateOrderTickets - Block purchase when stock is exhausted (quantity <= 0) in OrderController::buildOrderItems - Add organizer email notification on new order (order_notification_orga template) - Add organizer email notification on cancel/refund (order_cancelled_orga template) - Add ExpirePendingOrdersCommand (app:orders:expire-pending) cron every 5min via Ansible - Cancels pending orders older than 30 minutes, restores stock, invalidates tickets - Includes BilletBuyerRepository::findExpiredPending query method - 3 unit tests covering: no expired orders, stock restoration, unlimited billets - Add payment_intent.payment_failed webhook: cancels order, logs audit, emails buyer - Add charge.refunded webhook: sets order to refunded, invalidates tickets, notifies orga and buyer - Validate billet type (billet/reservation_brocante/vote) against organizer offer - getAllowedBilletTypes: gratuit=billet only, basic/sur-mesure=all types - Server-side validation in hydrateBilletFromRequest, UI filtering in templates - Update TASK_CHECKUP.md: all Billetterie & Commandes items now complete Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,8 @@
|
||||
<label for="billet_type" class="text-xs font-black uppercase tracking-widest form-label">Type</label>
|
||||
<select id="billet_type" name="type" class="form-input focus:border-indigo-600">
|
||||
<option value="billet">Billet</option>
|
||||
<option value="reservation_brocante">Reservation brocante</option>
|
||||
<option value="vote">Vote</option>
|
||||
{% if 'reservation_brocante' in allowedTypes %}<option value="reservation_brocante">Reservation brocante</option>{% endif %}
|
||||
{% if 'vote' in allowedTypes %}<option value="vote">Vote</option>{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<label for="billet_type" class="text-xs font-black uppercase tracking-widest form-label">Type</label>
|
||||
<select id="billet_type" name="type" class="form-input focus:border-indigo-600">
|
||||
<option value="billet" {{ billet.type == 'billet' ? 'selected' : '' }}>Billet</option>
|
||||
<option value="reservation_brocante" {{ billet.type == 'reservation_brocante' ? 'selected' : '' }}>Reservation brocante</option>
|
||||
<option value="vote" {{ billet.type == 'vote' ? 'selected' : '' }}>Vote</option>
|
||||
{% if 'reservation_brocante' in allowedTypes %}<option value="reservation_brocante" {{ billet.type == 'reservation_brocante' ? 'selected' : '' }}>Reservation brocante</option>{% endif %}
|
||||
{% if 'vote' in allowedTypes %}<option value="vote" {{ billet.type == 'vote' ? 'selected' : '' }}>Vote</option>{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
53
templates/email/order_cancelled_orga.html.twig
Normal file
53
templates/email/order_cancelled_orga.html.twig
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends 'email/base.html.twig' %}
|
||||
|
||||
{% block title %}Commande {{ action }} - {{ order.event.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Commande {{ action }}</h2>
|
||||
<p>Bonjour,</p>
|
||||
<p>La commande <strong>{{ order.orderNumber }}</strong> pour l'evenement <strong>{{ order.event.title }}</strong> a ete <strong>{{ action }}</strong>.</p>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 20px 0; border: 2px solid #111827;">
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280; width: 140px;">Commande</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700; font-size: 14px;">{{ order.orderNumber }}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Statut</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700; color: #dc2626;">{{ action|upper }}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Acheteur</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700;">{{ order.firstName }} {{ order.lastName }}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Email</td>
|
||||
<td style="padding: 10px 12px;">{{ order.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Montant HT</td>
|
||||
<td style="padding: 10px 12px; font-weight: 900; color: #4f46e5;">{{ order.totalHTDecimal|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
|
||||
<thead>
|
||||
<tr style="background: #111827; color: #fff;">
|
||||
<th style="padding: 10px 12px; text-align: left; font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px;">Billet</th>
|
||||
<th style="padding: 10px 12px; text-align: center; font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px;">Qt</th>
|
||||
<th style="padding: 10px 12px; text-align: right; font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px;">Total HT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in order.items %}
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 700; font-size: 14px;">{{ item.billetName }}</td>
|
||||
<td style="padding: 10px 12px; text-align: center; font-weight: 700;">{{ item.quantity }}</td>
|
||||
<td style="padding: 10px 12px; text-align: right; font-weight: 900; color: #4f46e5;">{{ item.lineTotalHTDecimal|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="font-size: 12px; color: #9ca3af;">Les billets associes a cette commande ont ete invalides. Vous pouvez consulter le detail depuis votre espace organisateur.</p>
|
||||
{% endblock %}
|
||||
55
templates/email/order_notification_orga.html.twig
Normal file
55
templates/email/order_notification_orga.html.twig
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends 'email/base.html.twig' %}
|
||||
|
||||
{% block title %}Nouvelle commande - {{ order.event.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Nouvelle commande !</h2>
|
||||
<p>Bonjour,</p>
|
||||
<p>Une nouvelle commande a ete passee pour votre evenement <strong>{{ order.event.title }}</strong>.</p>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 20px 0; border: 2px solid #111827;">
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280; width: 140px;">Commande</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700; font-size: 14px;">{{ order.orderNumber }}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Acheteur</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700;">{{ order.firstName }} {{ order.lastName }}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Email</td>
|
||||
<td style="padding: 10px 12px;">{{ order.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Date</td>
|
||||
<td style="padding: 10px 12px;">{{ order.paidAt|date('d/m/Y H:i') }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
|
||||
<thead>
|
||||
<tr style="background: #111827; color: #fff;">
|
||||
<th style="padding: 10px 12px; text-align: left; font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px;">Billet</th>
|
||||
<th style="padding: 10px 12px; text-align: center; font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px;">Qt</th>
|
||||
<th style="padding: 10px 12px; text-align: right; font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px;">Total HT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in order.items %}
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 700; font-size: 14px;">{{ item.billetName }}</td>
|
||||
<td style="padding: 10px 12px; text-align: center; font-weight: 700;">{{ item.quantity }}</td>
|
||||
<td style="padding: 10px 12px; text-align: right; font-weight: 900; color: #4f46e5;">{{ item.lineTotalHTDecimal|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="border-top: 3px solid #111827;">
|
||||
<td colspan="2" style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 13px;">Total HT</td>
|
||||
<td style="padding: 10px 12px; text-align: right; font-weight: 900; font-size: 16px; color: #4f46e5;">{{ order.totalHTDecimal|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<p style="font-size: 12px; color: #9ca3af;">Vous pouvez consulter le detail de cette commande depuis votre espace organisateur, onglet Statistiques.</p>
|
||||
{% endblock %}
|
||||
24
templates/email/order_refunded.html.twig
Normal file
24
templates/email/order_refunded.html.twig
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'email/base.html.twig' %}
|
||||
|
||||
{% block title %}Remboursement - {{ order.event.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Votre commande a ete remboursee</h2>
|
||||
<p>Bonjour {{ order.firstName }},</p>
|
||||
<p>Votre commande <strong>{{ order.orderNumber }}</strong> pour l'evenement <strong>{{ order.event.title }}</strong> a ete remboursee.</p>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 20px 0; border: 2px solid #111827;">
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280; width: 140px;">Commande</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700;">{{ order.orderNumber }}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Montant rembourse</td>
|
||||
<td style="padding: 10px 12px; font-weight: 900; color: #16a34a;">{{ order.totalHTDecimal|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>Le remboursement sera visible sur votre releve bancaire sous 5 a 10 jours ouvrables. Les billets associes a cette commande ont ete invalides.</p>
|
||||
|
||||
<p style="font-size: 12px; color: #9ca3af;">Si vous avez des questions, contactez l'organisateur de l'evenement.</p>
|
||||
{% endblock %}
|
||||
28
templates/email/payment_failed.html.twig
Normal file
28
templates/email/payment_failed.html.twig
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'email/base.html.twig' %}
|
||||
|
||||
{% block title %}Echec de paiement - {{ order.event.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Echec de paiement</h2>
|
||||
<p>Bonjour {{ order.firstName }},</p>
|
||||
<p>Votre paiement pour la commande <strong>{{ order.orderNumber }}</strong> (evenement <strong>{{ order.event.title }}</strong>) n'a pas pu aboutir.</p>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 20px 0; border: 2px solid #dc2626;">
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280; width: 140px;">Commande</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700;">{{ order.orderNumber }}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Montant</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700; color: #4f46e5;">{{ order.totalHTDecimal|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 12px; font-weight: 900; text-transform: uppercase; font-size: 11px; color: #6b7280;">Motif</td>
|
||||
<td style="padding: 10px 12px; font-weight: 700; color: #dc2626;">{{ errorMessage }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>Votre commande a ete annulee. Vous pouvez retenter votre achat depuis la page de l'evenement.</p>
|
||||
|
||||
<p style="font-size: 12px; color: #9ca3af;">Si vous pensez qu'il s'agit d'une erreur, contactez votre banque ou reessayez avec un autre moyen de paiement.</p>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user