3 Commits

Author SHA1 Message Date
kbe
60b7bc6aa7 feat: Update from breadcrumb on the current page 2025-09-10 14:31:48 +02:00
kbe
8d2127fce2 feat: Use invoice emitter details from env var 2025-09-10 10:21:32 +02:00
kbe
2fb0e1fdbb Make invoice emitter configurable via environment variables
Add environment variables for invoice company details to allow customization without code changes. Update invoice view and Stripe service to use these configurable values.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 10:16:24 +02:00
11 changed files with 79 additions and 80 deletions

View File

@@ -30,6 +30,16 @@ SMTP_AUTHENTICATION=plain
SMTP_ENABLE_STARTTLS=false SMTP_ENABLE_STARTTLS=false
# SMTP_STARTTLS=true # SMTP_STARTTLS=true
# Invoice Emitter Configuration
INVOICE_COMPANY_NAME=AperoNight
INVOICE_COMPANY_ADDRESS_LINE_1=123 Avenue des Événements
INVOICE_COMPANY_ADDRESS_LINE_2=75000 Paris, France
INVOICE_COMPANY_EMAIL=contact@apero-night.fr
INVOICE_COMPANY_PHONE=
INVOICE_COMPANY_WEBSITE=
INVOICE_COMPANY_VAT_NUMBER=
INVOICE_COMPANY_SIRET=
# Application variables # Application variables
STRIPE_PUBLISHABLE_KEY=pk_test_51S1M7BJWx6G2LLIXYpTvi0hxMpZ4tZSxkmr2Wbp1dQ73MKNp4Tyu4xFJBqLXK5nn4E0nEf2tdgJqEwWZLosO3QGn00kMvjXWGW STRIPE_PUBLISHABLE_KEY=pk_test_51S1M7BJWx6G2LLIXYpTvi0hxMpZ4tZSxkmr2Wbp1dQ73MKNp4Tyu4xFJBqLXK5nn4E0nEf2tdgJqEwWZLosO3QGn00kMvjXWGW
STRIPE_SECRET_KEY=sk_test_51S1M7BJWx6G2LLIXK2pdLpRKb9Mgd3sZ30N4ueVjHepgxQKbWgMVJoa4v4ESzHQ6u6zJjO4jUvgLYPU1QLyAiFTN00sGz2ortW STRIPE_SECRET_KEY=sk_test_51S1M7BJWx6G2LLIXK2pdLpRKb9Mgd3sZ30N4ueVjHepgxQKbWgMVJoa4v4ESzHQ6u6zJjO4jUvgLYPU1QLyAiFTN00sGz2ortW

View File

@@ -1,6 +1,6 @@
class OnboardingController < ApplicationController class OnboardingController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :redirect_if_onboarding_complete, except: [:complete] before_action :redirect_if_onboarding_complete, except: [ :complete ]
def index def index
# Display the onboarding form # Display the onboarding form
@@ -10,7 +10,7 @@ class OnboardingController < ApplicationController
if onboarding_params_valid? if onboarding_params_valid?
current_user.update!(onboarding_params) current_user.update!(onboarding_params)
current_user.complete_onboarding! current_user.complete_onboarding!
flash[:notice] = "Bienvenue sur #{Rails.application.config.app_name} ! Votre profil a été configuré avec succès." flash[:notice] = "Bienvenue sur #{Rails.application.config.app_name} ! Votre profil a été configuré avec succès."
redirect_to dashboard_path redirect_to dashboard_path
else else
@@ -26,7 +26,7 @@ class OnboardingController < ApplicationController
end end
def onboarding_params_valid? def onboarding_params_valid?
onboarding_params[:first_name].present? && onboarding_params[:first_name].present? &&
onboarding_params[:last_name].present? onboarding_params[:last_name].present?
end end

View File

