2 Commits

Author SHA1 Message Date
kbe
87ccebf229 feat(promotion code): Promotion code system done
I added the features for users to use promotion code
and for promoters to create on their events.
May be rewrite to discount code?
2025-09-29 15:25:52 +02:00
kbe
72d54e02ab feat: Update promotion code forms to display amounts in euros
- Changed discount amount input from cents to euros in new and edit forms
- Added decimal support with step="0.01" for precise euro amounts
- Updated form labels and help text to reflect euro display
- Added value conversion from stored cents to euros for editing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 14:46:39 +02:00
18 changed files with 762 additions and 81 deletions

View File

@@ -8,7 +8,6 @@
### Medium Priority ### Medium Priority
- [ ] feat: Promotion code on ticket
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display - [ ] feat: Promoter system with event creation, ticket types creation and metrics display
- [ ] feat: Multiple ticket types (early bird, VIP, general admission) - [ ] feat: Multiple ticket types (early bird, VIP, general admission)
- [ ] feat: Refund management system - [ ] feat: Refund management system

View File

@@ -0,0 +1,82 @@
class Promoter::PromotionCodesController < ApplicationController
before_action :authenticate_user!
before_action :set_event
before_action :set_promotion_code, only: [ :edit, :update, :destroy ]
# GET /promoter/events/:event_id/promotion_codes
# Display all promotion codes for a specific event
def index
@promotion_codes = @event.promotion_codes.includes(:user)
end
# GET /promoter/events/:event_id/promotion_codes/new
# Show form to create a new promotion code
def new
@promotion_code = @event.promotion_codes.new
end
# GET /promoter/events/:event_id/promotion_codes/:id/edit
# Show form to edit an existing promotion code
def edit
end
# POST /promoter/events/:event_id/promotion_codes
# Create a new promotion code for the event
def create
@promotion_code = @event.promotion_codes.new(promotion_code_params_with_conversion)
@promotion_code.user = current_user
if @promotion_code.save
redirect_to promoter_event_promotion_codes_path(@event), notice: "Promotion code was successfully created."
else
render :new, status: :unprocessable_entity
end
end
# PATCH/PUT /promoter/events/:event_id/promotion_codes/:id
# Update an existing promotion code
def update
if @promotion_code.update(promotion_code_params_with_conversion)
redirect_to promoter_event_promotion_codes_path(@event), notice: "Promotion code was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
# DELETE /promoter/events/:event_id/promotion_codes/:id
# Delete a promotion code
def destroy
@promotion_code.destroy
redirect_to promoter_event_promotion_codes_path(@event), notice: "Promotion code was successfully destroyed."
end
private
# Find the event based on the URL parameter
def set_event
@event = Event.find(params[:event_id])
end
# Find the promotion code based on the URL parameter
def set_promotion_code
@promotion_code = @event.promotion_codes.find(params[:id])
end
# Strong parameters for promotion code form (accepts euros for display)
def promotion_code_params
params.require(:promotion_code).permit(:code, :discount_amount_euros, :expires_at, :active, :usage_limit)
end
# Convert euros to cents for database storage
# The form displays euros for user convenience, but the database stores cents
def promotion_code_params_with_conversion
params = promotion_code_params
if params[:discount_amount_euros].present?
# Convert euros to cents (e.g., 20.50 -> 2050)
params[:discount_amount_cents] = (params[:discount_amount_euros].to_f * 100).to_i
params.delete(:discount_amount_euros) # Remove the temporary euro parameter
end
params
end
end

View File

@@ -21,6 +21,7 @@ class Event < ApplicationRecord
has_many :ticket_types has_many :ticket_types
has_many :tickets, through: :ticket_types has_many :tickets, through: :ticket_types
has_many :orders has_many :orders
has_many :promotion_codes
# === Callbacks === # === Callbacks ===
before_validation :geocode_address, if: :should_geocode_address? before_validation :geocode_address, if: :should_geocode_address?

View File

@@ -90,10 +90,34 @@ class Order < ApplicationRecord
end end
end end
# Calculate total from ticket prices only (platform fee deducted from promoter payout) # Calculate total from ticket prices minus promotion code discounts
def calculate_total! def calculate_total!
ticket_total = tickets.sum(:price_cents) ticket_total = tickets.sum(:price_cents)
update!(total_amount_cents: ticket_total) discount_total = promotion_codes.sum(:discount_amount_cents)
# Ensure total doesn't go below zero
final_total = [ticket_total - discount_total, 0].max
update!(total_amount_cents: final_total)
end
# Subtotal amount before discounts
def subtotal_amount_cents
tickets.sum(:price_cents)
end
# Subtotal amount in euros
def subtotal_amount_euros
subtotal_amount_cents / 100.0
end
# Total discount amount from all promotion codes
def discount_amount_cents
promotion_codes.sum(:discount_amount_cents)
end
# Discount amount in euros
def discount_amount_euros
discount_amount_cents / 100.0
end end
# Platform fee: €0.50 fixed + 1.5% of ticket price, per ticket # Platform fee: €0.50 fixed + 1.5% of ticket price, per ticket

