From 9c56b2e1e5c6e4ba9881916bb4149b9800158556 Mon Sep 17 00:00:00 2001 From: kbe Date: Tue, 16 Sep 2025 16:36:39 +0200 Subject: [PATCH] refactor: prepare for free ticket --- app/controllers/orders_controller.rb | 9 +++++ app/helpers/tickets_helper.rb | 7 ++++ app/models/order.rb | 5 +++ app/models/ticket.rb | 2 +- app/models/ticket_type.rb | 6 +++- app/views/components/_ticket_card.html.erb | 2 +- app/views/promoter/ticket_types/edit.html.erb | 4 ++- app/views/promoter/ticket_types/new.html.erb | 8 ++--- app/views/promoter/ticket_types/show.html.erb | 4 +-- .../purchase_confirmation.html.erb | 4 +-- .../purchase_confirmation.text.erb | 6 ++-- app/views/tickets/show.html.erb | 2 +- test/controllers/orders_controller_test.rb | 2 +- test/models/order_test.rb | 18 ++++++++++ test/models/ticket_type_test.rb | 34 +++++++++++++++++++ 15 files changed, 96 insertions(+), 17 deletions(-) diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb index 7ce04a8..9456dd9 100644 --- a/app/controllers/orders_controller.rb +++ b/app/controllers/orders_controller.rb @@ -126,6 +126,15 @@ class OrdersController < ApplicationController @total_amount = @order.total_amount_cents @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 if Rails.application.config.stripe[:secret_key].present? begin diff --git a/app/helpers/tickets_helper.rb b/app/helpers/tickets_helper.rb index 4722254..c661310 100644 --- a/app/helpers/tickets_helper.rb +++ b/app/helpers/tickets_helper.rb @@ -1,2 +1,9 @@ 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 diff --git a/app/models/order.rb b/app/models/order.rb index 666a6a3..3c84bc7 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -116,6 +116,11 @@ class Order < ApplicationRecord promoter_payout_cents / 100.0 end + # Check if order contains only free tickets + def free? + total_amount_cents == 0 + end + # Create Stripe invoice for accounting records # # This method creates a post-payment invoice in Stripe for accounting purposes diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 51b0ae1..94b2508 100755 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -9,7 +9,7 @@ class Ticket < ApplicationRecord validates :qr_code, presence: true, uniqueness: true validates :order_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 :first_name, presence: true validates :last_name, presence: true diff --git a/app/models/ticket_type.rb b/app/models/ticket_type.rb index f06cfd5..1274758 100755 --- a/app/models/ticket_type.rb +++ b/app/models/ticket_type.rb @@ -6,7 +6,7 @@ class TicketType < ApplicationRecord # Validations validates :name, presence: true, length: { minimum: 3, maximum: 50 } 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 :sale_start_at, presence: true validates :sale_end_at, presence: true @@ -48,6 +48,10 @@ class TicketType < ApplicationRecord [ quantity - tickets.count, 0 ].max end + def free? + price_cents == 0 + end + def sales_status return :draft if sale_start_at.nil? || sale_end_at.nil? return :expired if sale_end_at < Time.current diff --git a/app/views/components/_ticket_card.html.erb b/app/views/components/_ticket_card.html.erb index 11fe4d5..81c9056 100755 --- a/app/views/components/_ticket_card.html.erb +++ b/app/views/components/_ticket_card.html.erb @@ -7,7 +7,7 @@

"> - <%= number_to_currency(price_cents / 100.0, unit: "€") %> + <%= format_ticket_price(price_cents) %>

diff --git a/app/views/promoter/ticket_types/edit.html.erb b/app/views/promoter/ticket_types/edit.html.erb index 0f7e836..87fae02 100644 --- a/app/views/promoter/ticket_types/edit.html.erb +++ b/app/views/promoter/ticket_types/edit.html.erb @@ -81,7 +81,7 @@
<%= form.number_field :price_euros, 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", data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
@@ -91,6 +91,8 @@ Modifier le prix n'affectera pas les billets déjà vendus

+ <% else %> +

Prix unitaire du billet (0€ pour un billet gratuit)

