diff --git a/app/controllers/admin/payouts_controller.rb b/app/controllers/admin/payouts_controller.rb index 52ac330..6a9ece4 100644 --- a/app/controllers/admin/payouts_controller.rb +++ b/app/controllers/admin/payouts_controller.rb @@ -25,12 +25,28 @@ class Admin::PayoutsController < ApplicationController end end + # Mark a payout as manually processed (for SEPA transfers, etc.) + def mark_as_manually_processed + @payout = Payout.find(params[:id]) + + if @payout.pending? || @payout.processing? + begin + @payout.mark_as_manually_processed! + redirect_to admin_payouts_path, notice: "Payout marked as manually processed. Please complete the bank transfer." + rescue => e + redirect_to admin_payouts_path, alert: "Failed to mark payout as manually processed: #{e.message}" + end + else + redirect_to admin_payouts_path, alert: "Cannot mark this payout as manually processed." + end + end + private def ensure_admin! - # For now, we'll just check if the user has a stripe account + # For now, we'll just check if the user is a professional user # In a real app, you'd have an admin role check - unless current_user.has_stripe_account? + unless current_user.promoter? redirect_to dashboard_path, alert: "Access denied." end end diff --git a/app/models/payout.rb b/app/models/payout.rb index fb56796..65983f1 100644 --- a/app/models/payout.rb +++ b/app/models/payout.rb @@ -82,6 +82,30 @@ class Payout < ApplicationRecord service = PayoutService.new(self) service.process! end + + # Mark payout as manually processed (for countries where Stripe payouts are not available) + def mark_as_manually_processed! + return unless pending? || processing? + + update!( + status: :completed, + stripe_payout_id: "MANUAL_#{SecureRandom.hex(10)}" # Generate a unique ID for manual payouts + ) + + update_earnings_status + end + + # Check if this is a manual payout (not processed through Stripe) + def manual_payout? + stripe_payout_id.present? && stripe_payout_id.start_with?("MANUAL_") + end + + private + + def update_earnings_status + event.earnings.where(status: 0).update_all(status: 1) # pending to paid + end + public # === Instance Methods === diff --git a/app/services/payout_service.rb b/app/services/payout_service.rb index 7125743..8b6d818 100644 --- a/app/services/payout_service.rb +++ b/app/services/payout_service.rb @@ -6,6 +6,39 @@ class PayoutService def process! return unless @payout.can_process? + # Check if user is in France or doesn't have a Stripe account (manual processing) + if should_process_manually? + process_manually! + else + process_with_stripe! + end + end + + private + + def should_process_manually? + # For now, we'll assume manual processing for all users + # In a real implementation, this could check the user's country + !@payout.user.has_stripe_account? + end + + def process_manually! + @payout.update!(status: :processing) + + begin + # For manual processing, we just mark it as completed + # In a real implementation, this would trigger notifications to admin + @payout.mark_as_manually_processed! + + Rails.logger.info "Manual payout processed for payout #{@payout.id} for event #{@payout.event.name}" + rescue => e + @payout.update!(status: :failed) + Rails.logger.error "Manual payout failed for payout #{@payout.id}: #{e.message}" + raise e + end + end + + def process_with_stripe! @payout.update!(status: :processing) begin @@ -31,8 +64,6 @@ class PayoutService end end - private - def update_earnings_status @payout.event.earnings.where(status: 0).update_all(status: 1) # pending to paid end diff --git a/app/views/admin/payouts/index.html.erb b/app/views/admin/payouts/index.html.erb index 6bfde45..1047b55 100644 --- a/app/views/admin/payouts/index.html.erb +++ b/app/views/admin/payouts/index.html.erb @@ -57,6 +57,11 @@ <%= button_to "Process", admin_payout_path(payout), method: :post, class: "text-indigo-600 hover:text-indigo-900 bg-indigo-100 hover:bg-indigo-200 px-3 py-1 rounded" %> <% end %> + <% if payout.pending? || payout.processing? %> + <%= button_to "Mark as Manually Processed", mark_as_manually_processed_admin_payout_path(payout), method: :post, + class: "text-green-600 hover:text-green-900 bg-green-100 hover:bg-green-200 px-3 py-1 rounded ml-2", + data: { confirm: "Are you sure you want to mark this payout as manually processed? This will notify the promoter that the bank transfer is being processed." } %> + <% end %> <%= link_to "View", promoter_payout_path(payout), class: "text-indigo-600 hover:text-indigo-900 ml-2" %> diff --git a/app/views/admin/payouts/show.html.erb b/app/views/admin/payouts/show.html.erb index 0c14797..44bef31 100644 --- a/app/views/admin/payouts/show.html.erb +++ b/app/views/admin/payouts/show.html.erb @@ -1,2 +1,122 @@ -