View File

@@ -12,9 +12,28 @@ class PromotionCode < ApplicationRecord
before_create :increment_uses_count before_create :increment_uses_count
# Associations # Associations
belongs_to :user
belongs_to :event
has_many :order_promotion_codes has_many :order_promotion_codes
has_many :orders, through: :order_promotion_codes has_many :orders, through: :order_promotion_codes
# Instance methods
def discount_amount_euros
discount_amount_cents / 100.0
end
def active?
active && (expires_at.nil? || expires_at > Time.current)
end
def expired?
expires_at.present? && expires_at < Time.current
end
def can_be_used?
active? && (usage_limit.nil? || uses_count < usage_limit)
end
private private
def increment_uses_count def increment_uses_count

View File

@@ -1,30 +1,12 @@
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8"> <div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<nav class="inline-flex items-center gap-2 bg-white px-4 py-3 rounded-xl shadow-sm border border-gray-100 mb-8" aria-label="Breadcrumb"> <%= render 'components/breadcrumb', crumbs: [
<div class="inline-flex items-center text-sm font-medium"> { name: 'Accueil', path: root_path },
<%= link_to root_path, class: "text-gray-700 hover:text-purple-600 transition-colors" do %> { name: 'Événements', path: events_path },
<i data-lucide="home" class="w-4 h-4 mr-2"></i> { name: @order.event.name, path: event_path(@order.event.slug, @order.event) },
Accueil { name: "Commande ##{@order.id}", path: nil }
<% end %> ] %>
</div>
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
<div class="inline-flex items-center text-sm font-medium">
<%= link_to events_path, class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
Événements
<% end %>
</div>
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
<div class="inline-flex items-center text-sm font-medium">
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
<%= @order.event.name %>
<% end %>
</div>
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
<div class="text-sm font-medium text-purple-600">
Commande #<%= @order.id %>
</div>
</nav>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Order Summary --> <!-- Order Summary -->
@@ -77,8 +59,8 @@
</div> </div>
<!-- Order Items --> <!-- Order Items -->
<div class="space-y-4 mb-6"> <div class="space-y-4 mb-6 border-b border-gray-200 pb-6 mb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif de votre commande</h3> <h3 class="text-lg font-semibold text-gray-900 mb-4 ">Récapitulatif de votre commande</h3>
<% @tickets.each do |ticket| %> <% @tickets.each do |ticket| %>
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0"> <div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
@@ -99,12 +81,46 @@
<% end %> <% end %>
</div> </div>
<!-- Promotion Code Discount -->
<% if @order.promotion_codes.any? %>
<div class="space-y-2 mb-6 pb-6 border-b border-gray-200">
<% @order.promotion_codes.each do |promo_code| %>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-green-600">
<i data-lucide="tag" class="w-4 h-4 mr-1"></i>
Code: <%= promo_code.code %>
</span>
<span class="text-sm font-semibold text-green-600">-<%= promo_code.discount_amount_euros %>€</span>
</div>
<% end %>
</div>
<% end %>
<!-- Order Total --> <!-- Order Total -->
<div class=" pt-12"> <div class=" pt-12">
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center justify-between text-lg pt-2"> <!-- Subtotal -->
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Sous-total</span>
<span class="text-sm font-medium text-gray-600"><%= @order.subtotal_amount_euros %>€</span>
</div>
<!-- Discount -->
<% if @order.discount_amount_cents > 0 %>
<div class="flex items-center justify-between">
<span class="text-sm text-green-600">Réduction</span>
<span class="text-sm font-semibold text-green-600">-<%= @order.discount_amount_euros %>€</span>
</div>
<% end %>
<!-- Total -->
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
<span class="font-medium text-gray-900">Total</span> <span class="font-medium text-gray-900">Total</span>
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span> <% if @order.total_amount_cents == 0 %>
<span class="font-bold text-2xl text-green-600">GRATUIT</span>
<% else %>
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
<% end %>
</div> </div>
</div> </div>
<p class="text-xs text-gray-500 mt-2">TVA incluse</p> <p class="text-xs text-gray-500 mt-2">TVA incluse</p>
@@ -150,7 +166,11 @@
> >
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i> <i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
Payer <%= @order.total_amount_euros %> <% if @order.total_amount_cents == 0 %>
Confirmer la commande
<% else %>
Payer <%= @order.total_amount_euros %>€
<% end %>
</div> </div>
</button> </button>
@@ -251,7 +271,11 @@ const stripeResult = await stripe.redirectToCheckout({
button.innerHTML = ` button.innerHTML = `
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i> <i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
Payer <%= @order.total_amount_euros %> <% if @order.total_amount_cents == 0 %>
Confirmer la commande
<% else %>
Payer <%= @order.total_amount_euros %>€
<% end %>
</div> </div>
`; `;
alert('Erreur: ' + error.message); alert('Erreur: ' + error.message);

View File

@@ -123,13 +123,58 @@
<% end %> <% end %>
</div> </div>
<!-- Total --> <!-- Promotion Codes Applied -->
<div class="mt-6"> <% if @order.promotion_codes.any? %>
<div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<h3 class="text-lg font-semibold text-green-900 mb-3 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"/>
</svg>
Codes promotionnels appliqués
</h3>
<% @order.promotion_codes.each do |promo_code| %>
<div class="flex items-center justify-between py-2 px-3 bg-white rounded-lg border border-green-200 mb-2 last:mb-0">
<div class="flex items-center">
<span class="text-sm font-medium text-green-800">
<svg class="w-4 h-4 mr-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<%= promo_code.code %>
</span>
</div>
<span class="text-sm font-semibold text-green-700">-<%= promo_code.discount_amount_euros %>€</span>
</div>
<% end %>
</div>
<% end %>
<!-- Price Breakdown -->
<div class="mt-6 p-4 bg-gray-50 border border-gray-200 rounded-lg">
<h3 class="text-lg font-semibold text-gray-900 mb-3">Détail du paiement</h3>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center justify-between text-lg pt-2"> <!-- Subtotal -->
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Sous-total</span>
<span class="font-medium text-gray-700"><%= @order.subtotal_amount_euros %>€</span>
</div>
<!-- Discount -->
<% if @order.discount_amount_cents > 0 %>
<div class="flex items-center justify-between text-sm">
<span class="text-green-600">Réduction</span>
<span class="font-semibold text-green-600">-<%= @order.discount_amount_euros %>€</span>
</div>
<% end %>
<!-- Total -->
<div class="flex items-center justify-between pt-2 border-t border-gray-300">
<span class="font-medium text-gray-900">Total payé</span> <span class="font-medium text-gray-900">Total payé</span>
<span class="font-bold text-2xl text-green-600"> <span class="font-bold text-xl text-green-600">
<%= @order.total_amount_euros %> <% if @order.total_amount_cents == 0 %>
GRATUIT
<% else %>
<%= @order.total_amount_euros %>€
<% end %>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -94,14 +94,57 @@
<% end %> <% end %>
</div> </div>
<!-- Total --> <!-- Promotion Codes Applied -->
<% if @order.promotion_codes.any? %>
<div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<h3 class="text-lg font-semibold text-green-900 mb-3 flex items-center">
<i data-lucide="tag" class="w-5 h-5 mr-2"></i>
Codes promotionnels appliqués
</h3>
<% @order.promotion_codes.each do |promo_code| %>
<div class="flex items-center justify-between py-2 px-3 bg-white rounded-lg border border-green-200 mb-2 last:mb-0">
<div class="flex items-center">
<span class="text-sm font-medium text-green-800">
<i data-lucide="check-circle" class="w-4 h-4 mr-1 inline"></i>
<%= promo_code.code %>
</span>
</div>
<span class="text-sm font-semibold text-green-700">-<%= promo_code.discount_amount_euros %>€</span>
</div>
<% end %>
</div>
<% end %>
<!-- Price Breakdown -->
<div class="border-t border-gray-200 pt-6 mt-6"> <div class="border-t border-gray-200 pt-6 mt-6">
<div class="flex items-center justify-between text-lg pt-2"> <h3 class="text-lg font-semibold text-gray-900 mb-3">Détail du paiement</h3>
<div class="space-y-2">
<!-- Subtotal -->
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Sous-total</span>
<span class="font-medium text-gray-700"><%= @order.subtotal_amount_euros %>€</span>
</div>
<!-- Discount -->
<% if @order.discount_amount_cents > 0 %>
<div class="flex items-center justify-between text-sm">
<span class="text-green-600">Réduction</span>
<span class="font-semibold text-green-600">-<%= @order.discount_amount_euros %>€</span>
</div>
<% end %>
<!-- Total -->
<div class="flex items-center justify-between pt-2 border-t border-gray-300">
<span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span> <span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span>
<span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>"> <span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>">
<%= @order.total_amount_euros %> <% if @order.total_amount_cents == 0 %>
GRATUIT
<% else %>
<%= @order.total_amount_euros %>€
<% end %>
</span> </span>
</div> </div>
</div>
</div> </div>
<!-- View Invoice --> <!-- View Invoice -->

View File

@@ -7,11 +7,11 @@
{ name: 'Tableau de bord', path: dashboard_path } { name: 'Tableau de bord', path: dashboard_path }
] %> ] %>
<!-- Page Header --> <!-- Promoter Page Header -->
<div class="mb-8"> <div class="mb-8">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 class="text-3xl font-bold text-gray-900">Mon tableau de bord</h1> <h1 class="text-3xl font-bold text-gray-900">Mon tableau de bord promoteur</h1>
<p class="text-gray-600 mt-1">Gérez vos commandes et accédez à vos billets</p> <p class="text-gray-600 mt-1">Gérez vos commandes et accédez à vos billets</p>
</div> </div>
@@ -76,7 +76,9 @@
</div> </div>
</div> </div>
<!-- Brouillons -->
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-2xl p-6 border border-orange-200"> <div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-2xl p-6 border border-orange-200">
<%= link_to promoter_events_path do %>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-orange-600 text-sm font-medium">Brouillons</p> <p class="text-orange-600 text-sm font-medium">Brouillons</p>
@@ -86,7 +88,9 @@
<i data-lucide="edit-3" class="w-6 h-6 text-orange-700"></i> <i data-lucide="edit-3" class="w-6 h-6 text-orange-700"></i>
</div> </div>
</div> </div>
</div> <% end %>
</div><!-- /Brouillons -->
</div> </div>
<!-- Revenue Chart & Recent Events --> <!-- Revenue Chart & Recent Events -->
@@ -273,6 +277,16 @@
</div> </div>
<% end %> <% end %>
<!-- User Page Header -->
<div class="mb-8">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 class="text-3xl font-bold text-gray-900">Mon tableau de bord</h1>
<p class="text-gray-600 mt-1">Accédez à vos billets et évenements</p>
</div>
</div>
</div>
<!-- User's Orders Section --> <!-- User's Orders Section -->
<div class="bg-white rounded-2xl shadow-lg mb-8"> <div class="bg-white rounded-2xl shadow-lg mb-8">
<div class="border-b border-gray-100 p-4 sm:p-6"> <div class="border-b border-gray-100 p-4 sm:p-6">

