feat: implement payout system database schema and models
This commit is contained in:
13
app/controllers/admin/payouts_controller.rb
Normal file
13
app/controllers/admin/payouts_controller.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class Admin::PayoutsController < ApplicationController
|
||||||
|
def index
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
end
|
||||||
|
end
|
||||||
2
app/helpers/admin/payouts_helper.rb
Normal file
2
app/helpers/admin/payouts_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module Admin::PayoutsHelper
|
||||||
|
end
|
||||||
16
app/models/earning.rb
Normal file
16
app/models/earning.rb
Normal 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
|
||||||
@@ -16,16 +16,26 @@ class Event < ApplicationRecord
|
|||||||
sold_out: 3
|
sold_out: 3
|
||||||
}, default: :draft
|
}, default: :draft
|
||||||
|
|
||||||
|
enum :payout_status, {
|
||||||
|
not_requested: 0,
|
||||||
|
requested: 1,
|
||||||
|
processing: 2,
|
||||||
|
completed: 3,
|
||||||
|
failed: 4
|
||||||
|
}, default: :not_requested
|
||||||
|
|
||||||
# === Relations ===
|
# === Relations ===
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :ticket_types
|
has_many :ticket_types
|
||||||
has_many :tickets, through: :ticket_types
|
has_many :tickets, through: :ticket_types
|
||||||
has_many :orders
|
has_many :orders
|
||||||
|
has_many :earnings, dependent: :destroy
|
||||||
|
|
||||||
# === Callbacks ===
|
# === Callbacks ===
|
||||||
before_validation :geocode_address, if: :should_geocode_address?
|
before_validation :geocode_address, if: :should_geocode_address?
|
||||||
|
|
||||||
# Validations for Event attributes
|
# === Validations ===
|
||||||
|
|
||||||
# Basic information
|
# Basic information
|
||||||
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||||
validates :slug, 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 ===
|
# === 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
|
# Check if coordinates were successfully geocoded or are fallback coordinates
|
||||||
def geocoding_successful?
|
def geocoding_successful?
|
||||||
coordinates_look_valid?
|
coordinates_look_valid?
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class Order < ApplicationRecord
|
|||||||
}
|
}
|
||||||
|
|
||||||
before_validation :set_expiry, on: :create
|
before_validation :set_expiry, on: :create
|
||||||
|
after_update :create_earnings_if_paid, if: -> { saved_change_to_status? && status == "paid" }
|
||||||
|
|
||||||
# === Instance Methods ===
|
# === Instance Methods ===
|
||||||
|
|
||||||
@@ -162,4 +163,30 @@ class Order < ApplicationRecord
|
|||||||
def draft?
|
def draft?
|
||||||
status == "draft"
|
status == "draft"
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class User < ApplicationRecord
|
|||||||
has_many :events, dependent: :destroy
|
has_many :events, dependent: :destroy
|
||||||
has_many :tickets, dependent: :destroy
|
has_many :tickets, dependent: :destroy
|
||||||
has_many :orders, dependent: :destroy
|
has_many :orders, dependent: :destroy
|
||||||
|
has_many :earnings, dependent: :destroy
|
||||||
|
|
||||||
# Validations - allow reasonable name lengths
|
# Validations - allow reasonable name lengths
|
||||||
validates :last_name, length: { minimum: 2, maximum: 50, allow_blank: true }
|
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
|
# Alias for can_manage_events? to make views more semantic
|
||||||
can_manage_events?
|
can_manage_events?
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
92
app/services/payout_service.rb
Normal file
92
app/services/payout_service.rb
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
class PayoutService
|
||||||
|
def initialize(promoter_id = nil)
|
||||||
|
@promoter_id = promoter_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_pending_payouts
|
||||||
|
scope = Earnings.pending
|
||||||
|
scope = scope.where(user_id: @promoter_id) if @promoter_id.present?
|
||||||
|
|
||||||
|
scope.includes(:user, :order, :event).group_by(&:user_id).each do |user_id, earnings|
|
||||||
|
process_payout_for_user(user_id, earnings)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_event_payout(event)
|
||||||
|
return unless event.can_request_payout?
|
||||||
|
|
||||||
|
earnings = event.earnings.pending
|
||||||
|
total_cents = earnings.sum(:amount_cents)
|
||||||
|
fees_cents = event.total_fees_cents
|
||||||
|
net_cents = total_cents - fees_cents
|
||||||
|
|
||||||
|
return if net_cents <= 0
|
||||||
|
|
||||||
|
begin
|
||||||
|
event.update!(payout_status: :processing)
|
||||||
|
|
||||||
|
transfer = Stripe::Transfer.create(
|
||||||
|
amount: net_cents / 100,
|
||||||
|
currency: "eur",
|
||||||
|
destination: event.user.stripe_account_id,
|
||||||
|
description: "Payout for event: #{event.name}",
|
||||||
|
metadata: {
|
||||||
|
event_id: event.id,
|
||||||
|
promoter_id: event.user_id,
|
||||||
|
gross_amount: total_cents,
|
||||||
|
fees: fees_cents,
|
||||||
|
net_amount: net_cents
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
earnings.update_all(
|
||||||
|
status: :paid,
|
||||||
|
fee_cents: fees_cents,
|
||||||
|
net_amount_cents: net_cents,
|
||||||
|
stripe_payout_id: transfer.id
|
||||||
|
)
|
||||||
|
|
||||||
|
event.update!(
|
||||||
|
payout_status: :completed,
|
||||||
|
payout_requested_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
Rails.logger.info "Processed event payout #{transfer.id} for event #{event.id}: €#{net_cents / 100.0}"
|
||||||
|
rescue Stripe::StripeError => e
|
||||||
|
event.update!(payout_status: :failed)
|
||||||
|
Rails.logger.error "Payout failed for event #{event.id}: #{e.message}"
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_payout_for_user(user_id, earnings)
|
||||||
|
user = User.find(user_id)
|
||||||
|
return unless user.stripe_account_id.present?
|
||||||
|
|
||||||
|
total_amount_cents = earnings.sum(:amount_cents)
|
||||||
|
|
||||||
|
begin
|
||||||
|
transfer = Stripe::Transfer.create(
|
||||||
|
amount: total_amount_cents / 100,
|
||||||
|
currency: "eur",
|
||||||
|
destination: user.stripe_account_id,
|
||||||
|
description: "Payout for promoter #{user_id} - Total: €#{total_amount_cents / 100.0}",
|
||||||
|
metadata: {
|
||||||
|
promoter_id: user_id,
|
||||||
|
earnings_ids: earnings.map(&:id).join(",")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
earnings.update_all(
|
||||||
|
status: :paid,
|
||||||
|
stripe_payout_id: transfer.id
|
||||||
|
)
|
||||||
|
|
||||||
|
Rails.logger.info "Processed payout #{transfer.id} for promoter #{user_id}: €#{total_amount_cents / 100.0}"
|
||||||
|
rescue Stripe::StripeError => e
|
||||||
|
Rails.logger.error "Failed to process payout for promoter #{user_id}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
2
app/views/admin/payouts/create.html.erb
Normal file
2
app/views/admin/payouts/create.html.erb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<h1>Admin::Payouts#create</h1>
|
||||||
|
<p>Find me in app/views/admin/payouts/create.html.erb</p>
|
||||||
2
app/views/admin/payouts/index.html.erb
Normal file
2
app/views/admin/payouts/index.html.erb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<h1>Admin::Payouts#index</h1>
|
||||||
|
<p>Find me in app/views/admin/payouts/index.html.erb</p>
|
||||||
2
app/views/admin/payouts/new.html.erb
Normal file
2
app/views/admin/payouts/new.html.erb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<h1>Admin::Payouts#new</h1>
|
||||||
|
<p>Find me in app/views/admin/payouts/new.html.erb</p>
|
||||||
2
app/views/admin/payouts/show.html.erb
Normal file
2
app/views/admin/payouts/show.html.erb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<h1>Admin::Payouts#show</h1>
|
||||||
|
<p>Find me in app/views/admin/payouts/show.html.erb</p>
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
|
namespace :admin do
|
||||||
|
resources :payouts, only: [ :index, :create ]
|
||||||
|
end
|
||||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||||
|
|
||||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class DeviseCreateUsers < ActiveRecord::Migration[8.0]
|
|||||||
# Add onboarding check on user model
|
# Add onboarding check on user model
|
||||||
t.boolean :onboarding_completed, default: false, null: false
|
t.boolean :onboarding_completed, default: false, null: false
|
||||||
|
|
||||||
|
# add_column :users, :stripe_connected_account_id, :string
|
||||||
|
|
||||||
t.timestamps null: false
|
t.timestamps null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
16
db/migrate/20250916212717_create_earnings.rb
Normal file
16
db/migrate/20250916212717_create_earnings.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
class CreateEarnings < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :earnings do |t|
|
||||||
|
t.integer :amount_cents
|
||||||
|
t.integer :fee_cents
|
||||||
|
t.integer :status
|
||||||
|
t.string :stripe_payout_id
|
||||||
|
|
||||||
|
t.references :event, null: false, foreign_key: false, index: true
|
||||||
|
t.references :user, null: false, foreign_key: false, index: true
|
||||||
|
t.references :order, null: false, foreign_key: false, index: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
6
db/migrate/20250916215118_add_payout_fields_to_events.rb
Normal file
6
db/migrate/20250916215118_add_payout_fields_to_events.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class AddPayoutFieldsToEvents < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :events, :payout_requested_at, :datetime
|
||||||
|
add_column :events, :payout_status, :integer
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20250916215119_add_net_amount_to_earnings.rb
Normal file
5
db/migrate/20250916215119_add_net_amount_to_earnings.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddNetAmountToEarnings < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :earnings, :net_amount_cents, :integer
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
class AddIndexToStripeConnectedAccountIdOnUsers < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :stripe_connected_account_id_on_users, :stripe_connected_account_id, :string
|
||||||
|
add_index :stripe_connected_account_id_on_users, :stripe_connected_account_id
|
||||||
|
end
|
||||||
|
end
|
||||||
11
db/migrate/20250916215130_update_payout_status_on_events.rb
Normal file
11
db/migrate/20250916215130_update_payout_status_on_events.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
class UpdatePayoutStatusOnEvents < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
change_column_default :events, :payout_status, from: nil, to: 0
|
||||||
|
add_index :events, :payout_status
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
change_column_default :events, :payout_status, from: 0, to: nil
|
||||||
|
remove_index :events, :payout_status
|
||||||
|
end
|
||||||
|
end
|
||||||
23
db/schema.rb
generated
23
db/schema.rb
generated
@@ -10,7 +10,23 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_09_11_063815) do
|
ActiveRecord::Schema[8.0].define(version: 2025_09_16_215119) do
|
||||||
|
create_table "earnings", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||||
|
t.integer "amount_cents"
|
||||||
|
t.integer "fee_cents"
|
||||||
|
t.integer "status"
|
||||||
|
t.string "stripe_payout_id"
|
||||||
|
t.bigint "event_id", null: false
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.bigint "order_id", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.integer "net_amount_cents"
|
||||||
|
t.index ["event_id"], name: "index_earnings_on_event_id"
|
||||||
|
t.index ["order_id"], name: "index_earnings_on_order_id"
|
||||||
|
t.index ["user_id"], name: "index_earnings_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "slug", null: false
|
t.string "slug", null: false
|
||||||
@@ -25,9 +41,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_11_063815) do
|
|||||||
t.decimal "longitude", precision: 10, scale: 6, null: false
|
t.decimal "longitude", precision: 10, scale: 6, null: false
|
||||||
t.boolean "featured", default: false, null: false
|
t.boolean "featured", default: false, null: false
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
|
t.boolean "allow_booking_during_event", default: false, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.boolean "allow_booking_during_event", default: false, null: false
|
t.datetime "payout_requested_at"
|
||||||
|
t.integer "payout_status"
|
||||||
t.index ["featured"], name: "index_events_on_featured"
|
t.index ["featured"], name: "index_events_on_featured"
|
||||||
t.index ["latitude", "longitude"], name: "index_events_on_latitude_and_longitude"
|
t.index ["latitude", "longitude"], name: "index_events_on_latitude_and_longitude"
|
||||||
t.index ["state"], name: "index_events_on_state"
|
t.index ["state"], name: "index_events_on_state"
|
||||||
@@ -101,6 +119,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_11_063815) do
|
|||||||
t.boolean "onboarding_completed", default: false, null: false
|
t.boolean "onboarding_completed", default: false, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.string "stripe_connected_account_id"
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|||||||
24
docs/promoter-payouts.md
Normal file
24
docs/promoter-payouts.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Promoter Payouts Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
To handle promoter payouts in the Rails app (where promoters are users creating events), track all order payments in the database for auditing and fee calculation. Save payments (e.g., via Stripe webhooks) and apply platform fees per order processed—e.g., promoter gets 90% of ticket revenue minus your fee, stored in a new `earnings` table linked to events/orders.
|
||||||
|
|
||||||
|
## Recommended Architecture
|
||||||
|
|
||||||
|
### 1. Models & DB
|
||||||
|
- Add `has_many :earnings, dependent: :destroy` to `Event` and `User` models.
|
||||||
|
- Create `Earnings` model: `belongs_to :event, :user; fields: amount_cents (Decimal), fee_cents (Decimal), status (enum: pending/paid), stripe_payout_id (String), order_id (ref)`.
|
||||||
|
- On order payment success (in your Stripe webhook or after_create callback on Order), create Earnings record: `earnings = event.earnings.create!(amount_cents: total_revenue_cents * 0.9, fee_cents: total_revenue_cents * 0.1, status: :pending, order: order)`.
|
||||||
|
|
||||||
|
### 2. Payout Processing
|
||||||
|
- Use Stripe Connect (setup promoter Stripe accounts via `account_links` in user onboarding).
|
||||||
|
- Create a `PayoutService`: Batch pending earnings per promoter, transfer via `Stripe::Transfer.create` to their connected account, update status to `:paid`.
|
||||||
|
- Run via cron job (e.g., in `lib/tasks/payouts.rake`) or admin-triggered job.
|
||||||
|
|
||||||
|
### 3. Admin Dashboard for Due Payouts
|
||||||
|
- Add admin routes: `resources :admin, only: [] do; resources :payouts; end` in `config/routes.rb`.
|
||||||
|
- Controller: `Admin::PayoutsController` with `index` action querying `Earnings.pending.where(user_id: params[:promoter_id]).group_by(&:user).sum(:amount_cents)`.
|
||||||
|
- View: Table showing promoter name, total due, unpaid earnings list; button to trigger payout.
|
||||||
|
- Use Pundit or CanCanCan for admin-only access (add `is_admin?` to User).
|
||||||
|
|
||||||
|
This ensures transparency, scalability, and easy auditing. Start by migrating the Earnings model: `rails g model Earnings event:references user:references order:references amount_cents:decimal fee_cents:decimal status:integer stripe_payout_id:string`. Test with Stripe test mode.
|
||||||
7
lib/tasks/payouts.rake
Normal file
7
lib/tasks/payouts.rake
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace :payouts do
|
||||||
|
desc "Process all pending promoter payouts"
|
||||||
|
task process: :environment do
|
||||||
|
PayoutService.new.process_pending_payouts
|
||||||
|
puts "Pending payouts processed."
|
||||||
|
end
|
||||||
|
end
|
||||||
19
test/fixtures/earnings.yml
vendored
Normal file
19
test/fixtures/earnings.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
one:
|
||||||
|
event: concert_event
|
||||||
|
user: users(one)
|
||||||
|
order: paid_order
|
||||||
|
amount_cents: 9000 # €90.00
|
||||||
|
fee_cents: 1000 # €10.00
|
||||||
|
status: pending
|
||||||
|
stripe_payout_id:
|
||||||
|
|
||||||
|
two:
|
||||||
|
event: winter_gala
|
||||||
|
user: users(two)
|
||||||
|
order: expired_order
|
||||||
|
amount_cents: 4500 # €45.00
|
||||||
|
fee_cents: 500 # €5.00
|
||||||
|
status: paid
|
||||||
|
stripe_payout_id: payout_123
|
||||||
86
test/models/earning_test.rb
Normal file
86
test/models/earning_test.rb
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class EarningTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@user = users(:one) || User.create!(email: "test@example.com", password: "password")
|
||||||
|
@event = events(:concert_event) || Event.create!(name: "Test Event", slug: "test-event", description: "Description", venue_name: "Venue", venue_address: "Address", latitude: 48.8566, longitude: 2.3522, start_time: Time.current, user: @user)
|
||||||
|
@order = orders(:paid_order) || Order.create!(user: @user, event: @event, status: "paid", total_amount_cents: 10000)
|
||||||
|
@earning = Earning.create!(event: @event, user: @user, order: @order, amount_cents: 9000, fee_cents: 1000, status: :pending)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "valid earning" do
|
||||||
|
assert @earning.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "amount_cents must be present and non-negative" do
|
||||||
|
@earning.amount_cents = nil
|
||||||
|
assert_not @earning.valid?
|
||||||
|
assert_includes @earning.errors[:amount_cents], "can't be blank"
|
||||||
|
|
||||||
|
@earning.amount_cents = -1
|
||||||
|
assert_not @earning.valid?
|
||||||
|
assert_includes @earning.errors[:amount_cents], "must be greater than or equal to 0"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fee_cents must be present and non-negative" do
|
||||||
|
@earning.fee_cents = nil
|
||||||
|
assert_not @earning.valid?
|
||||||
|
assert_includes @earning.errors[:fee_cents], "can't be blank"
|
||||||
|
|
||||||
|
@earning.fee_cents = -1
|
||||||
|
assert_not @earning.valid?
|
||||||
|
assert_includes @earning.errors[:fee_cents], "must be greater than or equal to 0"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "status must be present" do
|
||||||
|
@earning.status = nil
|
||||||
|
assert_not @earning.valid?
|
||||||
|
assert_includes @earning.errors[:status], "can't be blank"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stripe_payout_id must be unique if present" do
|
||||||
|
@earning.stripe_payout_id = "test_payout"
|
||||||
|
@earning.save!
|
||||||
|
|
||||||
|
duplicate = @earning.dup
|
||||||
|
duplicate.stripe_payout_id = "test_payout"
|
||||||
|
|
||||||
|
assert_not duplicate.valid?
|
||||||
|
assert_includes duplicate.errors[:stripe_payout_id], "has already been taken"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "belongs to associations" do
|
||||||
|
assert_instance_of Event, @earning.event
|
||||||
|
assert_instance_of User, @earning.user
|
||||||
|
assert_instance_of Order, @earning.order
|
||||||
|
end
|
||||||
|
|
||||||
|
test "status enum" do
|
||||||
|
assert_equal 0, Earning.statuses[:pending]
|
||||||
|
assert_equal 1, Earning.statuses[:paid]
|
||||||
|
|
||||||
|
assert @earning.pending?
|
||||||
|
assert_not @earning.paid?
|
||||||
|
|
||||||
|
@earning.status = :paid
|
||||||
|
@earning.save!
|
||||||
|
assert @earning.paid?
|
||||||
|
assert_not @earning.pending?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "pending scope from enum" do
|
||||||
|
pending_earning = Earning.create!(event: @event, user: @user, order: @order, amount_cents: 9000, fee_cents: 1000, status: :pending)
|
||||||
|
paid_earning = Earning.create!(event: @event, user: @user, order: @order, amount_cents: 4500, fee_cents: 500, status: :paid)
|
||||||
|
|
||||||
|
assert_includes Earning.pending, pending_earning
|
||||||
|
assert_not_includes Earning.pending, paid_earning
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid scope from enum" do
|
||||||
|
pending_earning = Earning.create!(event: @event, user: @user, order: @order, amount_cents: 9000, fee_cents: 1000, status: :pending)
|
||||||
|
paid_earning = Earning.create!(event: @event, user: @user, order: @order, amount_cents: 4500, fee_cents: 500, status: :paid)
|
||||||
|
|
||||||
|
assert_not_includes Earning.paid, pending_earning
|
||||||
|
assert_includes Earning.paid, paid_earning
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user