feat(promotion-code): Complete promotion code integration and testing

- Add comprehensive promotion code methods to Order model
- Implement Stripe invoice integration for promotion code discounts
- Display promotion codes on invoice with proper discount breakdown
- Fix and enhance all unit tests for promotion code functionality
- Add discount calculation with capping to prevent negative totals
- Ensure promotion codes work across entire order lifecycle

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kbe
2025-09-29 20:33:54 +02:00
parent 87ccebf229
commit 635644b55a
7 changed files with 558 additions and 30 deletions

View File

@@ -6,32 +6,57 @@ class OrdersControllerPromotionTest < ActionDispatch::IntegrationTest
# Setup test data
def setup
@user = users(:one)
@event = events(:one)
@order = orders(:one)
@event = events(:concert_event)
@order = orders(:draft_order)
sign_in @user
end
# Test applying a valid promotion code
def test_apply_valid_promotion_code
# Create ticket type and tickets for the order
ticket_type = TicketType.create!(
name: "Test Ticket Type",
description: "A valid description for the ticket type that is long enough",
price_cents: 2000,
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
Ticket.create!(
order: @order,
ticket_type: ticket_type,
status: "draft",
first_name: "John",
last_name: "Doe"
)
# Recalculate the order total
@order.calculate_total!
promotion_code = PromotionCode.create(
code: "TESTDISCOUNT",
discount_amount_cents: 1000, # €10.00
discount_amount_cents: 500, # €5.00
expires_at: 1.month.from_now,
active: true
active: true,
user: @user,
event: @event
)
get checkout_order_path(@order), params: { promotion_code: "TESTDISCOUNT" }
assert_response :success
assert_not_nil flash[:notice]
assert_match /Code promotionnel appliqué: TESTDISCOUNT/, flash[:notice]
assert_not_nil flash.now[:notice]
assert_match /Code promotionnel appliqué: TESTDISCOUNT/, flash.now[:notice]
end
# Test applying an invalid promotion code
def test_apply_invalid_promotion_code
get checkout_order_path(@order), params: { promotion_code: "INVALIDCODE" }
assert_response :success
assert_not_nil flash[:alert]
assert_equal "Code promotionnel invalide", flash[:alert]
assert_not_nil flash.now[:alert]
assert_equal "Code promotionnel invalide", flash.now[:alert]
end
# Test applying an expired promotion code
@@ -40,13 +65,15 @@ class OrdersControllerPromotionTest < ActionDispatch::IntegrationTest
code: "EXPIREDCODE",
discount_amount_cents: 1000,
expires_at: 1.day.ago,
active: true
active: true,
user: @user,
event: @event
)
get checkout_order_path(@order), params: { promotion_code: "EXPIREDCODE" }
assert_response :success
assert_not_nil flash[:alert]
assert_equal "Code promotionnel invalide", flash[:alert]
assert_not_nil flash.now[:alert]
assert_equal "Code promotionnel invalide", flash.now[:alert]
end
# Test applying an inactive promotion code
@@ -55,12 +82,14 @@ class OrdersControllerPromotionTest < ActionDispatch::IntegrationTest
code: "INACTIVECODE",
discount_amount_cents: 1000,
expires_at: 1.month.from_now,
active: false
active: false,
user: @user,
event: @event
)
get checkout_order_path(@order), params: { promotion_code: "INACTIVECODE" }
assert_response :success
assert_not_nil flash[:alert]
assert_equal "Code promotionnel invalide", flash[:alert]
assert_not_nil flash.now[:alert]
assert_equal "Code promotionnel invalide", flash.now[:alert]
end
end

View File

