feat: replace Stripe Global Payouts with manual bank transfer system for France compliance

- Replace Stripe automatic payouts with manual admin-processed bank transfers
- Add banking information fields (IBAN, bank name, account holder) to User model
- Implement manual payout workflow: pending → approved → processing → completed
- Add comprehensive admin interface for payout review and processing
- Update Payout model with manual processing fields and workflow methods
- Add transfer reference tracking and rejection/failure handling
- Consolidate all migration fragments into clean "create" migrations
- Add comprehensive documentation for manual payout workflow
- Fix Event payout_status enum definition and database column issues

This addresses France's lack of Stripe Global Payouts support by implementing
a complete manual bank transfer workflow while maintaining audit trails and
proper admin controls.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kbe
2025-09-17 11:55:07 +02:00
parent 3c1e17c2af
commit 1889ee7fb2
20 changed files with 838 additions and 141 deletions

View File

@@ -3,36 +3,51 @@ class PayoutService
@payout = payout
end
# Legacy method for backward compatibility - now redirects to manual workflow
def process!
return unless @payout.can_process?
Rails.logger.warn "PayoutService#process! called - manual processing required for payout #{@payout.id}"
raise "Automatic payout processing is disabled. Use manual workflow in admin interface."
end
@payout.update!(status: :processing)
# Generate payout summary for manual transfer
def generate_transfer_summary
return nil unless @payout.approved? || @payout.processing?
begin
net_amount = @payout.amount_cents - @payout.fee_cents
transfer = Stripe::Transfer.create({
amount: (net_amount / 100.0).to_i,
currency: "eur",
destination: @payout.user.stripe_connected_account_id,
description: "Payout for event #{@payout.event.name}",
metadata: { payout_id: @payout.id, event_id: @payout.event_id }
}, idempotency_key: SecureRandom.uuid)
{
payout_id: @payout.id,
recipient: @payout.user.name,
account_holder: @payout.user.account_holder_name,
bank_name: @payout.user.bank_name,
iban: @payout.user.iban,
amount_euros: @payout.net_amount_euros,
description: "Payout for event: #{@payout.event.name}",
event_name: @payout.event.name,
event_date: @payout.event.date,
total_orders: @payout.total_orders_count,
refunded_orders: @payout.refunded_orders_count
}
end
@payout.update!(
status: :completed,
stripe_payout_id: transfer.id
)
# Validate banking information before processing
def validate_banking_info
errors = []
user = @payout.user
update_earnings_status
rescue Stripe::StripeError => e
@payout.update!(status: :failed)
Rails.logger.error "Stripe payout failed for payout #{@payout.id}: #{e.message}"
raise e
end
errors << "Missing IBAN" unless user.iban.present?
errors << "Missing bank name" unless user.bank_name.present?
errors << "Missing account holder name" unless user.account_holder_name.present?
errors << "Invalid IBAN format" if user.iban.present? && !valid_iban?(user.iban)
errors
end
private
def valid_iban?(iban)
# Basic IBAN validation (simplified)
iban.match?(/\A[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}\z/)
end
def update_earnings_status
@payout.event.earnings.where(status: 0).update_all(status: 1) # pending to paid
end