feat(payouts): implement promoter earnings viewing, request flow, and admin Stripe processing with webhooks

Add model methods for accurate net calculations (€0.50 + 1.5% fees), eligibility, refund handling
Update promoter/payouts controller for index (pending events), create (eligibility checks)
Integrate admin processing via Stripe::Transfer, webhook for status sync
Enhance views: index pending cards, events/show preview/form
Add comprehensive tests (models, controllers, service, integration); run migrations
This commit is contained in:
kbe
2025-09-17 02:07:52 +02:00
parent 47f4f50e5b
commit 3c1e17c2af
31 changed files with 1096 additions and 148 deletions

View File

@@ -317,4 +317,142 @@ class EventTest < ActiveSupport::TestCase
# Check that ticket types were NOT duplicated
assert_equal 0, duplicated_event.ticket_types.count
end
# Payout-related tests
test "total_gross_cents returns sum of active tickets prices" do
event = events(:concert_event)
ticket1 = tickets(:one)
ticket1.status = "active"
ticket1.price_cents = 1000
ticket1.save!
ticket2 = Ticket.create!(order: orders(:one), ticket_type: ticket_types(:one), qr_code: "qr2", price_cents: 2000, status: "active", first_name: "Test", last_name: "User")
ticket2.event = event
ticket2.save!
assert_equal 3000, event.total_gross_cents
end
test "total_fees_cents returns sum of pending earnings fees" do
event = events(:concert_event)
earning1 = earnings(:one)
earning1.status = "pending"
earning1.fee_cents = 100
earning1.save!
earning2 = Earning.create!(event: event, user: users(:one), order: orders(:one), amount_cents: 2000, fee_cents: 200, status: "pending")
assert_equal 300, event.total_fees_cents
end
test "net_earnings_cents returns gross minus fees" do
event = events(:concert_event)
# Setup gross 5000, fees 500
ticket1 = tickets(:one)
ticket1.status = "active"
ticket1.price_cents = 2500
ticket1.save!
ticket2 = Ticket.create!(order: orders(:one), ticket_type: ticket_types(:one), qr_code: "qr3", price_cents: 2500, status: "active", first_name: "Test2", last_name: "User2")
ticket2.event = event
ticket2.save!
earning1 = earnings(:one)
earning1.status = "pending"
earning1.fee_cents = 250
earning1.save!
earning2 = Earning.create!(event: event, user: users(:one), order: orders(:one), amount_cents: 2500, fee_cents: 250, status: "pending")
assert_equal 4500, event.net_earnings_cents
end
test "can_request_payout? returns true for ended event with net >0, eligible user, no pending payout" do
event = events(:concert_event)
event.update!(end_time: 1.day.ago) # ended
# Setup net >0
ticket = Ticket.create!(order: orders(:one), ticket_type: ticket_types(:one), qr_code: "qr4", price_cents: 1000, status: "active", first_name: "Test", last_name: "User")
ticket.event = event
ticket.save!
earning = Earning.create!(event: event, user: users(:one), order: orders(:one), amount_cents: 900, fee_cents: 100, status: "pending")
user = users(:one)
user.update!(is_professionnal: true) # eligible
# No pending payout
assert event.can_request_payout?(user)
end
test "can_request_payout? returns false for not ended event" do
event = events(:concert_event)
event.update!(end_time: 1.day.from_now) # not ended
user = users(:one)
user.update!(is_professionnal: true)
assert_not event.can_request_payout?(user)
end
test "can_request_payout? returns false if net <=0" do
event = events(:concert_event)
event.update!(end_time: 1.day.ago)
user = users(:one)
user.update!(is_professionnal: true)
assert_not event.can_request_payout?(user)
end
test "can_request_payout? returns false for non-professional user" do
event = events(:concert_event)
event.update!(end_time: 1.day.ago)
# Setup net >0
ticket = Ticket.create!(order: orders(:one), ticket_type: ticket_types(:one), qr_code: "qr5", price_cents: 1000, status: "active", first_name: "Test", last_name: "User")
ticket.event = event
ticket.save!
earning = Earning.create!(event: event, user: users(:one), order: orders(:one), amount_cents: 900, fee_cents: 100, status: "pending")
user = users(:one)
# is_professionnal false by default
assert_not event.can_request_payout?(user)
end
test "can_request_payout? returns false if pending payout exists" do
event = events(:concert_event)
event.update!(end_time: 1.day.ago)
# Setup net >0
ticket = Ticket.create!(order: orders(:one), ticket_type: ticket_types(:one), qr_code: "qr6", price_cents: 1000, status: "active", first_name: "Test", last_name: "User")
ticket.event = event
ticket.save!
earning = Earning.create!(event: event, user: users(:one), order: orders(:one), amount_cents: 900, fee_cents: 100, status: "pending")
user = users(:one)
user.update!(is_professionnal: true)
Payout.create!(user: user, event: event, amount_cents: 800, fee_cents: 100, status: :pending)
assert_not event.can_request_payout?(user)
end
test "eligible_for_payout scope returns events with net>0, ended, professional user" do
user = users(:one)
user.update!(is_professionnal: true)
eligible = Event.create!(name: "Eligible", slug: "eligible", description: "desc", venue_name: "v", venue_address: "a", latitude: 48.0, longitude: 2.0, start_time: 1.day.ago, end_time: 2.days.ago, user: user, state: :published)
# Setup net >0
ticket = Ticket.create!(order: orders(:one), ticket_type: ticket_types(:one), qr_code: "qr7", price_cents: 1000, status: "active", first_name: "Test", last_name: "User")
ticket.event = eligible
ticket.save!
earning = Earning.create!(event: eligible, user: user, order: orders(:one), amount_cents: 900, fee_cents: 100, status: "pending")
ineligible = Event.create!(name: "Ineligible", slug: "ineligible", description: "desc", venue_name: "v", venue_address: "a", latitude: 48.0, longitude: 2.0, start_time: 1.day.from_now, end_time: 2.days.from_now, user: user, state: :published)
# net =0
eligible_events = Event.eligible_for_payout
assert_includes eligible_events, eligible
assert_not_includes eligible_events, ineligible
end
end