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