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?
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
|
||||
### Medium Priority
|
||||
|
||||
- [ ] feat: Promotion code on ticket
|
||||
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display
|
||||
- [ ] feat: Multiple ticket types (early bird, VIP, general admission)
|
||||
- [ ] feat: Refund management system
|
||||
|
||||
82
app/controllers/promoter/promotion_codes_controller.rb
Normal file
82
app/controllers/promoter/promotion_codes_controller.rb
Normal 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
|
||||
@@ -21,6 +21,7 @@ class Event < ApplicationRecord
|
||||
has_many :ticket_types
|
||||
has_many :tickets, through: :ticket_types
|
||||
has_many :orders
|
||||
has_many :promotion_codes
|
||||
|
||||
# === Callbacks ===
|
||||
before_validation :geocode_address, if: :should_geocode_address?
|
||||
|
||||
@@ -90,10 +90,34 @@ class Order < ApplicationRecord
|
||||
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!
|
||||
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
|
||||
|
||||
# Platform fee: €0.50 fixed + 1.5% of ticket price, per ticket
|
||||
|
||||
@@ -12,9 +12,28 @@ class PromotionCode < ApplicationRecord
|
||||
before_create :increment_uses_count
|
||||
|
||||
# Associations
|
||||
belongs_to :user
|
||||
belongs_to :event
|
||||
has_many :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
|
||||
|
||||
def increment_uses_count
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
<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 -->
|
||||
<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">
|
||||
<div class="inline-flex items-center text-sm font-medium">
|
||||
<%= link_to root_path, class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
|
||||
<i data-lucide="home" class="w-4 h-4 mr-2"></i>
|
||||
Accueil
|
||||
<% 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>
|
||||
<%= render 'components/breadcrumb', crumbs: [
|
||||
{ name: 'Accueil', path: root_path },
|
||||
{ name: 'Événements', path: events_path },
|
||||
{ name: @order.event.name, path: event_path(@order.event.slug, @order.event) },
|
||||
{ name: "Commande ##{@order.id}", path: nil }
|
||||
] %>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Order Summary -->
|
||||
@@ -77,8 +59,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif de votre commande</h3>
|
||||
<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>
|
||||
|
||||
<% @tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||
@@ -99,12 +81,46 @@
|
||||
<% end %>
|
||||
</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 -->
|
||||
<div class=" pt-12">
|
||||
<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-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>
|
||||
<p class="text-xs text-gray-500 mt-2">TVA incluse</p>
|
||||
@@ -150,7 +166,11 @@
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<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>
|
||||
</button>
|
||||
|
||||
@@ -251,7 +271,11 @@ const stripeResult = await stripe.redirectToCheckout({
|
||||
button.innerHTML = `
|
||||
<div class="flex items-center justify-center">
|
||||
<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>
|
||||
`;
|
||||
alert('Erreur: ' + error.message);
|
||||
|
||||
@@ -123,13 +123,58 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Total -->
|
||||
<div class="mt-6">
|
||||
<!-- 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">
|
||||
<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="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-bold text-2xl text-green-600">
|
||||
<%= @order.total_amount_euros %>€
|
||||
<span class="font-bold text-xl text-green-600">
|
||||
<% if @order.total_amount_cents == 0 %>
|
||||
GRATUIT
|
||||
<% else %>
|
||||
<%= @order.total_amount_euros %>€
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,14 +94,57 @@
|
||||
<% end %>
|
||||
</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="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-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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Invoice -->
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
{ name: 'Tableau de bord', path: dashboard_path }
|
||||
] %>
|
||||
|
||||
<!-- Page Header -->
|
||||
<!-- Promoter 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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brouillons -->
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div><!-- /Brouillons -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Revenue Chart & Recent Events -->
|
||||
@@ -273,6 +277,16 @@
|
||||
</div>
|
||||
<% 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 -->
|
||||
<div class="bg-white rounded-2xl shadow-lg mb-8">
|
||||
<div class="border-b border-gray-100 p-4 sm:p-6">
|
||||
|
||||
@@ -209,6 +209,42 @@
|
||||
|
||||
<!-- Sidebar -->
|
||||
<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 -->
|
||||
<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>
|
||||
@@ -269,36 +305,6 @@
|
||||
</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>
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :discount_amount_cents, "Montant de la réduction (en euros)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :discount_amount_cents, 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) %>
|
||||
<%= 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>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">
|
||||
<%= link_to promotion_code.code, promoter_event_promotion_code_path(@event, promotion_code), class: "hover:text-purple-600 transition-colors" %>
|
||||
<%= 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>
|
||||
|
||||
@@ -49,14 +49,14 @@
|
||||
<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>
|
||||
<%= 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_cents, "Montant de la réduction (en euros)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :discount_amount_cents, 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>
|
||||
<%= 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">
|
||||
@@ -93,4 +93,4 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
<% content_for(:title, "Code de réduction #{@promotion_code.code} - #{@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: @promotion_code.code }
|
||||
] %>
|
||||
|
||||
<div class="max-w-4xl 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">Détails du 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>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
<!-- Main content -->
|
||||
<div class="lg:col-span-2 space-y-6 lg:space-y-8">
|
||||
<!-- Code details -->
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Informations du code</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="text-sm text-gray-500 mb-2">Code</div>
|
||||
<div class="text-lg font-semibold bg-gray-100 px-3 py-2 rounded-lg font-mono">
|
||||
<%= @promotion_code.code %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-sm text-gray-500 mb-2">Statut</div>
|
||||
<div>
|
||||
<% 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>
|
||||
|
||||
<div>
|
||||
<div class="text-sm text-gray-500 mb-2">Montant de la réduction</div>
|
||||
<div class="text-lg font-semibold text-green-600">
|
||||
<%= number_to_currency(@promotion_code.discount_amount_cents / 100.0, unit: "€") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-sm text-gray-500 mb-2">Événement</div>
|
||||
<div class="text-lg font-semibold">
|
||||
<%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage stats -->
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Statistiques d'utilisation</h3>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="text-center p-4 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">Utilisations</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div class="text-2xl font-bold text-blue-600">
|
||||
<% 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-4 bg-gray-50 rounded-lg">
|
||||
<div class="text-2xl font-bold text-purple-600">
|
||||
<%= @promotion_code.orders.count %>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">Commandes</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div class="text-2xl font-bold text-green-600">
|
||||
<%= number_to_currency(@promotion_code.orders.sum(:total_amount_cents) / 100.0, unit: "€") %>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">Montant total</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders using this code -->
|
||||
<% if @promotion_code.orders.any? %>
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Commandes utilisant ce code</h3>
|
||||
<div class="space-y-3">
|
||||
<% @promotion_code.orders.includes(:user).order(created_at: :desc).limit(5).each do |order| %>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">Commande #<%= order.id %></div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= order.user.email %> • <%= l(order.created_at, format: :short) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="font-medium text-gray-900"><%= number_to_currency(order.total_amount_cents / 100.0, unit: "€") %></div>
|
||||
<div class="text-sm text-gray-500"><%= order.status %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @promotion_code.orders.count > 5 %>
|
||||
<div class="text-center pt-2">
|
||||
<span class="text-sm text-gray-500">
|
||||
Et <%= @promotion_code.orders.count - 5 %> autres commandes...
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Additional info -->
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Informations supplémentaires</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Créé par</span>
|
||||
<p class="text-sm font-medium"><%= @promotion_code.user.email %></p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Créé le</span>
|
||||
<p class="text-sm"><%= l(@promotion_code.created_at, format: :long) %></p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Modifié le</span>
|
||||
<p class="text-sm"><%= l(@promotion_code.updated_at, format: :long) %></p>
|
||||
</div>
|
||||
<% if @promotion_code.expires_at %>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Date d'expiration</span>
|
||||
<p class="text-sm"><%= l(@promotion_code.expires_at, format: :long) %></p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Date d'expiration</span>
|
||||
<p class="text-sm">Jamais</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="bg-white rounded-2xl border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Actions</h3>
|
||||
<div class="space-y-3">
|
||||
<%= link_to edit_promoter_event_promotion_code_path(@event, @promotion_code), class: "w-full inline-flex items-center justify-center px-4 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<i data-lucide="edit" class="w-4 h-4 mr-2"></i>
|
||||
Modifier
|
||||
<% 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: "w-full inline-flex items-center justify-center px-4 py-3 text-red-600 font-medium 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
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to promoter_event_promotion_codes_path(@event), class: "w-full inline-flex items-center justify-center px-4 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="arrow-left" class="w-4 h-4 mr-2"></i>
|
||||
Retour à la liste
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,6 +91,16 @@ Rails.application.routes.draw do
|
||||
post :duplicate
|
||||
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
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
class CreateOrders < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
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.integer :total_amount_cents, null: false, default: 0
|
||||
t.integer :payment_attempts, null: false, default: 0
|
||||
t.timestamp :expires_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
|
||||
end
|
||||
|
||||
|
||||
@@ -7,8 +7,12 @@ class CreatePromotionCodes < ActiveRecord::Migration[8.0]
|
||||
t.boolean :active, default: true, null: false
|
||||
t.integer :usage_limit, default: nil
|
||||
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
|
||||
|
||||
# Unique index for code
|
||||
|
||||
10
db/schema.rb
generated
10
db/schema.rb
generated
@@ -44,13 +44,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_181311) do
|
||||
end
|
||||
|
||||
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.integer "total_amount_cents", default: 0, null: false
|
||||
t.integer "payment_attempts", default: 0, null: false
|
||||
t.timestamp "expires_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 "updated_at", null: false
|
||||
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.integer "usage_limit"
|
||||
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 "updated_at", null: false
|
||||
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
|
||||
|
||||
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", "promotion_codes"
|
||||
add_foreign_key "promotion_codes", "events"
|
||||
add_foreign_key "promotion_codes", "users"
|
||||
end
|
||||
|
||||
23
db/seeds.rb
23
db/seeds.rb
@@ -280,3 +280,26 @@ TicketType.find_or_create_by!(event: belle_epoque_october_event, name: "Entry 10
|
||||
end
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user