View File

@@ -209,6 +209,42 @@
<!-- Sidebar --> <!-- Sidebar -->
<div class="space-y-6"> <div class="space-y-6">
<!-- Quick actions -->
<div class="bg-white rounded-2xl border border-gray-200 p-4 sm:p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Actions rapides</h3>
<div class="space-y-3">
<%= link_to promoter_event_ticket_types_path(@event), class: "w-full inline-flex items-center justify-center px-4 py-3 bg-purple-600 text-white font-medium text-sm rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
<i data-lucide="ticket" class="w-4 h-4 mr-2"></i>
Gérer les types de billets
<% end %>
<%= link_to promoter_event_promotion_codes_path(@event), class: "w-full inline-flex items-center justify-center px-4 py-3 bg-green-600 text-white font-medium text-sm rounded-lg hover:bg-green-700 transition-colors duration-200" do %>
<i data-lucide="tag" class="w-4 h-4 mr-2"></i>
Gérer les codes de réduction
<% end %>
<% if @event.sold_out? %>
<%= button_to mark_available_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-blue-50 text-blue-700 font-medium text-sm rounded-lg hover:bg-blue-100 transition-colors duration-200" do %>
<i data-lucide="refresh-ccw" class="w-4 h-4 mr-2"></i>
Marquer comme disponible
<% end %>
<% elsif @event.published? %>
<%= button_to mark_sold_out_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-gray-50 text-gray-700 font-medium text-sm rounded-lg hover:bg-gray-100 transition-colors duration-200" do %>
<i data-lucide="users" class="w-4 h-4 mr-2"></i>
Marquer comme complet
<% end %>
<% end %>
<hr class="border-gray-200">
<%= button_to promoter_event_path(@event), method: :delete,
data: { confirm: "Êtes-vous sûr de vouloir supprimer cet événement ? Cette action est irréversible." },
class: "w-full inline-flex items-center justify-center px-4 py-3 text-red-600 font-medium text-sm rounded-lg hover:bg-red-50 transition-colors duration-200" do %>
<i data-lucide="trash-2" class="w-4 h-4 mr-2"></i>
Supprimer l'événement
<% end %>
</div>
</div>
<!-- Event stats --> <!-- Event stats -->
<div class="bg-white rounded-2xl border border-gray-200 p-4 sm:p-6"> <div class="bg-white rounded-2xl border border-gray-200 p-4 sm:p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Statistiques</h3> <h3 class="text-lg font-semibold text-gray-900 mb-4">Statistiques</h3>
@@ -269,36 +305,6 @@
</div> </div>
</div> </div>
<!-- Quick actions -->
<div class="bg-white rounded-2xl border border-gray-200 p-4 sm:p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Actions rapides</h3>
<div class="space-y-3">
<%= link_to promoter_event_ticket_types_path(@event), class: "w-full inline-flex items-center justify-center px-4 py-3 bg-purple-600 text-white font-medium text-sm rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
<i data-lucide="ticket" class="w-4 h-4 mr-2"></i>
Gérer les types de billets
<% end %>
<% if @event.sold_out? %>
<%= button_to mark_available_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-blue-50 text-blue-700 font-medium text-sm rounded-lg hover:bg-blue-100 transition-colors duration-200" do %>
<i data-lucide="refresh-ccw" class="w-4 h-4 mr-2"></i>
Marquer comme disponible
<% end %>
<% elsif @event.published? %>
<%= button_to mark_sold_out_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-gray-50 text-gray-700 font-medium text-sm rounded-lg hover:bg-gray-100 transition-colors duration-200" do %>
<i data-lucide="users" class="w-4 h-4 mr-2"></i>
Marquer comme complet
<% end %>
<% end %>
<hr class="border-gray-200">
<%= button_to promoter_event_path(@event), method: :delete,
data: { confirm: "Êtes-vous sûr de vouloir supprimer cet événement ? Cette action est irréversible." },
class: "w-full inline-flex items-center justify-center px-4 py-3 text-red-600 font-medium text-sm rounded-lg hover:bg-red-50 transition-colors duration-200" do %>
<i data-lucide="trash-2" class="w-4 h-4 mr-2"></i>
Supprimer l'événement
<% end %>
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,109 @@
<% content_for(:title, "Modifier le code de réduction - #{@event.name}") %>
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<%= render 'components/breadcrumb', crumbs: [
{ name: 'Accueil', path: root_path },
{ name: 'Tableau de bord', path: dashboard_path },
{ name: 'Mes événements', path: promoter_events_path },
{ name: @event.name, path: promoter_event_path(@event) },
{ name: 'Codes de réduction', path: promoter_event_promotion_codes_path(@event) },
{ name: "Modifier #{@promotion_code.code}" }
] %>
<div class="max-w-2xl mx-auto">
<div class="mb-8">
<div class="flex items-center space-x-4 mb-4">
<%= link_to promoter_event_promotion_codes_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
<i data-lucide="arrow-left" class="w-5 h-5"></i>
<% end %>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Modifier le code de réduction</h1>
<p class="text-gray-600">
<code class="bg-gray-100 px-2 py-1 rounded text-sm"><%= @promotion_code.code %></code> pour <%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
</p>
</div>
</div>
</div>
<%= form_with(model: [@event, @promotion_code], url: promoter_event_promotion_code_path(@event, @promotion_code), method: :patch, local: true, class: "bg-white rounded-2xl border border-gray-200 p-6 sm:p-8") do |form| %>
<% if @promotion_code.errors.any? %>
<div class="bg-red-50 border border-red-200 rounded-2xl p-4 mb-6">
<div class="flex items-start">
<i data-lucide="alert-circle" class="w-5 h-5 text-red-400 mr-3 mt-0.5 flex-shrink-0"></i>
<div class="flex-1">
<h3 class="text-sm font-medium text-red-800 mb-2">
<%= pluralize(@promotion_code.errors.count, "erreur") %> ont empêché ce code de réduction d'être sauvegardé :
</h3>
<ul class="list-disc list-inside text-sm text-red-700">
<% @promotion_code.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</div>
</div>
<% end %>
<div class="space-y-6">
<div>
<%= form.label :code, "Code de réduction", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.text_field :code, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "Ex: SUMMER2024, BIENVENUE10, etc." %>
<p class="text-sm text-gray-500 mt-2">Ce code sera visible par les clients lors du paiement</p>
</div>
<div>
<%= form.label :discount_amount_euros, "Montant de la réduction (en euros)", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.number_field :discount_amount_euros, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "10", min: 0, step: "0.01", value: number_with_precision(@promotion_code.discount_amount_cents.to_f / 100, precision: 2) %>
<p class="text-sm text-gray-500 mt-2">Entrez le montant en euros (ex: 10, 5.50, 25)</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<%= form.label :expires_at, "Date d'expiration", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.datetime_local_field :expires_at, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors" %>
<p class="text-sm text-gray-500 mt-2">Laissez vide pour une durée illimitée</p>
</div>
<div>
<%= form.label :usage_limit, "Limite d'utilisation", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.number_field :usage_limit, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "Ex: 50", min: 1 %>
<p class="text-sm text-gray-500 mt-2">Laissez vide pour une utilisation illimitée</p>
</div>
</div>
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div class="flex items-center">
<%= form.check_box :active, class: "h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded" %>
<%= form.label :active, "Code actif", class: "ml-3 block text-sm font-medium text-gray-900" %>
</div>
<div class="text-sm text-gray-500">
Les clients peuvent utiliser ce code de réduction
</div>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-start">
<i data-lucide="info" class="w-5 h-5 text-blue-400 mr-3 mt-0.5 flex-shrink-0"></i>
<div>
<h4 class="text-sm font-medium text-blue-800 mb-1">Statut actuel</h4>
<div class="text-sm text-blue-700">
<p>Utilisations: <%= @promotion_code.uses_count %><%= " / #{@promotion_code.usage_limit}" if @promotion_code.usage_limit %></p>
<p>Commandes associées: <%= @promotion_code.orders.count %></p>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between pt-6 mt-8 border-t border-gray-200">
<%= link_to promoter_event_promotion_codes_path(@event), class: "inline-flex items-center px-6 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition-colors duration-200" do %>
<i data-lucide="x" class="w-4 h-4 mr-2"></i>
Annuler
<% end %>
<%= form.submit "Mettre à jour le code de réduction", class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" %>
</div>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,175 @@
<% content_for(:title, "Codes de réduction - #{@event.name}") %>
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<%= render 'components/breadcrumb', crumbs: [
{ name: 'Accueil', path: root_path },
{ name: 'Tableau de bord', path: dashboard_path },
{ name: 'Mes événements', path: promoter_events_path },
{ name: @event.name, path: promoter_event_path(@event) },
{ name: 'Codes de réduction' }
] %>
<div class="mb-8">
<div class="flex items-center space-x-4 mb-4">
<%= link_to promoter_event_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
<i data-lucide="arrow-left" class="w-5 h-5"></i>
<% end %>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Codes de réduction</h1>
<p class="text-gray-600">
<%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
</p>
</div>
<%= link_to new_promoter_event_promotion_code_path(@event), class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
Nouveau code
<% end %>
</div>
<!-- Event status info -->
<% if @event.draft? %>
<div class="bg-gray-50 border border-gray-200 rounded-2xl p-4 mb-6">
<div class="flex items-center">
<i data-lucide="info" class="w-5 h-5 text-gray-400 mr-3"></i>
<p class="text-sm text-gray-600">
Cet événement est en brouillon. Les codes de réduction ne seront actifs qu'une fois l'événement publié.
</p>
</div>
</div>
<% end %>
</div>
<% if @promotion_codes.any? %>
<div class="grid gap-6">
<% @promotion_codes.each do |promotion_code| %>
<div class="bg-white rounded-2xl border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200">
<div class="flex items-start justify-between">
<!-- Promotion code info -->
<div class="flex-1">
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">
<%= promotion_code.code %>
</h3>
<p class="text-gray-600 mb-3">Réduction de <%= number_to_currency(promotion_code.discount_amount_cents / 100.0, unit: "€") %></p>
</div>
<!-- Status badge -->
<div class="ml-4">
<% if promotion_code.active? && (promotion_code.expires_at.nil? || promotion_code.expires_at > Time.current) %>
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-green-100 text-green-800">
<i data-lucide="check-circle" class="w-4 h-4 mr-1"></i>
Actif
</span>
<% elsif promotion_code.expires_at && promotion_code.expires_at <= Time.current %>
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-red-100 text-red-800">
<i data-lucide="x-circle" class="w-4 h-4 mr-1"></i>
Expiré
</span>
<% else %>
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-gray-100 text-gray-800">
<i data-lucide="pause-circle" class="w-4 h-4 mr-1"></i>
Inactif
</span>
<% end %>
</div>
</div>
<!-- Promotion code details grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold text-green-600">
<%= number_to_currency(promotion_code.discount_amount_cents / 100.0, unit: "€") %>
</div>
<div class="text-sm text-gray-500">Réduction</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold text-gray-900">
<% if promotion_code.usage_limit %>
<%= promotion_code.usage_limit - promotion_code.uses_count %>
<% else %>
<% end %>
</div>
<div class="text-sm text-gray-500">Restants</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold text-gray-900">
<%= promotion_code.uses_count %>
</div>
<div class="text-sm text-gray-500">Utilisés</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold text-blue-600">
<%= promotion_code.orders.count %>
</div>
<div class="text-sm text-gray-500">Commandes</div>
</div>
</div>
<!-- Additional info -->
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-4">
<% if promotion_code.expires_at %>
<span class="flex items-center">
<i data-lucide="clock" class="w-4 h-4 mr-1"></i>
Expire le : <%= l(promotion_code.expires_at, format: :short) %>
</span>
<% else %>
<span class="flex items-center">
<i data-lucide="infinity" class="w-4 h-4 mr-1"></i>
Pas d'expiration
</span>
<% end %>
<span class="flex items-center">
<i data-lucide="user" class="w-4 h-4 mr-1"></i>
<% if promotion_code.user.first_name && promotion_code.user.last_name %>
Créé par : <%= promotion_code.user.first_name %> <%= promotion_code.user.last_name %>
<% else %>
Créé par : <%= promotion_code.user.email %>
<% end %>
</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
<div class="flex items-center space-x-3">
<%= link_to edit_promoter_event_promotion_code_path(@event, promotion_code), class: "text-gray-400 hover:text-blue-600 transition-colors", title: "Modifier" do %>
<i data-lucide="edit" class="w-5 h-5"></i>
<% end %>
<% if promotion_code.orders.empty? %>
<%= button_to promoter_event_promotion_code_path(@event, promotion_code), method: :delete,
data: { confirm: "Êtes-vous sûr de vouloir supprimer ce code de réduction ?" },
class: "text-gray-400 hover:text-red-600 transition-colors", title: "Supprimer" do %>
<i data-lucide="trash-2" class="w-5 h-5"></i>
<% end %>
<% end %>
</div>
<div class="text-sm text-gray-500">
Créé il y a <%= time_ago_in_words(promotion_code.created_at) %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="bg-white rounded-2xl border-2 border-dashed border-gray-300 p-12 text-center">
<div class="mx-auto h-24 w-24 rounded-full bg-gray-100 flex items-center justify-center mb-6">
<i data-lucide="tag" class="w-12 h-12 text-gray-400"></i>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Aucun code de réduction</h3>
<p class="text-gray-500 mb-6">Créez des codes de réduction pour offrir des remises spéciales à vos clients.</p>
<%= link_to new_promoter_event_promotion_code_path(@event), class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
Créer mon premier code de réduction
<% end %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,96 @@
<% content_for(:title, "Nouveau code de réduction - #{@event.name}") %>
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<%= render 'components/breadcrumb', crumbs: [
{ name: 'Accueil', path: root_path },
{ name: 'Tableau de bord', path: dashboard_path },
{ name: 'Mes événements', path: promoter_events_path },
{ name: @event.name, path: promoter_event_path(@event) },
{ name: 'Codes de réduction', path: promoter_event_promotion_codes_path(@event) },
{ name: 'Nouveau code' }
] %>
<div class="max-w-2xl mx-auto">
<div class="mb-8">
<div class="flex items-center space-x-4 mb-4">
<%= link_to promoter_event_promotion_codes_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
<i data-lucide="arrow-left" class="w-5 h-5"></i>
<% end %>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Nouveau code de réduction</h1>
<p class="text-gray-600">
Pour <%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
</p>
</div>
</div>
</div>
<%= form_with(model: [@event, @promotion_code], url: promoter_event_promotion_codes_path(@event), local: true, class: "bg-white rounded-2xl border border-gray-200 p-6 sm:p-8") do |form| %>
<% if @promotion_code.errors.any? %>
<div class="bg-red-50 border border-red-200 rounded-2xl p-4 mb-6">
<div class="flex items-start">
<i data-lucide="alert-circle" class="w-5 h-5 text-red-400 mr-3 mt-0.5 flex-shrink-0"></i>
<div class="flex-1">
<h3 class="text-sm font-medium text-red-800 mb-2">
<%= pluralize(@promotion_code.errors.count, "erreur") %> ont empêché ce code de réduction d'être sauvegardé :
</h3>
<ul class="list-disc list-inside text-sm text-red-700">
<% @promotion_code.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</div>
</div>
<% end %>
<div class="space-y-6">
<div>
<%= form.label :code, "Code de réduction", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.text_field :code, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "Ex: BIENVENUE10, VIP20" %>
<p class="text-sm text-gray-500 mt-2">Ce code sera à appliquer par le client lors du paiement.</p>
</div>
<div>
<%= form.label :discount_amount_euros, "Montant de la réduction (en euros)", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.number_field :discount_amount_euros, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "10", min: 0, step: "0.01", value: number_with_precision(@promotion_code.discount_amount_cents.to_f / 100, precision: 2) %>
<p class="text-sm text-gray-500 mt-2">Entrez le montant en euros</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<%= form.label :expires_at, "Date d'expiration", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.datetime_local_field :expires_at, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors" %>
<p class="text-sm text-gray-500 mt-2">Laissez vide pour une durée illimitée</p>
</div>
<div>
<%= form.label :usage_limit, "Limite d'utilisation", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.number_field :usage_limit, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "Ex: 50", min: 1 %>
<p class="text-sm text-gray-500 mt-2">Laissez vide pour une utilisation illimitée</p>
</div>
</div>
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div class="flex items-center">
<%= form.check_box :active, class: "h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded" %>
<%= form.label :active, "Code actif", class: "ml-3 block text-sm font-medium text-gray-900" %>
</div>
<div class="text-sm text-gray-500">
Les clients peuvent utiliser ce code de réduction
</div>
</div>
</div>
<div class="flex items-center justify-between pt-6 mt-8 border-t border-gray-200">
<%= link_to promoter_event_promotion_codes_path(@event), class: "inline-flex items-center px-6 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition-colors duration-200" do %>
<i data-lucide="x" class="w-4 h-4 mr-2"></i>
Annuler
<% end %>
<%= form.submit "Créer le code de réduction", class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" %>
</div>
<% end %>
</div>
</div>