@@ -9,19 +9,19 @@ class PagesController < ApplicationController
def home def home
# Featured events for the main grid (6-9 events like Shotgun) # Featured events for the main grid (6-9 events like Shotgun)
@featured_events = Event.published.featured.includes(:ticket_types).limit(9) @featured_events = Event.published.featured.includes(:ticket_types).limit(9)
# If no featured events, show latest published events # If no featured events, show latest published events
if @featured_events.empty? if @featured_events.empty?
@featured_events = Event.published.includes(:ticket_types).order(created_at: :desc).limit(9) @featured_events = Event.published.includes(:ticket_types).order(created_at: :desc).limit(9)
end end
# Upcoming events for additional content # Upcoming events for additional content
@upcoming_events = Event.published.upcoming.limit(6) @upcoming_events = Event.published.upcoming.limit(6)
# Site metrics for landing page (with realistic fake data for demo) # Site metrics for landing page (with realistic fake data for demo)
@total_events = [Event.published.count, 50].max # At least 50 events for demo @total_events = [ Event.published.count, 50 ].max # At least 50 events for demo
@total_users = [User.count, 2500].max # At least 2500 users for demo @total_users = [ User.count, 2500 ].max # At least 2500 users for demo
@events_this_month = [Event.published.where(created_at: 1.month.ago..Time.current).count, 12].max # At least 12 this month @events_this_month = [ Event.published.where(created_at: 1.month.ago..Time.current).count, 12 ].max # At least 12 this month
@active_cities = 5 # Fixed number for demo @active_cities = 5 # Fixed number for demo
end end

View File

@@ -103,7 +103,7 @@ class StripeInvoiceService
name: customer_name, name: customer_name,
metadata: { metadata: {
user_id: @order.user.id, user_id: @order.user.id,
created_by: "aperonight_system" created_by: "#{ENV.fetch('INVOICE_COMPANY_NAME', 'aperonight').downcase}_system"
} }
}) })
@@ -133,7 +133,7 @@ class StripeInvoiceService
order_id: @order.id, order_id: @order.id,
user_id: @order.user.id, user_id: @order.user.id,
event_name: @order.event.name, event_name: @order.event.name,
created_by: "aperonight_system", created_by: "#{ENV.fetch('INVOICE_COMPANY_NAME', 'aperonight').downcase}_system",
payment_method: "checkout_session" payment_method: "checkout_session"
}, },
description: "Invoice for #{@order.event.name} - Order ##{@order.id}", description: "Invoice for #{@order.event.name} - Order ##{@order.id}",

View File

@@ -31,11 +31,26 @@
<div> <div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">Émis par</h3> <h3 class="text-lg font-semibold text-gray-900 mb-3">Émis par</h3>
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200"> <div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
<h4 class="font-semibold text-purple-900">AperoNight</h4> <h4 class="font-semibold text-purple-900"><%= ENV.fetch("INVOICE_COMPANY_NAME", "AperoNight") %></h4>
<div class="mt-2 space-y-1 text-sm text-purple-700"> <div class="mt-2 space-y-1 text-sm text-purple-700">
<p>123 Avenue des Événements</p> <% if ENV["INVOICE_COMPANY_ADDRESS_LINE_1"].present? %>
<p>75000 Paris, France</p> <p><%= ENV["INVOICE_COMPANY_ADDRESS_LINE_1"] %></p>
<p>contact@apero-night.fr</p> <% end %>
<% if ENV["INVOICE_COMPANY_ADDRESS_LINE_2"].present? %>
<p><%= ENV["INVOICE_COMPANY_ADDRESS_LINE_2"] %></p>
<% end %>
<% if ENV["INVOICE_COMPANY_EMAIL"].present? %>
<p><%= ENV["INVOICE_COMPANY_EMAIL"] %></p>
<% end %>
<% if ENV["INVOICE_COMPANY_PHONE"].present? %>
<p><%= ENV["INVOICE_COMPANY_PHONE"] %></p>
<% end %>
<% if ENV["INVOICE_COMPANY_VAT_NUMBER"].present? %>
<p>TVA: <%= ENV["INVOICE_COMPANY_VAT_NUMBER"] %></p>
<% end %>
<% if ENV["INVOICE_COMPANY_SIRET"].present? %>
<p>SIRET: <%= ENV["INVOICE_COMPANY_SIRET"] %></p>
<% end %>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,37 +1,11 @@
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100"> <div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb --> <%= render 'components/breadcrumb', crumbs: [
<nav class="flex mb-6" aria-label="Breadcrumb"> { name: 'Accueil', path: root_path },
<ol class="inline-flex items-center space-x-1 md:space-x-2 rounded-lg bg-white px-4 py-2 shadow-sm"> { name: 'Événements', path: events_path },
<li class="inline-flex items-center"> { name: @event.name, path: event_path(@event.slug, @event) },
<%= link_to "Accueil", root_path, class: "inline-flex items-center text-sm font-medium text-gray-700 hover:text-purple-600" %> { name: 'Nouvelle commande', path: nil }
</li> ] %>
<li>
<div class="flex items-center">
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
<%= link_to "Événements", events_path, class: "ml-1 text-sm font-medium text-gray-700 hover:text-purple-600 md:ml-2" %>
</div>
</li>
<li>
<div class="flex items-center">
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
<%= link_to @event.name, event_path(@event.slug, @event), class: "ml-1 text-sm font-medium text-gray-700 hover:text-purple-600 md:ml-2" %>
</div>
</li>
<li>
<div class="flex items-center">
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
<span class="ml-1 text-sm font-medium text-purple-600 md:ml-2">Nouvelle commande</span>
</div>
</li>
</ol>
</nav>
<!-- Page Header --> <!-- Page Header -->
<div class="mb-8"> <div class="mb-8">