@@ -582,6 +582,243 @@ class OrderTest < ActiveSupport::TestCase
assert_equal 95.0, order.promoter_payout_euros
end
# === Promotion Code Tests ===
test "subtotal_amount_cents should calculate total without discounts" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 0,
status: "draft", payment_attempts: 0
)
# Create ticket type and tickets
ticket_type = TicketType.create!(
name: "Test Ticket Type",
description: "A valid description for the ticket type that is long enough",
price_cents: 1500,
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
Ticket.create!(
order: order,
ticket_type: ticket_type,
status: "draft",
first_name: "John",
last_name: "Doe"
)
Ticket.create!(
order: order,
ticket_type: ticket_type,
status: "draft",
first_name: "Jane",
last_name: "Doe"
)
# Create promotion code
promotion_code = PromotionCode.create!(
code: "TESTCODE",
discount_amount_cents: 500,
user: @user,
event: @event
)
order.promotion_codes << promotion_code
order.calculate_total!
assert_equal 3000, order.subtotal_amount_cents # 2 tickets * 1500 cents
assert_equal 2500, order.total_amount_cents # 3000 - 500 discount
end
test "subtotal_amount_euros should convert subtotal cents to euros" do
order = Order.new(total_amount_cents: 2500)
def order.subtotal_amount_cents; 3000; end
assert_equal 30.0, order.subtotal_amount_euros
end
test "discount_amount_cents should calculate total discount from promotion codes" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 0,
status: "draft", payment_attempts: 0
)
# Create ticket type and tickets for subtotal
ticket_type = TicketType.create!(
name: "Test Ticket Type",
description: "A valid description for the ticket type that is long enough",
price_cents: 2000,
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
Ticket.create!(
order: order,
ticket_type: ticket_type,
status: "draft",
first_name: "John",
last_name: "Doe"
)
# Create multiple promotion codes
promo1 = PromotionCode.create!(
code: "PROMO1",
discount_amount_cents: 300,
user: @user,
event: @event
)
promo2 = PromotionCode.create!(
code: "PROMO2",
discount_amount_cents: 700,
user: @user,
event: @event
)
order.promotion_codes << [ promo1, promo2 ]
order.calculate_total!
assert_equal 1000, order.discount_amount_cents # 300 + 700 (within 2000 subtotal)
end
test "discount_amount_euros should convert discount cents to euros" do
order = Order.new(total_amount_cents: 2000)
def order.discount_amount_cents; 1000; end
assert_equal 10.0, order.discount_amount_euros
end
test "calculate_total! should apply promotion code discounts" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 0,
status: "draft", payment_attempts: 0
)
# Create ticket type and tickets
ticket_type = TicketType.create!(
name: "Test Ticket Type",
description: "A valid description for the ticket type that is long enough",
price_cents: 2000,
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
Ticket.create!(
order: order,
ticket_type: ticket_type,
status: "draft",
first_name: "John",
last_name: "Doe"
)
# Create promotion code
promotion_code = PromotionCode.create!(
code: "TESTCODE",
discount_amount_cents: 500,
user: @user,
event: @event
)
order.promotion_codes << promotion_code
order.calculate_total!
assert_equal 2000, order.subtotal_amount_cents
assert_equal 500, order.discount_amount_cents
assert_equal 1500, order.total_amount_cents
end
test "calculate_total! should handle zero total after promotion codes" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 0,
status: "draft", payment_attempts: 0
)
# Create ticket type and tickets
ticket_type = TicketType.create!(
name: "Test Ticket Type",
description: "A valid description for the ticket type that is long enough",
price_cents: 500,
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
Ticket.create!(
order: order,
ticket_type: ticket_type,
status: "draft",
first_name: "John",
last_name: "Doe"
)
# Create promotion code that covers the entire amount
promotion_code = PromotionCode.create!(
code: "FULLDISCOUNT",
discount_amount_cents: 500,
user: @user,
event: @event
)
order.promotion_codes << promotion_code
order.calculate_total!
assert_equal 500, order.subtotal_amount_cents
assert_equal 500, order.discount_amount_cents
assert_equal 0, order.total_amount_cents
assert order.free?
end
test "calculate_total! should not allow negative totals with promotion codes" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 0,
status: "draft", payment_attempts: 0
)
# Create ticket type and tickets
ticket_type = TicketType.create!(
name: "Test Ticket Type",
description: "A valid description for the ticket type that is long enough",
price_cents: 300,
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
Ticket.create!(
order: order,
ticket_type: ticket_type,
status: "draft",
first_name: "John",
last_name: "Doe"
)
# Create promotion code that exceeds the ticket amount
promotion_code = PromotionCode.create!(
code: "TOOMUCH",
discount_amount_cents: 1000,
user: @user,
event: @event
)
order.promotion_codes << promotion_code
order.calculate_total!
assert_equal 300, order.subtotal_amount_cents
assert_equal 300, order.discount_amount_cents # Capped at subtotal
assert_equal 0, order.total_amount_cents
end
# === Stripe Integration Tests (Mock) ===
test "create_stripe_invoice! should return nil for non-paid orders" do

View File

@@ -1,13 +1,34 @@
require "test_helper"
class PromotionCodeTest < ActiveSupport::TestCase
def setup
@user = User.create!(
email: "test@example.com",
password: "password123",
password_confirmation: "password123"
)
@event = Event.create!(
name: "Test Event",
slug: "test-event",
description: "A valid description for the test event that is long enough",
latitude: 48.8566,
longitude: 2.3522,
venue_name: "Test Venue",
venue_address: "123 Test Street",
user: @user
)
end
# Test valid promotion code creation
def test_valid_promotion_code
promotion_code = PromotionCode.create(
code: "DISCOUNT10",
discount_amount_cents: 1000, # €10.00
expires_at: 1.month.from_now,
active: true
active: true,
user: @user,
event: @event
)
assert promotion_code.valid?
@@ -25,23 +46,23 @@ class PromotionCodeTest < ActiveSupport::TestCase
# Test unique code validation
def test_unique_code_validation
PromotionCode.create(code: "UNIQUE123", discount_amount_cents: 500)
duplicate_code = PromotionCode.new(code: "UNIQUE123", discount_amount_cents: 500)
PromotionCode.create(code: "UNIQUE123", discount_amount_cents: 500, user: @user, event: @event)
duplicate_code = PromotionCode.new(code: "UNIQUE123", discount_amount_cents: 500, user: @user, event: @event)
refute duplicate_code.valid?
assert_not_nil duplicate_code.errors[:code]
end
# Test discount amount validation
def test_discount_amount_validation
promotion_code = PromotionCode.new(code: "VALID123", discount_amount_cents: -100)
promotion_code = PromotionCode.new(code: "VALID123", discount_amount_cents: -100, user: @user, event: @event)
refute promotion_code.valid?
assert_not_nil promotion_code.errors[:discount_amount_cents]
end
# Test active scope
def test_active_scope
active_code = PromotionCode.create(code: "ACTIVE123", discount_amount_cents: 500, active: true)
inactive_code = PromotionCode.create(code: "INACTIVE123", discount_amount_cents: 500, active: false)
active_code = PromotionCode.create(code: "ACTIVE123", discount_amount_cents: 500, active: true, user: @user, event: @event)
inactive_code = PromotionCode.create(code: "INACTIVE123", discount_amount_cents: 500, active: false, user: @user, event: @event)
assert_includes PromotionCode.active, active_code
refute_includes PromotionCode.active, inactive_code
@@ -49,8 +70,8 @@ class PromotionCodeTest < ActiveSupport::TestCase
# Test expired scope
def test_expired_scope
expired_code = PromotionCode.create(code: "EXPIRED123", discount_amount_cents: 500, expires_at: 1.day.ago)
future_code = PromotionCode.create(code: "FUTURE123", discount_amount_cents: 500, expires_at: 1.month.from_now)
expired_code = PromotionCode.create(code: "EXPIRED123", discount_amount_cents: 500, expires_at: 1.day.ago, user: @user, event: @event)
future_code = PromotionCode.create(code: "FUTURE123", discount_amount_cents: 500, expires_at: 1.month.from_now, user: @user, event: @event)
assert_includes PromotionCode.expired, expired_code
refute_includes PromotionCode.expired, future_code
@@ -58,10 +79,191 @@ class PromotionCodeTest < ActiveSupport::TestCase
# Test valid scope
def test_valid_scope
valid_code = PromotionCode.create(code: "VALID123", discount_amount_cents: 500, active: true, expires_at: 1.month.from_now)
invalid_code = PromotionCode.create(code: "INVALID123", discount_amount_cents: 500, active: false, expires_at: 1.day.ago)
valid_code = PromotionCode.create(code: "VALID123", discount_amount_cents: 500, active: true, expires_at: 1.month.from_now, user: @user, event: @event)
invalid_code = PromotionCode.create(code: "INVALID123", discount_amount_cents: 500, active: false, expires_at: 1.day.ago, user: @user, event: @event)
assert_includes PromotionCode.valid, valid_code
refute_includes PromotionCode.valid, invalid_code
end
# Test discount_amount_euros method
def test_discount_amount_euros_converts_cents_to_euros
promotion_code = PromotionCode.new(discount_amount_cents: 1000)
assert_equal 10.0, promotion_code.discount_amount_euros
promotion_code = PromotionCode.new(discount_amount_cents: 550)
assert_equal 5.5, promotion_code.discount_amount_euros
end
# Test active? method
def test_active_method
# Active and not expired
active_code = PromotionCode.create(
code: "ACTIVE1",
discount_amount_cents: 500,
active: true,
expires_at: 1.month.from_now,
user: @user,
event: @event
)
assert active_code.active?
# Active but expired
expired_active_code = PromotionCode.create(
code: "ACTIVE2",
discount_amount_cents: 500,
active: true,
expires_at: 1.day.ago,
user: @user,
event: @event
)
assert_not expired_active_code.active?
# Inactive but not expired
inactive_code = PromotionCode.create(
code: "INACTIVE1",
discount_amount_cents: 500,
active: false,
expires_at: 1.month.from_now,
user: @user,
event: @event
)
assert_not inactive_code.active?
# Active with no expiration
no_expiry_code = PromotionCode.create(
code: "NOEXPIRY",
discount_amount_cents: 500,
active: true,
expires_at: nil,
user: @user,
event: @event
)
assert no_expiry_code.active?
end
# Test expired? method
def test_expired_method
# Expired code
expired_code = PromotionCode.create(
code: "EXPIRED1",
discount_amount_cents: 500,
expires_at: 1.day.ago,
user: @user,
event: @event
)
assert expired_code.expired?
# Future code
future_code = PromotionCode.create(
code: "FUTURE1",
discount_amount_cents: 500,
expires_at: 1.month.from_now,
user: @user,
event: @event
)
assert_not future_code.expired?
# No expiration
no_expiry_code = PromotionCode.create(
code: "NOEXPIRY1",
discount_amount_cents: 500,
expires_at: nil,
user: @user,
event: @event
)
assert_not no_expiry_code.expired?
end
# Test can_be_used? method
def test_can_be_used_method
# Can be used: active, not expired, under usage limit
usable_code = PromotionCode.create(
code: "USABLE1",
discount_amount_cents: 500,
active: true,
expires_at: 1.month.from_now,
usage_limit: 10,
uses_count: 0,
user: @user,
event: @event
)
assert usable_code.can_be_used?
# Cannot be used: inactive
inactive_code = PromotionCode.create(
code: "INACTIVE2",
discount_amount_cents: 500,
active: false,
expires_at: 1.month.from_now,
usage_limit: 10,
uses_count: 0,
user: @user,
event: @event
)
assert_not inactive_code.can_be_used?
# Cannot be used: expired
expired_code = PromotionCode.create(
code: "EXPIRED2",
discount_amount_cents: 500,
active: true,
expires_at: 1.day.ago,
usage_limit: 10,
uses_count: 0,
user: @user,
event: @event
)
assert_not expired_code.can_be_used?
# Cannot be used: at usage limit
limit_reached_code = PromotionCode.create(
code: "LIMIT1",
discount_amount_cents: 500,
active: true,
expires_at: 1.month.from_now,
usage_limit: 5,
uses_count: 5,
user: @user,
event: @event
)
assert_not limit_reached_code.can_be_used?
# Can be used: no usage limit
no_limit_code = PromotionCode.create(
code: "NOLIMIT1",
discount_amount_cents: 500,
active: true,
expires_at: 1.month.from_now,
usage_limit: nil,
uses_count: 100,
user: @user,
event: @event
)
assert no_limit_code.can_be_used?
end
# Test increment_uses_count callback
def test_increment_uses_count_callback
promotion_code = PromotionCode.create(
code: "INCREMENT1",
discount_amount_cents: 500,
uses_count: 0,
user: @user,
event: @event
)
assert_equal 0, promotion_code.uses_count
# The callback should only run on create, so we test the initial value
new_code = PromotionCode.create(
code: "INCREMENT2",
discount_amount_cents: 500,
uses_count: nil,
user: @user,
event: @event
)
assert_equal 0, new_code.uses_count
end
end