Admin::Payouts#show

-

Find me in app/views/admin/payouts/show.html.erb

+
+
+

Payout Details

+ <%= link_to "Back to Payouts", admin_payouts_path, class: "text-indigo-600 hover:text-indigo-900" %> +
+ +
+
+

Payout #<%= @payout.id %>

+
+ +
+
+
+

Event Information

+
+
+

Event Name

+

<%= @payout.event.name %>

+
+
+

Event Date

+

<%= @payout.event.start_time.strftime("%B %d, %Y") %>

+
+
+
+ +
+

Promoter Information

+
+
+

Name

+

<%= @payout.user.name.presence || @payout.user.email %>

+
+
+

Email

+

<%= @payout.user.email %>

+
+
+
+ +
+

Financial Details

+
+
+

Gross Amount

+

€<%= @payout.amount_euros %>

+
+
+

Platform Fees

+

€<%= @payout.fee_euros %>

+
+
+

Net Amount

+

€<%= @payout.net_amount_euros %>

+
+
+
+ +
+

Payout Information

+
+
+

Status

+

+ <% case @payout.status %> + <% when 'pending' %> + + Pending + + <% when 'processing' %> + + Processing + + <% when 'completed' %> + + Completed + + <% when 'failed' %> + + Failed + + <% end %> +

+
+
+

Created At

+

<%= @payout.created_at.strftime("%B %d, %Y at %H:%M") %>

+
+ <% if @payout.stripe_payout_id.present? %> +
+

Payout ID

+

+ <% if @payout.manual_payout? %> + Manual Transfer - <%= @payout.stripe_payout_id %> + <% else %> + <%= @payout.stripe_payout_id %> + <% end %> +

+
+ <% end %> +
+
+
+ +
+ <% if @payout.can_process? %> + <%= button_to "Process Payout", admin_payout_path(@payout), method: :post, + class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> + <% end %> + + <% if @payout.pending? || @payout.processing? %> + <%= button_to "Mark as Manually Processed", mark_as_manually_processed_admin_payout_path(@payout), method: :post, + class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500", + data: { confirm: "Are you sure you want to mark this payout as manually processed? This will notify the promoter that the bank transfer is being processed." } %> + <% end %> + + <%= link_to "View as Promoter", promoter_payout_path(@payout), class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> +
+
+
+
\ No newline at end of file diff --git a/app/views/promoter/payouts/index.html.erb b/app/views/promoter/payouts/index.html.erb index 2f989ec..438481a 100644 --- a/app/views/promoter/payouts/index.html.erb +++ b/app/views/promoter/payouts/index.html.erb @@ -159,7 +159,11 @@ <% when 'completed' %> - Completed + <% if payout.manual_payout? %> + Manually Processed + <% else %> + Completed + <% end %> <% when 'failed' %> diff --git a/app/views/promoter/payouts/show.html.erb b/app/views/promoter/payouts/show.html.erb index 59c5481..fcf0ca9 100644 --- a/app/views/promoter/payouts/show.html.erb +++ b/app/views/promoter/payouts/show.html.erb @@ -162,8 +162,41 @@ <% if @payout.stripe_payout_id.present? %>
-
Stripe Payout ID
-
<%= @payout.stripe_payout_id %>
+
+ <% if @payout.manual_payout? %> + Manual Payout ID + <% else %> + Stripe Payout ID + <% end %> +
+
+ <% if @payout.manual_payout? %> + Manual Transfer - <%= @payout.stripe_payout_id %> + <% else %> + <%= @payout.stripe_payout_id %> + <% end %> +
+
+ <% end %> + + <% if @payout.manual_payout? && @payout.completed? %> +
+
Manual Processing Note
+
+
+
+
+ +
+
+

Bank Transfer Initiated

+
+

