Files
e-ticket/templates/account/add_billet.html.twig
Serreau Jovann 61200adc74 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>
2026-03-23 00:12:30 +01:00

81 lines
4.8 KiB
Twig

{% extends 'base.html.twig' %}
{% block title %}Ajouter un billet - {{ category.name }} - E-Ticket{% endblock %}
{% block body %}
<div class="page-container">
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'categories'}) }}" class="inline-flex items-center gap-2 text-sm font-black uppercase tracking-widest text-gray-500 hover:text-gray-900 transition-colors mb-8">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
Retour aux categories
</a>
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page">Ajouter un billet</h1>
<p class="font-bold text-gray-600 italic mb-8">{{ event.title }}{{ category.name }}</p>
{% for message in app.flashes('error') %}
<div class="flash-error"><p class="font-black text-sm">{{ message }}</p></div>
{% endfor %}
<div class="card-brutal">
<form method="post" action="{{ path('app_account_event_add_billet', {id: event.id, categoryId: category.id}) }}" enctype="multipart/form-data" class="form-col">
<div>
<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>
{% 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>
<div>
<label for="billet_name" class="text-xs font-black uppercase tracking-widest form-label">Nom du billet</label>
<input type="text" id="billet_name" name="name" required class="form-input focus:border-indigo-600" placeholder="Ex: Entree adulte, Pass VIP...">
</div>
<div>
<label for="billet_price" class="text-xs font-black uppercase tracking-widest form-label">Prix HT (&euro;)</label>
<input type="number" id="billet_price" name="price_ht" required min="0" step="0.01" class="form-input focus:border-indigo-600" placeholder="Ex: 15.00">
</div>
<div>
<label for="billet_quantity" class="text-xs font-black uppercase tracking-widest form-label">Quantite disponible (vide = illimite)</label>
<input type="number" id="billet_quantity" name="quantity" min="1" class="form-input focus:border-indigo-600" placeholder="Illimite">
</div>
{% include 'account/_billet_commission.html.twig' %}
<div>
<label for="billet_description" class="text-xs font-black uppercase tracking-widest form-label">Description</label>
<textarea id="billet_description" name="description" rows="3" class="form-input focus:border-indigo-600" placeholder="Description optionnelle du billet..."></textarea>
</div>
<div>
<label for="billet_picture" class="text-xs font-black uppercase tracking-widest form-label">Image</label>
<input type="file" id="billet_picture" name="picture" accept="image/png, image/jpeg, image/webp, image/gif" class="form-file">
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="billet_generated" name="is_generated_billet" value="1" class="w-5 h-5 border-2 border-gray-900 cursor-pointer" checked>
<label for="billet_generated" class="text-sm font-black uppercase tracking-widest cursor-pointer">Generer un billet PDF</label>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="billet_exit" name="has_defined_exit" value="1" class="w-5 h-5 border-2 border-gray-900 cursor-pointer">
<label for="billet_exit" class="text-sm font-black uppercase tracking-widest cursor-pointer">Sortie definitive</label>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="billet_not_buyable" name="not_buyable" value="1" class="w-5 h-5 border-2 border-gray-900 cursor-pointer">
<label for="billet_not_buyable" class="text-sm font-black uppercase tracking-widest cursor-pointer">Non achetable</label>
</div>
<div>
<button type="submit" class="btn-brutal font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
Ajouter le billet
</button>
</div>
</form>
</div>
</div>
{% endblock %}