feat: implement payout system database schema and models

This commit is contained in:
kbe
2025-09-16 23:52:26 +02:00
parent e5ed1a34dd
commit 0399761fb3
23 changed files with 421 additions and 5 deletions

16
app/models/earning.rb Normal file
View File

@@ -0,0 +1,16 @@
class Earning < ApplicationRecord
# === Relations ===
belongs_to :event
belongs_to :user
belongs_to :order
# === Enums ===
enum :status, { pending: 0, paid: 1 }
# === Validations ===
validates :amount_cents, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :fee_cents, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :net_amount_cents, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
validates :status, presence: true
validates :stripe_payout_id, allow_blank: true, uniqueness: true
end

View File

@@ -16,16 +16,26 @@ class Event < ApplicationRecord
sold_out: 3
}, default: :draft
enum :payout_status, {
not_requested: 0,
requested: 1,
processing: 2,
completed: 3,
failed: 4
}, default: :not_requested
# === Relations ===
belongs_to :user
has_many :ticket_types
has_many :tickets, through: :ticket_types
has_many :orders
has_many :earnings, dependent: :destroy
# === Callbacks ===
before_validation :geocode_address, if: :should_geocode_address?
# Validations for Event attributes
# === Validations ===
# Basic information
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
validates :slug, presence: true, length: { minimum: 3, maximum: 100 }
@@ -57,6 +67,32 @@ class Event < ApplicationRecord
# === Instance Methods ===
# Payout status enum
enum :payout_status, {
not_requested: 0,
requested: 1,
processing: 2,
completed: 3,
failed: 4
}, default: :not_requested
# Payout methods
def can_request_payout?
event_ended? && earnings.pending.any? && user.can_receive_payouts?
end
def total_earnings_cents
earnings.pending.sum(:amount_cents)
end
def total_fees_cents
(total_earnings_cents * 0.1).to_i # 10% platform fee
end
def net_earnings_cents
total_earnings_cents - total_fees_cents
end
# Check if coordinates were successfully geocoded or are fallback coordinates
def geocoding_successful?
coordinates_look_valid?

View File

@@ -32,6 +32,7 @@ class Order < ApplicationRecord
}
before_validation :set_expiry, on: :create
after_update :create_earnings_if_paid, if: -> { saved_change_to_status? && status == "paid" }
# === Instance Methods ===
@@ -159,7 +160,33 @@ class Order < ApplicationRecord
self.expires_at = DRAFT_EXPIRY_TIME.from_now if expires_at.blank?
end
def draft?
status == "draft"
def draft?
status == "draft"
end
def create_earnings_if_paid
return unless event.present? && user.present?
return if event.earnings.exists?(order_id: id)
event.earnings.create!(
user: user,
order: self,
amount_cents: promoter_payout_cents,
fee_cents: platform_fee_cents,
status: :pending
)
end
def create_earnings_if_paid
return unless event.present? && user.present?
return if event.earnings.exists?(order_id: id)
event.earnings.create!(
user: user,
order: self,
amount_cents: promoter_payout_cents,
fee_cents: platform_fee_cents,
status: :pending
)
end
end

View File

@@ -23,6 +23,7 @@ class User < ApplicationRecord
has_many :events, dependent: :destroy
has_many :tickets, dependent: :destroy
has_many :orders, dependent: :destroy
has_many :earnings, dependent: :destroy
# Validations - allow reasonable name lengths
validates :last_name, length: { minimum: 2, maximum: 50, allow_blank: true }
@@ -48,4 +49,21 @@ class User < ApplicationRecord
# Alias for can_manage_events? to make views more semantic
can_manage_events?
end
def name
[ first_name, last_name ].compact.join(" ").strip
end
# Stripe Connect methods
def stripe_account_id
stripe_connected_account_id
end
def has_stripe_account?
stripe_connected_account_id.present?
end
def can_receive_payouts?
has_stripe_account? && promoter?
end
end