refactor: prepare for free ticket
This commit is contained in:
@@ -126,6 +126,15 @@ class OrdersController < ApplicationController
|
|||||||
@total_amount = @order.total_amount_cents
|
@total_amount = @order.total_amount_cents
|
||||||
@expiring_soon = @order.expiring_soon?
|
@expiring_soon = @order.expiring_soon?
|
||||||
|
|
||||||
|
# For free orders, automatically mark as paid and redirect to success
|
||||||
|
if @order.free?
|
||||||
|
@order.mark_as_paid!
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
session.delete(:ticket_names)
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
return redirect_to order_path(@order), notice: "Vos billets gratuits ont été confirmés !"
|
||||||
|
end
|
||||||
|
|
||||||
# Create Stripe checkout session if Stripe is configured
|
# Create Stripe checkout session if Stripe is configured
|
||||||
if Rails.application.config.stripe[:secret_key].present?
|
if Rails.application.config.stripe[:secret_key].present?
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
module TicketsHelper
|
module TicketsHelper
|
||||||
|
def format_ticket_price(price_cents)
|
||||||
|
if price_cents == 0
|
||||||
|
"Gratuit"
|
||||||
|
else
|
||||||
|
number_to_currency(price_cents / 100.0, unit: "€")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ class Order < ApplicationRecord
|
|||||||
promoter_payout_cents / 100.0
|
promoter_payout_cents / 100.0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check if order contains only free tickets
|
||||||
|
def free?
|
||||||
|
total_amount_cents == 0
|
||||||
|
end
|
||||||
|
|
||||||
# Create Stripe invoice for accounting records
|
# Create Stripe invoice for accounting records
|
||||||
#
|
#
|
||||||
# This method creates a post-payment invoice in Stripe for accounting purposes
|
# This method creates a post-payment invoice in Stripe for accounting purposes
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Ticket < ApplicationRecord
|
|||||||
validates :qr_code, presence: true, uniqueness: true
|
validates :qr_code, presence: true, uniqueness: true
|
||||||
validates :order_id, presence: true
|
validates :order_id, presence: true
|
||||||
validates :ticket_type_id, presence: true
|
validates :ticket_type_id, presence: true
|
||||||
validates :price_cents, presence: true, numericality: { greater_than: 0 }
|
validates :price_cents, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||||
validates :status, presence: true, inclusion: { in: %w[draft active used expired refunded] }
|
validates :status, presence: true, inclusion: { in: %w[draft active used expired refunded] }
|
||||||
validates :first_name, presence: true
|
validates :first_name, presence: true
|
||||||
validates :last_name, presence: true
|
validates :last_name, presence: true
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class TicketType < ApplicationRecord
|
|||||||
# Validations
|
# Validations
|
||||||
validates :name, presence: true, length: { minimum: 3, maximum: 50 }
|
validates :name, presence: true, length: { minimum: 3, maximum: 50 }
|
||||||
validates :description, presence: true, length: { minimum: 10, maximum: 500 }
|
validates :description, presence: true, length: { minimum: 10, maximum: 500 }
|
||||||
validates :price_cents, presence: true, numericality: { greater_than: 0 }
|
validates :price_cents, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||||
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
||||||
validates :sale_start_at, presence: true
|
validates :sale_start_at, presence: true
|
||||||
validates :sale_end_at, presence: true
|
validates :sale_end_at, presence: true
|
||||||
@@ -48,6 +48,10 @@ class TicketType < ApplicationRecord
|
|||||||
[ quantity - tickets.count, 0 ].max
|
[ quantity - tickets.count, 0 ].max
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def free?
|
||||||
|
price_cents == 0
|
||||||
|
end
|
||||||
|
|
||||||
def sales_status
|
def sales_status
|
||||||
return :draft if sale_start_at.nil? || sale_end_at.nil?
|
return :draft if sale_start_at.nil? || sale_end_at.nil?
|
||||||
return :expired if sale_end_at < Time.current
|
return :expired if sale_end_at < Time.current
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-xl font-bold text-purple-700 <%= "text-gray-400" if sold_out %>">
|
<p class="text-xl font-bold text-purple-700 <%= "text-gray-400" if sold_out %>">
|
||||||
<%= number_to_currency(price_cents / 100.0, unit: "€") %>
|
<%= format_ticket_price(price_cents) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<%= form.number_field :price_euros,
|
<%= form.number_field :price_euros,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
min: 0.01,
|
min: 0,
|
||||||
class: "w-full px-4 py-2 pl-8 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
class: "w-full px-4 py-2 pl-8 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
||||||
data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
|
data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
|
||||||
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
||||||
@@ -91,6 +91,8 @@
|
|||||||
<i data-lucide="alert-triangle" class="w-4 h-4 inline mr-1"></i>
|
<i data-lucide="alert-triangle" class="w-4 h-4 inline mr-1"></i>
|
||||||
Modifier le prix n'affectera pas les billets déjà vendus
|
Modifier le prix n'affectera pas les billets déjà vendus
|
||||||
</p>
|
</p>
|
||||||
|
<% else %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Prix unitaire du billet (0€ pour un billet gratuit)</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -68,12 +68,12 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<%= form.number_field :price_euros,
|
<%= form.number_field :price_euros,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
min: 0.01,
|
min: 0,
|
||||||
class: "w-full px-4 py-2 pl-8 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
class: "w-full px-4 py-2 pl-8 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
||||||
data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
|
data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
|
||||||
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-sm text-gray-500">Prix unitaire du billet</p>
|
<p class="mt-1 text-sm text-gray-500">Prix unitaire du billet (0€ pour un billet gratuit)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-sm font-medium text-gray-900">
|
<p class="text-sm font-medium text-gray-900">
|
||||||
<%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
|
<%= format_ticket_price(ticket.price_cents) %>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-500">
|
<p class="text-xs text-gray-500">
|
||||||
<%= ticket.created_at.strftime("%d/%m/%Y") %>
|
<%= ticket.created_at.strftime("%d/%m/%Y") %>
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
<div class="text-3xl font-bold text-purple-600">
|
<div class="text-3xl font-bold text-purple-600">
|
||||||
<%= number_to_currency(@ticket_type.price_euros, unit: "€") %>
|
<%= format_ticket_price(@ticket_type.price_cents) %>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-500">Prix unitaire</div>
|
<div class="text-sm text-gray-500">Prix unitaire</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<p style="margin: 0; font-weight: bold; color: #212529;"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></p>
|
<p style="margin: 0; font-weight: bold; color: #212529;"><%= format_ticket_price(ticket.price_cents) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Prix</p>
|
<p style="margin: 0; color: #6c757d; font-size: 14px;">Prix</p>
|
||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
|
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= format_ticket_price(@ticket.price_cents) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ DÉTAILS DE VOTRE COMMANDE
|
|||||||
Événement : <%= @event.name %>
|
Événement : <%= @event.name %>
|
||||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||||
Nombre de billets : <%= @tickets.count %>
|
Nombre de billets : <%= @tickets.count %>
|
||||||
Total : <%= number_to_currency(@order.total_amount_euros, unit: "€") %>
|
Total : <%= @order.free? ? "Gratuit" : number_to_currency(@order.total_amount_euros, unit: "€") %>
|
||||||
|
|
||||||
BILLETS INCLUS :
|
BILLETS INCLUS :
|
||||||
<% @tickets.each_with_index do |ticket, index| %>
|
<% @tickets.each_with_index do |ticket, index| %>
|
||||||
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> - <%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
|
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> - <%= format_ticket_price(ticket.price_cents) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
Vos billets sont attachés à cet email en format PDF. Présentez-les à l'entrée de l'événement pour y accéder.
|
Vos billets sont attachés à cet email en format PDF. Présentez-les à l'entrée de l'événement pour y accéder.
|
||||||
@@ -32,7 +32,7 @@ DÉTAILS DE VOTRE BILLET
|
|||||||
Événement : <%= @event.name %>
|
Événement : <%= @event.name %>
|
||||||
Type de billet : <%= @ticket.ticket_type.name %>
|
Type de billet : <%= @ticket.ticket_type.name %>
|
||||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||||
Prix : <%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>
|
Prix : <%= format_ticket_price(@ticket.price_cents) %>
|
||||||
|
|
||||||
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
|
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Prix</label>
|
<label class="block text-sm font-medium text-gray-500 mb-1">Prix</label>
|
||||||
<p class="text-xl font-bold text-gray-900">
|
<p class="text-xl font-bold text-gray-900">
|
||||||
<%= number_to_currency(@ticket.price_euros, unit: "€", separator: ",", delimiter: " ", format: "%n %u") %>
|
<%= format_ticket_price(@ticket.price_cents) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class OrdersControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal "draft", new_order.status
|
assert_equal "draft", new_order.status
|
||||||
assert_equal @user, new_order.user
|
assert_equal @user, new_order.user
|
||||||
assert_equal @event, new_order.event
|
assert_equal @event, new_order.event
|
||||||
assert_equal @ticket_type.price_cents + 100, new_order.total_amount_cents # includes 1€ service fee
|
assert_equal @ticket_type.price_cents, new_order.total_amount_cents # Service fee deducted from promoter payout, not added to customer
|
||||||
|
|
||||||
assert_redirected_to checkout_order_path(new_order)
|
assert_redirected_to checkout_order_path(new_order)
|
||||||
assert_equal new_order.id, session[:draft_order_id]
|
assert_equal new_order.id, session[:draft_order_id]
|
||||||
|
|||||||
@@ -603,4 +603,22 @@ class OrderTest < ActiveSupport::TestCase
|
|||||||
result = order.stripe_invoice_pdf_url
|
result = order.stripe_invoice_pdf_url
|
||||||
assert_nil result
|
assert_nil result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "free? should return true for zero amount orders" do
|
||||||
|
free_order = Order.create!(
|
||||||
|
user: @user, event: @event, total_amount_cents: 0,
|
||||||
|
status: "draft", payment_attempts: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert free_order.free?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "free? should return false for non-zero amount orders" do
|
||||||
|
paid_order = Order.create!(
|
||||||
|
user: @user, event: @event, total_amount_cents: 1000,
|
||||||
|
status: "draft", payment_attempts: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_not paid_order.free?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -244,4 +244,38 @@ class TicketTypeTest < ActiveSupport::TestCase
|
|||||||
)
|
)
|
||||||
assert_not ticket_type.save
|
assert_not ticket_type.save
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should allow free tickets with zero price" do
|
||||||
|
user = User.create!(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123",
|
||||||
|
password_confirmation: "password123"
|
||||||
|
)
|
||||||
|
|
||||||
|
event = Event.create!(
|
||||||
|
name: "Test Event",
|
||||||
|
slug: "test-event",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
latitude: 48.8566,
|
||||||
|
longitude: 2.3522,
|
||||||
|
venue_name: "Test Venue",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
user: user
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket_type = TicketType.new(
|
||||||
|
name: "Free Ticket",
|
||||||
|
description: "Valid description for the free ticket type",
|
||||||
|
price_cents: 0,
|
||||||
|
quantity: 50,
|
||||||
|
sale_start_at: Time.current,
|
||||||
|
sale_end_at: Time.current + 1.day,
|
||||||
|
event: event
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ticket_type.save
|
||||||
|
assert ticket_type.free?
|
||||||
|
assert_equal 0, ticket_type.price_cents
|
||||||
|
assert_equal 0.0, ticket_type.price_euros
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user