Files
aperonight/test/models/order_test.rb
kbe 049e5505ef refactor(pricing): implement hybrid fee model (€0.50 + 1.5%) deducted from promoter payout
- Remove 1€ fixed fee from orders and Stripe invoices
- Add platform_fee_cents, promoter_payout_cents methods to Order model
- Update views to show clean ticket totals without added fees
- Update tests for new fee calculation logic
- Update pricing docs with implemented model
2025-09-15 20:07:51 +02:00

607 lines
17 KiB
Ruby

require "test_helper"
class OrderTest < 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
# === Basic Model Tests ===
test "should be a class" do
assert_kind_of Class, Order
end
# === Constants Tests ===
test "should have correct constants defined" do
assert_equal 15.minutes, Order::DRAFT_EXPIRY_TIME
assert_equal 3, Order::MAX_PAYMENT_ATTEMPTS
end
# === Association Tests ===
test "should belong to user" do
association = Order.reflect_on_association(:user)
assert_equal :belongs_to, association.macro
end
test "should belong to event" do
association = Order.reflect_on_association(:event)
assert_equal :belongs_to, association.macro
end
test "should have many tickets with dependent destroy" do
association = Order.reflect_on_association(:tickets)
assert_equal :has_many, association.macro
assert_equal :destroy, association.options[:dependent]
end
# === Validation Tests ===
test "should not save order without user" do
order = Order.new(event: @event, total_amount_cents: 1000, status: "draft", payment_attempts: 0)
assert_not order.save
assert_includes order.errors[:user_id], "can't be blank"
end
test "should not save order without event" do
order = Order.new(user: @user, total_amount_cents: 1000, status: "draft", payment_attempts: 0)
assert_not order.save
assert_includes order.errors[:event_id], "can't be blank"
end
test "should use default status when not provided" do
order = Order.new(user: @user, event: @event)
order.save!
assert_equal "draft", order.status
end
test "should not save order with invalid status" do
order = Order.new(
user: @user,
event: @event,
total_amount_cents: 1000,
status: "invalid_status",
payment_attempts: 0
)
assert_not order.save
assert_includes order.errors[:status], "is not included in the list"
end
test "should save order with valid statuses" do
valid_statuses = %w[draft pending_payment paid completed cancelled expired]
valid_statuses.each do |status|
order = Order.new(
user: @user,
event: @event,
total_amount_cents: 1000,
status: status,
payment_attempts: 0
)
assert order.save, "Should save with status: #{status}"
end
end
test "should use default total_amount_cents when not provided" do
order = Order.new(user: @user, event: @event)
order.save!
assert_equal 0, order.total_amount_cents
end
test "should not save order with negative total_amount_cents" do
order = Order.new(
user: @user,
event: @event,
total_amount_cents: -100
)
assert_not order.save
assert_includes order.errors[:total_amount_cents], "must be greater than or equal to 0"
end
test "should save order with zero total_amount_cents" do
order = Order.new(
user: @user,
event: @event,
total_amount_cents: 0
)
assert order.save
end
test "should use default payment_attempts when not provided" do
order = Order.new(user: @user, event: @event)
order.save!
assert_equal 0, order.payment_attempts
end
test "should not save order with negative payment_attempts" do
order = Order.new(
user: @user,
event: @event,
payment_attempts: -1
)
assert_not order.save
assert_includes order.errors[:payment_attempts], "must be greater than or equal to 0"
end
# === Callback Tests ===
test "should set expiry time for draft order on create" do
order = Order.new(
user: @user,
event: @event
)
assert_nil order.expires_at
order.save!
assert_not_nil order.expires_at
assert_in_delta Time.current + Order::DRAFT_EXPIRY_TIME, order.expires_at, 5.seconds
end
test "should not set expiry time for non-draft order on create" do
order = Order.new(
user: @user,
event: @event,
status: "paid"
)
order.save!
assert_nil order.expires_at
end
test "should not override existing expires_at on create" do
custom_expiry = 1.hour.from_now
order = Order.new(
user: @user,
event: @event,
expires_at: custom_expiry
)
order.save!
assert_equal custom_expiry.to_i, order.expires_at.to_i
end
# === Scope Tests ===
test "draft scope should return only draft orders" do
draft_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
paid_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "paid", payment_attempts: 0
)
draft_orders = Order.draft
assert_includes draft_orders, draft_order
assert_not_includes draft_orders, paid_order
end
test "active scope should return paid and completed orders" do
draft_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
paid_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "paid", payment_attempts: 0
)
completed_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "completed", payment_attempts: 0
)
active_orders = Order.active
assert_not_includes active_orders, draft_order
assert_includes active_orders, paid_order
assert_includes active_orders, completed_order
end
test "expired_drafts scope should return expired draft orders" do
# Create an expired draft order
expired_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0,
expires_at: 1.hour.ago
)
# Create a non-expired draft order
active_draft = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
expired_drafts = Order.expired_drafts
assert_includes expired_drafts, expired_order
assert_not_includes expired_drafts, active_draft
end
test "can_retry_payment scope should return retryable orders" do
# Create a retryable order
retryable_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 1
)
# Create a non-retryable order (too many attempts)
max_attempts_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: Order::MAX_PAYMENT_ATTEMPTS
)
# Create an expired order
expired_order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 1,
expires_at: 1.hour.ago
)
retryable_orders = Order.can_retry_payment
assert_includes retryable_orders, retryable_order
assert_not_includes retryable_orders, max_attempts_order
assert_not_includes retryable_orders, expired_order
end
# === Instance Method Tests ===
test "total_amount_euros should convert cents to euros" do
order = Order.new(total_amount_cents: 1500)
assert_equal 15.0, order.total_amount_euros
order = Order.new(total_amount_cents: 1050)
assert_equal 10.5, order.total_amount_euros
end
test "can_retry_payment? should return true for retryable orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 1
)
assert order.can_retry_payment?
end
test "can_retry_payment? should return false for non-draft orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "paid", payment_attempts: 1
)
assert_not order.can_retry_payment?
end
test "can_retry_payment? should return false for max attempts reached" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: Order::MAX_PAYMENT_ATTEMPTS
)
assert_not order.can_retry_payment?
end
test "can_retry_payment? should return false for expired orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 1,
expires_at: 1.hour.ago
)
assert_not order.can_retry_payment?
end
test "expired? should return true for expired orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0,
expires_at: 1.hour.ago
)
assert order.expired?
end
test "expired? should return false for non-expired orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
assert_not order.expired?
end
test "expired? should return false when expires_at is nil" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "paid", payment_attempts: 0
)
assert_not order.expired?
end
test "expire_if_overdue! should mark expired draft as expired" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0,
expires_at: 1.hour.ago
)
order.expire_if_overdue!
order.reload
assert_equal "expired", order.status
end
test "expire_if_overdue! should not affect non-draft orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "paid", payment_attempts: 0,
expires_at: 1.hour.ago
)
order.expire_if_overdue!
order.reload
assert_equal "paid", order.status
end
test "expire_if_overdue! should not affect non-expired orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
order.expire_if_overdue!
order.reload
assert_equal "draft", order.status
end
test "increment_payment_attempt! should increment counter and set timestamp" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
assert_nil order.last_payment_attempt_at
order.increment_payment_attempt!
order.reload
assert_equal 1, order.payment_attempts
assert_not_nil order.last_payment_attempt_at
assert_in_delta Time.current, order.last_payment_attempt_at, 5.seconds
end
test "expiring_soon? should return true for orders expiring within 5 minutes" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0,
expires_at: 3.minutes.from_now
)
assert order.expiring_soon?
end
test "expiring_soon? should return false for orders expiring later" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0,
expires_at: 10.minutes.from_now
)
assert_not order.expiring_soon?
end
test "expiring_soon? should return false for non-draft orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "paid", payment_attempts: 0,
expires_at: 3.minutes.from_now
)
assert_not order.expiring_soon?
end
test "expiring_soon? should return false when expires_at is nil" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
order.update_column(:expires_at, nil) # Bypass validation to test edge case
assert_not order.expiring_soon?
end
test "mark_as_paid! should update status and activate tickets" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
# Create some 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: 1000,
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
ticket1 = Ticket.create!(
order: order,
ticket_type: ticket_type,
status: "draft",
first_name: "John",
last_name: "Doe"
)
ticket2 = Ticket.create!(
order: order,
ticket_type: ticket_type,
status: "draft",
first_name: "Jane",
last_name: "Doe"
)
order.mark_as_paid!
order.reload
ticket1.reload
ticket2.reload
assert_equal "paid", order.status
assert_equal "active", ticket1.status
assert_equal "active", ticket2.status
end
test "calculate_total! should sum ticket prices only (platform fee deducted from promoter payout)" 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"
)
order.calculate_total!
order.reload
assert_equal 3000, order.total_amount_cents # 2 tickets * 1500 cents (no service fee added to customer)
end
test "platform_fee_cents should calculate €0.50 + 1.5% per ticket" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 0,
status: "draft", payment_attempts: 0
)
ticket_type1 = TicketType.create!(
name: "Cheap Ticket",
description: "Cheap ticket type",
price_cents: 1000, # €10
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
ticket_type2 = TicketType.create!(
name: "Expensive Ticket",
description: "Expensive ticket type",
price_cents: 5000, # €50
quantity: 10,
sale_start_at: Time.current,
sale_end_at: Time.current + 1.day,
requires_id: false,
event: @event
)
ticket1 = Ticket.create!(order: order, ticket_type: ticket_type1, status: "draft", first_name: "John", last_name: "Doe")
ticket2 = Ticket.create!(order: order, ticket_type: ticket_type2, status: "draft", first_name: "Jane", last_name: "Doe")
expected_fee = (50 + (1000 * 0.015).to_i) + (50 + (5000 * 0.015).to_i) # 50+15 + 50+75 = 190
assert_equal 190, order.platform_fee_cents
end
test "promoter_payout_cents should be total minus platform fee" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 3000,
status: "paid", payment_attempts: 0
)
ticket_type = TicketType.create!(
name: "Test Ticket",
description: "Test ticket",
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: "active", first_name: "John", last_name: "Doe")
Ticket.create!(order: order, ticket_type: ticket_type, status: "active", first_name: "Jane", last_name: "Doe")
order.calculate_total! # Should still be 3000
expected_payout = 3000 - (50 + (1500 * 0.015).to_i) * 2 # 3000 - (50+22.5≈22)*2 = 3000 - 144 = 2856
assert_equal 2856, order.promoter_payout_cents
end
test "platform_fee_euros should convert cents to euros" do
order = Order.new(total_amount_cents: 0)
# Assuming one €10 ticket: 50 + 150 = 200 cents = €2.00
def order.platform_fee_cents; 200; end
assert_equal 2.0, order.platform_fee_euros
end
test "promoter_payout_euros should convert cents to euros" do
order = Order.new(total_amount_cents: 10000)
def order.platform_fee_cents; 500; end
assert_equal 95.0, order.promoter_payout_euros
end
# === Stripe Integration Tests (Mock) ===
test "create_stripe_invoice! should return nil for non-paid orders" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "draft", payment_attempts: 0
)
result = order.create_stripe_invoice!
assert_nil result
end
test "stripe_invoice_pdf_url should return nil when no invoice ID present" do
order = Order.create!(
user: @user, event: @event, total_amount_cents: 1000,
status: "paid", payment_attempts: 0
)
result = order.stripe_invoice_pdf_url
assert_nil result
end
end