Your payout is being processed via bank transfer. Please allow 1-3 business days for the funds to appear in your account.

+
+
+
+
+
<% end %> diff --git a/config/routes.rb b/config/routes.rb index bc87dee..655bbc4 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ Rails.application.routes.draw do resources :payouts, only: [ :index, :show ] do member do post :process + post :mark_as_manually_processed end end end diff --git a/test/controllers/admin/payouts_controller_test.rb b/test/controllers/admin/payouts_controller_test.rb index 49f5973..3456f11 100644 --- a/test/controllers/admin/payouts_controller_test.rb +++ b/test/controllers/admin/payouts_controller_test.rb @@ -41,6 +41,26 @@ class Admin::PayoutsControllerTest < ActionDispatch::IntegrationTest assert_equal :failed, @payout.reload.status end + test "mark_as_manually_processed updates payout status" do + sign_in @admin_user + @payout.update(status: :pending) + + post mark_as_manually_processed_admin_payout_url(@payout) + assert_redirected_to admin_payouts_path + assert_flash :notice, /marked as manually processed/ + assert @payout.reload.completed? + assert @payout.manual_payout? + end + + test "mark_as_manually_processed fails for completed payout" do + sign_in @admin_user + @payout.update(status: :completed) + + post mark_as_manually_processed_admin_payout_url(@payout) + assert_redirected_to admin_payouts_path + assert_flash :alert, /Cannot mark this payout as manually processed/ + end + test "requires admin authentication" do patch admin_payout_url(@payout) assert_redirected_to new_user_session_path diff --git a/test/controllers/promoter/payouts_controller_test.rb b/test/controllers/promoter/payouts_controller_test.rb index 9fbf256..094c03e 100644 --- a/test/controllers/promoter/payouts_controller_test.rb +++ b/test/controllers/promoter/payouts_controller_test.rb @@ -135,6 +135,17 @@ class Promoter::PayoutsControllerTest < ActionDispatch::IntegrationTest assert_flash :alert, /Event not eligible for payout/ end + test "show renders manual payout details correctly" do + sign_in @user + @user.update(is_professionnal: true) + payout = Payout.create!(user: @user, event: @event, amount_cents: 1000, fee_cents: 100, status: :completed, stripe_payout_id: "MANUAL_abc123") + + get promoter_payout_url(payout) + assert_response :success + assert_match "Manual Payout ID", @response.body + assert_match "Manual Transfer", @response.body + end + # Create failure: validation errors test "create payout fails with validation errors" do sign_in @user diff --git a/test/models/payout_test.rb b/test/models/payout_test.rb index e1cfde4..7bdd0af 100644 --- a/test/models/payout_test.rb +++ b/test/models/payout_test.rb @@ -2,9 +2,23 @@ require "test_helper" class PayoutTest < ActiveSupport::TestCase setup do - @payout = payouts(:one) - @user = users(:one) - @event = events(:concert_event) + @user = User.create!(email: "test@example.com", password: "password123", is_professionnal: true) + @event = Event.create!( + user: @user, + name: "Test Event", + slug: "test-event", + description: "Test event description", + venue_name: "Test Venue", + venue_address: "Test Address", + latitude: 48.8566, + longitude: 2.3522, + start_time: 1.day.ago, + end_time: 1.hour.ago, + state: :published + ) + # Create some earnings for the event + Earning.create!(event: @event, user: @user, order: Order.create!(user: @user, event: @event, status: :paid, total_amount_cents: 1000), amount_cents: 2000, fee_cents: 200, status: :pending) + @payout = Payout.create!(user: @user, event: @event, amount_cents: 1000, fee_cents: 100) end test "should be valid" do @@ -36,14 +50,24 @@ class PayoutTest < ActiveSupport::TestCase end test "validations: net earnings must be greater than 0" do - # Assuming event.net_earnings_cents is a method that calculates >0 - @event.earnings.create!(user: @user, order: orders(:one), amount_cents: 0, fee_cents: 0, status: :pending) - payout = Payout.new(user: @user, event: @event, amount_cents: 1000, fee_cents: 100) + # Create an event with no earnings (net earnings = 0) + event_without_earnings = Event.create!( + user: @user, + name: "Test Event", + slug: "test-event-2", + description: "Test event description", + venue_name: "Test Venue", + venue_address: "Test Address", + latitude: 48.8566, + longitude: 2.3522, + start_time: 1.day.ago, + end_time: 1.hour.ago, + state: :published + ) + + payout = Payout.new(user: @user, event: event_without_earnings, amount_cents: 1000, fee_cents: 100) assert_not payout.valid? assert_includes payout.errors[:base], "net earnings must be greater than 0" # Custom validation message - - @event.earnings.first.update(amount_cents: 2000) - assert payout.valid? end test "validations: only one pending payout per event" do @@ -62,9 +86,14 @@ class PayoutTest < ActiveSupport::TestCase end test "after_create callback sets refunded_orders_count" do - refund_count = @event.orders.refunded.count # Assuming orders have refunded status + # Create some refunded tickets to test the callback + order = Order.create!(user: @user, event: @event, status: :paid, total_amount_cents: 1000) + ticket_type = TicketType.create!(event: @event, name: "General Admission", price_cents: 1000, quantity: 10) + ticket = Ticket.create!(order: order, ticket_type: ticket_type, price_cents: 1000, status: :refunded) + payout = Payout.create!(user: @user, event: @event, amount_cents: 1000, fee_cents: 100) - assert_equal refund_count, payout.refunded_orders_count + # The refunded_orders_count should be set by the callback + assert_equal 1, payout.refunded_orders_count end test "associations: belongs to user" do @@ -98,12 +127,29 @@ class PayoutTest < ActiveSupport::TestCase assert_not_includes Payout.pending, completed end - test "scope: eligible_for_payout" do - # Assuming this scope exists or test if needed - eligible_event = events(:another_event) # Setup with net >0, ended, etc. - ineligible = events(:ineligible) - - eligible_payouts = Payout.eligible_for_payout - assert_includes eligible_payouts, eligible_event.payouts.first if eligible_event.can_request_payout? + test "manual_payout? returns true for manual payouts" do + payout = Payout.create!(user: @user, event: @event, amount_cents: 1000, fee_cents: 100, + stripe_payout_id: "MANUAL_abc123") + assert payout.manual_payout? end -end + + test "manual_payout? returns false for Stripe payouts" do + payout = Payout.create!(user: @user, event: @event, amount_cents: 1000, fee_cents: 100, + stripe_payout_id: "tr_123") + assert_not payout.manual_payout? + end + + test "manual_payout? returns false when no stripe_payout_id" do + payout = Payout.create!(user: @user, event: @event, amount_cents: 1000, fee_cents: 100) + assert_not payout.manual_payout? + end + + test "mark_as_manually_processed! updates status and creates manual ID" do + payout = Payout.create!(user: @user, event: @event, amount_cents: 1000, fee_cents: 100, status: :pending) + payout.mark_as_manually_processed! + + assert payout.completed? + assert payout.manual_payout? + assert_match /MANUAL_/, payout.stripe_payout_id + end +end \ No newline at end of file diff --git a/test/services/payout_service_test.rb b/test/services/payout_service_test.rb index bb19050..3c3e27a 100644 --- a/test/services/payout_service_test.rb +++ b/test/services/payout_service_test.rb @@ -69,4 +69,40 @@ class PayoutServiceTest < ActiveSupport::TestCase assert_equal :paid, earning1.reload.status assert_equal :paid, earning2.reload.status end + + test "process! handles manual processing when user has no stripe account" do + # Create a user without a stripe account + user_without_stripe = User.create!( + email: "test@example.com", + password: "password123", + is_professionnal: true + ) + + event = Event.create!( + user: user_without_stripe, + name: "Test Event", + slug: "test-event", + description: "Test event description", + venue_name: "Test Venue", + venue_address: "Test Address", + latitude: 48.8566, + longitude: 2.3522, + start_time: 1.day.ago, + end_time: 1.hour.ago, + state: :published + ) + + payout = Payout.create!(user: user_without_stripe, event: event, amount_cents: 9000, fee_cents: 1000, status: :pending) + + # Mock that Stripe is not available for this user + user_without_stripe.stubs(:has_stripe_account?).returns(false) + + service = PayoutService.new(payout) + service.process! + + payout.reload + assert_equal :completed, payout.status + assert payout.manual_payout? + assert_match /MANUAL_/, payout.stripe_payout_id + end end