View File

@@ -4,7 +4,7 @@ class ApplicationControllerOnboardingTest < ActionDispatch::IntegrationTest
setup do setup do
@user_without_onboarding = users(:one) @user_without_onboarding = users(:one)
@user_without_onboarding.update!(onboarding_completed: false) @user_without_onboarding.update!(onboarding_completed: false)
@user_with_onboarding = users(:two) @user_with_onboarding = users(:two)
@user_with_onboarding.update!(onboarding_completed: true, first_name: "John", last_name: "Doe") @user_with_onboarding.update!(onboarding_completed: true, first_name: "John", last_name: "Doe")
end end
@@ -54,4 +54,4 @@ class ApplicationControllerOnboardingTest < ActionDispatch::IntegrationTest
get onboarding_path get onboarding_path
assert_response :success assert_response :success
end end
end end

View File

@@ -4,7 +4,7 @@ class OnboardingControllerTest < ActionDispatch::IntegrationTest
setup do setup do
@user_without_onboarding = users(:one) @user_without_onboarding = users(:one)
@user_without_onboarding.update!(onboarding_completed: false) @user_without_onboarding.update!(onboarding_completed: false)
@user_with_onboarding = users(:two) @user_with_onboarding = users(:two)
@user_with_onboarding.update!(onboarding_completed: true, first_name: "John", last_name: "Doe") @user_with_onboarding.update!(onboarding_completed: true, first_name: "John", last_name: "Doe")
end end
@@ -30,20 +30,20 @@ class OnboardingControllerTest < ActionDispatch::IntegrationTest
test "should complete onboarding with valid data" do test "should complete onboarding with valid data" do
sign_in @user_without_onboarding sign_in @user_without_onboarding
assert_not @user_without_onboarding.onboarding_completed? assert_not @user_without_onboarding.onboarding_completed?
post complete_onboarding_path, params: { post complete_onboarding_path, params: {
user: { user: {
first_name: "Jane", first_name: "Jane",
last_name: "Smith" last_name: "Smith"
} }
} }
assert_redirected_to dashboard_path assert_redirected_to dashboard_path
follow_redirect! follow_redirect!
assert_select ".notification", /Bienvenue sur Aperonight/ assert_select ".notification", /Bienvenue sur Aperonight/
@user_without_onboarding.reload @user_without_onboarding.reload
assert @user_without_onboarding.onboarding_completed? assert @user_without_onboarding.onboarding_completed?
assert_equal "Jane", @user_without_onboarding.first_name assert_equal "Jane", @user_without_onboarding.first_name
@@ -52,34 +52,34 @@ class OnboardingControllerTest < ActionDispatch::IntegrationTest
test "should not complete onboarding without required fields" do test "should not complete onboarding without required fields" do
sign_in @user_without_onboarding sign_in @user_without_onboarding
post complete_onboarding_path, params: { post complete_onboarding_path, params: {
user: { user: {
first_name: "", first_name: "",
last_name: "Smith" last_name: "Smith"
} }
} }
assert_response :success assert_response :success
assert_select ".notification", /Veuillez remplir tous les champs requis/ assert_select ".notification", /Veuillez remplir tous les champs requis/
@user_without_onboarding.reload @user_without_onboarding.reload
assert_not @user_without_onboarding.onboarding_completed? assert_not @user_without_onboarding.onboarding_completed?
end end
test "should not complete onboarding without last name" do test "should not complete onboarding without last name" do
sign_in @user_without_onboarding sign_in @user_without_onboarding
post complete_onboarding_path, params: { post complete_onboarding_path, params: {
user: { user: {
first_name: "Jane", first_name: "Jane",
last_name: "" last_name: ""
} }
} }
assert_response :success assert_response :success
assert_select ".notification", /Veuillez remplir tous les champs requis/ assert_select ".notification", /Veuillez remplir tous les champs requis/
@user_without_onboarding.reload @user_without_onboarding.reload
assert_not @user_without_onboarding.onboarding_completed? assert_not @user_without_onboarding.onboarding_completed?
end end