View File

@@ -91,6 +91,16 @@ Rails.application.routes.draw do
post :duplicate post :duplicate
end end
end end
# Nested promotion codes routes
resources :promotion_codes, except: [ :show ]
end
end
# === Promotion Codes Routes ===
resources :promotion_codes, only: [ :index ] do
member do
post :apply
end end
end end

View File

@@ -1,14 +1,15 @@
class CreateOrders < ActiveRecord::Migration[8.0] class CreateOrders < ActiveRecord::Migration[8.0]
def change def change
create_table :orders do |t| create_table :orders do |t|
t.references :user, null: false, foreign_key: false
t.references :event, null: false, foreign_key: false
t.string :status, null: false, default: "draft" t.string :status, null: false, default: "draft"
t.integer :total_amount_cents, null: false, default: 0 t.integer :total_amount_cents, null: false, default: 0
t.integer :payment_attempts, null: false, default: 0 t.integer :payment_attempts, null: false, default: 0
t.timestamp :expires_at t.timestamp :expires_at
t.timestamp :last_payment_attempt_at t.timestamp :last_payment_attempt_at
t.references :user, null: false, foreign_key: false
t.references :event, null: false, foreign_key: false
t.timestamps t.timestamps
end end

View File

@@ -7,8 +7,12 @@ class CreatePromotionCodes < ActiveRecord::Migration[8.0]
t.boolean :active, default: true, null: false t.boolean :active, default: true, null: false
t.integer :usage_limit, default: nil t.integer :usage_limit, default: nil
t.integer :uses_count, default: 0, null: false t.integer :uses_count, default: 0, null: false
t.datetime :created_at, null: false
t.datetime :updated_at, null: false # Reference user(promoter) who has created the promotion code
t.references :user, null: false, foreign_key: true
t.references :event, null: false, foreign_key: true
t.timestamps
end end
# Unique index for code # Unique index for code