<% end %>
diff --git a/app/views/promoter/ticket_types/new.html.erb b/app/views/promoter/ticket_types/new.html.erb index dd57e59..bfa8077 100644 --- a/app/views/promoter/ticket_types/new.html.erb +++ b/app/views/promoter/ticket_types/new.html.erb @@ -66,14 +66,14 @@
<%= form.label :price_euros, "Prix (€)", class: "block text-sm font-medium text-gray-700 mb-2" %>
- <%= form.number_field :price_euros, - step: 0.01, - min: 0.01, + <%= form.number_field :price_euros, + step: 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", data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
-

Prix unitaire du billet

+

Prix unitaire du billet (0€ pour un billet gratuit)

diff --git a/app/views/promoter/ticket_types/show.html.erb b/app/views/promoter/ticket_types/show.html.erb index fea82b3..a5d1fe1 100644 --- a/app/views/promoter/ticket_types/show.html.erb +++ b/app/views/promoter/ticket_types/show.html.erb @@ -138,7 +138,7 @@

- <%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %> + <%= format_ticket_price(ticket.price_cents) %>

<%= ticket.created_at.strftime("%d/%m/%Y") %> @@ -164,7 +164,7 @@

- <%= number_to_currency(@ticket_type.price_euros, unit: "€") %> + <%= format_ticket_price(@ticket_type.price_cents) %>
Prix unitaire
diff --git a/app/views/ticket_mailer/purchase_confirmation.html.erb b/app/views/ticket_mailer/purchase_confirmation.html.erb index fbb52e2..7f276a6 100755 --- a/app/views/ticket_mailer/purchase_confirmation.html.erb +++ b/app/views/ticket_mailer/purchase_confirmation.html.erb @@ -57,7 +57,7 @@

📱 Voir le détail et le code QR

-

<%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>

+

<%= format_ticket_price(ticket.price_cents) %>

@@ -83,7 +83,7 @@

Prix

-

<%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>

+

<%= format_ticket_price(@ticket.price_cents) %>

diff --git a/app/views/ticket_mailer/purchase_confirmation.text.erb b/app/views/ticket_mailer/purchase_confirmation.text.erb index b4d9c78..3329cb9 100755 --- a/app/views/ticket_mailer/purchase_confirmation.text.erb +++ b/app/views/ticket_mailer/purchase_confirmation.text.erb @@ -13,11 +13,11 @@ DÉTAILS DE VOTRE COMMANDE Événement : <%= @event.name %> Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %> 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 : <% @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 %> 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 %> Type de billet : <%= @ticket.ticket_type.name %> 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. diff --git a/app/views/tickets/show.html.erb b/app/views/tickets/show.html.erb index 94da8ac..cba7c3f 100644 --- a/app/views/tickets/show.html.erb +++ b/app/views/tickets/show.html.erb @@ -80,7 +80,7 @@

- <%= number_to_currency(@ticket.price_euros, unit: "€", separator: ",", delimiter: " ", format: "%n %u") %> + <%= format_ticket_price(@ticket.price_cents) %>

diff --git a/test/controllers/orders_controller_test.rb b/test/controllers/orders_controller_test.rb index aaa6a3c..1cb416d 100644 --- a/test/controllers/orders_controller_test.rb +++ b/test/controllers/orders_controller_test.rb @@ -125,7 +125,7 @@ class OrdersControllerTest < ActionDispatch::IntegrationTest assert_equal "draft", new_order.status assert_equal @user, new_order.user 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_equal new_order.id, session[:draft_order_id] diff --git a/test/models/order_test.rb b/test/models/order_test.rb index 6db2124..0a67b6b 100644 --- a/test/models/order_test.rb +++ b/test/models/order_test.rb @@ -603,4 +603,22 @@ class OrderTest < ActiveSupport::TestCase result = order.stripe_invoice_pdf_url assert_nil result 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 diff --git a/test/models/ticket_type_test.rb b/test/models/ticket_type_test.rb index 3aea6b0..0b3caf3 100755 --- a/test/models/ticket_type_test.rb +++ b/test/models/ticket_type_test.rb @@ -244,4 +244,38 @@ class TicketTypeTest < ActiveSupport::TestCase ) assert_not ticket_type.save 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