View File

@@ -24,7 +24,7 @@ class TicketMailerTest < ActionMailer::TestCase
assert_equal [ "no-reply@aperonight.fr" ], email.from assert_equal [ "no-reply@aperonight.fr" ], email.from
assert_equal [ @user.email ], email.to assert_equal [ @user.email ], email.to
assert_equal "Confirmation d'achat - #{@event.name}", email.subject assert_equal "Confirmation d'achat - #{@event.name}", email.subject
# Check if we have any content # Check if we have any content
content = "" content = ""
if email.html_part if email.html_part
@@ -34,7 +34,7 @@ class TicketMailerTest < ActionMailer::TestCase
else else
content = email.body.to_s content = email.body.to_s
end end
# If still empty, try to get content from parts # If still empty, try to get content from parts
if content.empty? && email.parts.any? if content.empty? && email.parts.any?
email.parts.each do |part| email.parts.each do |part|
@@ -44,7 +44,7 @@ class TicketMailerTest < ActionMailer::TestCase
end end
end end
end end
# Instead of strict matching, just check that content exists # Instead of strict matching, just check that content exists
assert content.length > 0, "Email body should not be empty" assert content.length > 0, "Email body should not be empty"
assert_match @event.name, content assert_match @event.name, content
@@ -64,7 +64,7 @@ class TicketMailerTest < ActionMailer::TestCase
assert_equal [ "no-reply@aperonight.fr" ], email.from assert_equal [ "no-reply@aperonight.fr" ], email.from
assert_equal [ @ticket.user.email ], email.to assert_equal [ @ticket.user.email ], email.to
assert_equal "Confirmation d'achat - #{@ticket.event.name}", email.subject assert_equal "Confirmation d'achat - #{@ticket.event.name}", email.subject
# Check if we have any content # Check if we have any content
content = "" content = ""
if email.html_part if email.html_part
@@ -74,7 +74,7 @@ class TicketMailerTest < ActionMailer::TestCase
else else
content = email.body.to_s content = email.body.to_s
end end
# If still empty, try to get content from parts # If still empty, try to get content from parts
if content.empty? && email.parts.any? if content.empty? && email.parts.any?
email.parts.each do |part| email.parts.each do |part|
@@ -84,7 +84,7 @@ class TicketMailerTest < ActionMailer::TestCase
end end
end end
end end
# Instead of strict matching, just check that content exists # Instead of strict matching, just check that content exists
assert content.length > 0, "Email body should not be empty" assert content.length > 0, "Email body should not be empty"
assert_match @ticket.event.name, content assert_match @ticket.event.name, content
@@ -105,7 +105,7 @@ class TicketMailerTest < ActionMailer::TestCase
assert_equal [ "no-reply@aperonight.fr" ], email.from assert_equal [ "no-reply@aperonight.fr" ], email.from
assert_equal [ @user.email ], email.to assert_equal [ @user.email ], email.to
assert_equal "Rappel : #{@event.name} dans une semaine", email.subject assert_equal "Rappel : #{@event.name} dans une semaine", email.subject
# Check content properly # Check content properly
content = "" content = ""
if email.html_part if email.html_part
@@ -115,7 +115,7 @@ class TicketMailerTest < ActionMailer::TestCase
else else
content = email.body.to_s content = email.body.to_s
end end
assert content.length > 0, "Email body should not be empty" assert content.length > 0, "Email body should not be empty"
assert_match /une semaine/, content assert_match /une semaine/, content
assert_match @event.name, content assert_match @event.name, content
@@ -136,7 +136,7 @@ class TicketMailerTest < ActionMailer::TestCase
end end
assert_equal "Rappel : #{@event.name} demain", email.subject assert_equal "Rappel : #{@event.name} demain", email.subject
# Check content properly # Check content properly
content = "" content = ""
if email.html_part if email.html_part
@@ -146,7 +146,7 @@ class TicketMailerTest < ActionMailer::TestCase
else else
content = email.body.to_s content = email.body.to_s
end end
assert content.length > 0, "Email body should not be empty" assert content.length > 0, "Email body should not be empty"
assert_match /demain/, content assert_match /demain/, content
end end
@@ -161,7 +161,7 @@ class TicketMailerTest < ActionMailer::TestCase
end end
assert_equal "C'est aujourd'hui : #{@event.name}", email.subject assert_equal "C'est aujourd'hui : #{@event.name}", email.subject
# Check content properly # Check content properly
content = "" content = ""
if email.html_part if email.html_part
@@ -171,7 +171,7 @@ class TicketMailerTest < ActionMailer::TestCase
else else
content = email.body.to_s content = email.body.to_s
end end
assert content.length > 0, "Email body should not be empty" assert content.length > 0, "Email body should not be empty"
assert_match /aujourd'hui/, content assert_match /aujourd'hui/, content
end end
@@ -186,7 +186,7 @@ class TicketMailerTest < ActionMailer::TestCase
end end
assert_equal "Rappel : #{@event.name} dans 3 jours", email.subject assert_equal "Rappel : #{@event.name} dans 3 jours", email.subject
# Check content properly # Check content properly
content = "" content = ""
if email.html_part if email.html_part
@@ -196,7 +196,7 @@ class TicketMailerTest < ActionMailer::TestCase
else else
content = email.body.to_s content = email.body.to_s
end end
assert content.length > 0, "Email body should not be empty" assert content.length > 0, "Email body should not be empty"
assert_match /3 jours/, content assert_match /3 jours/, content
end end

