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 # === 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 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 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