class Payout < ApplicationRecord # === Relations === belongs_to :user belongs_to :event belongs_to :processed_by, class_name: "User", optional: true # === Enums === enum :status, { pending: 0, # Payout requested but not reviewed approved: 1, # Payout approved by admin, ready for transfer processing: 2, # Payout being processed (bank transfer initiated) completed: 3, # Payout successfully completed failed: 4, # Payout failed rejected: 5 # Payout rejected by admin }, default: :pending # === Validations === validates :amount_cents, presence: true, numericality: { greater_than: 0 } validates :fee_cents, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :status, presence: true validates :total_orders_count, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :refunded_orders_count, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :stripe_payout_id, allow_blank: true, uniqueness: true validate :unique_pending_event_id, if: :pending? validate :net_earnings_greater_than_zero, if: :pending? def net_earnings_greater_than_zero if event.net_earnings_cents <= 0 errors.add(:base, "net earnings must be greater than 0") end end def unique_pending_event_id if Payout.pending.where(event_id: event_id).where.not(id: id).exists? errors.add(:base, "only one pending payout allowed per event") end end # === Scopes === scope :completed, -> { where(status: :completed) } scope :pending, -> { where(status: :pending) } scope :approved, -> { where(status: :approved) } scope :processing, -> { where(status: :processing) } scope :rejected, -> { where(status: :rejected) } scope :failed, -> { where(status: :failed) } scope :eligible_for_payout, -> { joins(:event).where(events: { state: "published" }) } # === Callbacks === after_create :calculate_refunded_orders_count # === Instance Methods === # Amount in euros (formatted) def amount_euros amount_cents / 100.0 end # Fee in euros (formatted) def fee_euros fee_cents / 100.0 end # Net amount after fees def net_amount_cents amount_cents - fee_cents end # Net amount in euros def net_amount_euros net_amount_cents / 100.0 end # Check if payout can be approved (was pending) def can_approve? pending? && amount_cents > 0 && user.has_complete_banking_info? end # Check if payout can be manually processed (was approved) def can_process? approved? && amount_cents > 0 end # Check if payout can be rejected def can_reject? pending? end # Approve the payout for manual processing def approve!(admin_user) return false unless can_approve? update!( status: :approved, processed_by: admin_user, processed_at: Time.current ) end # Reject the payout with reason def reject!(admin_user, reason) return false unless can_reject? update!( status: :rejected, processed_by: admin_user, processed_at: Time.current, rejection_reason: reason ) end # Mark as processing (bank transfer initiated) def mark_processing!(admin_user, transfer_reference = nil) return false unless can_process? update!( status: :processing, processed_by: admin_user, processed_at: Time.current, bank_transfer_reference: transfer_reference ) end # Mark as completed (bank transfer confirmed) def mark_completed!(admin_user, transfer_reference = nil) return false unless processing? update!( status: :completed, processed_by: admin_user, processed_at: Time.current, bank_transfer_reference: transfer_reference || bank_transfer_reference ) update_earnings_status end # Mark as failed def mark_failed!(admin_user, reason) return false unless processing? update!( status: :failed, processed_by: admin_user, processed_at: Time.current, rejection_reason: reason ) 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 === def calculate_refunded_orders_count refunded_order_ids = event.tickets.where(status: "refunded").select(:order_id).distinct.pluck(:order_id) paid_statuses = %w[paid completed] count = event.orders.where(status: paid_statuses).where(id: refunded_order_ids).count update_column(:refunded_orders_count, count) end private def update_earnings_status event.earnings.where(status: 0).update_all(status: 1) # pending to paid end end