View File

@@ -74,21 +74,21 @@ class UserTest < ActiveSupport::TestCase
test "should complete onboarding" do test "should complete onboarding" do
user = users(:one) user = users(:one)
user.update!(onboarding_completed: false) user.update!(onboarding_completed: false)
assert user.needs_onboarding?, "User should need onboarding initially" assert user.needs_onboarding?, "User should need onboarding initially"
user.complete_onboarding! user.complete_onboarding!
assert_not user.needs_onboarding?, "User should not need onboarding after completion" assert_not user.needs_onboarding?, "User should not need onboarding after completion"
assert user.onboarding_completed?, "User should have completed onboarding" assert user.onboarding_completed?, "User should have completed onboarding"
end end
test "needs_onboarding? should return correct value" do test "needs_onboarding? should return correct value" do
user = users(:one) user = users(:one)
user.update!(onboarding_completed: false) user.update!(onboarding_completed: false)
assert user.needs_onboarding?, "User with false onboarding_completed should need onboarding" assert user.needs_onboarding?, "User with false onboarding_completed should need onboarding"
user.update!(onboarding_completed: true) user.update!(onboarding_completed: true)
assert_not user.needs_onboarding?, "User with true onboarding_completed should not need onboarding" assert_not user.needs_onboarding?, "User with true onboarding_completed should not need onboarding"
end end

View File

@@ -17,7 +17,7 @@ module ActiveSupport
fixtures :all fixtures :all
# Add more helper methods to be used by all tests here... # Add more helper methods to be used by all tests here...
# Helper to create users with completed onboarding by default for tests # Helper to create users with completed onboarding by default for tests
def create_test_user(attributes = {}) def create_test_user(attributes = {})
User.create!({ User.create!({