10
db/schema.rb generated
View File

@@ -44,13 +44,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_181311) do
end end
create_table "orders", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t| create_table "orders", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
t.bigint "user_id", null: false
t.bigint "event_id", null: false
t.string "status", default: "draft", null: false t.string "status", default: "draft", null: false
t.integer "total_amount_cents", default: 0, null: false t.integer "total_amount_cents", default: 0, null: false
t.integer "payment_attempts", default: 0, null: false t.integer "payment_attempts", default: 0, null: false
t.timestamp "expires_at" t.timestamp "expires_at"
t.timestamp "last_payment_attempt_at" t.timestamp "last_payment_attempt_at"
t.bigint "user_id", null: false
t.bigint "event_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["event_id", "status"], name: "idx_orders_event_status" t.index ["event_id", "status"], name: "idx_orders_event_status"
@@ -67,9 +67,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_181311) do
t.boolean "active", default: true, null: false t.boolean "active", default: true, null: false
t.integer "usage_limit" t.integer "usage_limit"
t.integer "uses_count", default: 0, null: false t.integer "uses_count", default: 0, null: false
t.bigint "user_id", null: false
t.bigint "event_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["code"], name: "index_promotion_codes_on_code", unique: true t.index ["code"], name: "index_promotion_codes_on_code", unique: true
t.index ["event_id"], name: "index_promotion_codes_on_event_id"
t.index ["user_id"], name: "index_promotion_codes_on_user_id"
end end
create_table "ticket_types", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t| create_table "ticket_types", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
@@ -128,4 +132,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_181311) do
add_foreign_key "order_promotion_codes", "orders" add_foreign_key "order_promotion_codes", "orders"
add_foreign_key "order_promotion_codes", "promotion_codes" add_foreign_key "order_promotion_codes", "promotion_codes"
add_foreign_key "promotion_codes", "events"
add_foreign_key "promotion_codes", "users"
end end

View File

@@ -280,3 +280,26 @@ TicketType.find_or_create_by!(event: belle_epoque_october_event, name: "Entry 10
end end
puts "Created 3 additional events from Bizouk with ticket types" puts "Created 3 additional events from Bizouk with ticket types"
# Create promotion codes for events
# Promotion code for belle_epoque_event
PromotionCode.find_or_create_by!(code: "BELLE10") do |pc|
pc.discount_amount_cents = 1000 # 10€ discount
pc.expires_at = belle_epoque_event.start_time + 1.day
pc.active = true
pc.usage_limit = 20
pc.user = promoter
pc.event = belle_epoque_october_event
end
# Promotion code for belle_epoque_october_event
PromotionCode.find_or_create_by!(code: "OCTOBRE5") do |pc|
pc.discount_amount_cents = 500 # 5€ discount
pc.expires_at = belle_epoque_october_event.start_time + 1.day
pc.active = true
pc.usage_limit = 30
pc.user = promoter
pc.event = belle_epoque_october_event
end
puts "Created promotion codes for events"