diff --git a/BACKLOG.md b/BACKLOG.md index abe8eab..9d04cd8 100755 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -17,7 +17,6 @@ - [ ] feat: Dynamic pricing based on demand - [ ] feat: Profesionnal account. User can ask to change from a customer to a professionnal account to create and manage events. - [ ] feat: User can choose to create a professionnal account on sign-up page to be allowed to create and manage events -- [ ] feat: Payout system for promoters (automated/manual payment processing) - [ ] feat: Platform commission tracking and fee structure display - [ ] feat: Tax reporting and revenue export for promoters - [ ] feat: Event update notifications to ticket holders @@ -45,14 +44,9 @@ - [ ] feat: Event recommendations system - [ ] feat: Invitation link. As organizer or promoter, you can invite people - -### Design & Infrastructure - -- [ ] style: Rewrite design system -- [ ] refactor: Rewrite design mockup - ## 🚧 Doing +- [x] feat: Payout system for promoters (automated/manual payment processing) - [ ] feat: Page to display all tickets for an event - [ ] feat: Add a link into notification email to order page that display all tickets @@ -68,3 +62,5 @@ - [x] feat: Ticket inventory management and capacity limits - [x] feat: Event discovery with search and filtering - [x] feat: Email notifications (purchase confirmations, event reminders) +- [x] style: Rewrite design system +- [x] refactor: Rewrite design mockup diff --git a/app/controllers/admin/payouts_controller.rb b/app/controllers/admin/payouts_controller.rb index c28d6b8..5e903df 100644 --- a/app/controllers/admin/payouts_controller.rb +++ b/app/controllers/admin/payouts_controller.rb @@ -1,13 +1,31 @@ class Admin::PayoutsController < ApplicationController + before_action :authenticate_user! + before_action :ensure_admin! + def index - end - - def show - end - - def new + @payouts = Payout.includes(:event, :user) + .order(created_at: :desc) + .page(params[:page]) end def create + @payout = Payout.find(params[:id]) + + begin + @payout.process_payout! + redirect_to admin_payouts_path, notice: "Payout processed successfully." + rescue => e + redirect_to admin_payouts_path, alert: "Failed to process payout: #{e.message}" + end end -end + + private + + def ensure_admin! + # For now, we'll just check if the user has a stripe account + # In a real app, you'd have an admin role check + unless current_user.has_stripe_account? + redirect_to dashboard_path, alert: "Access denied." + end + end +end \ No newline at end of file diff --git a/app/controllers/promoter/payouts_controller.rb b/app/controllers/promoter/payouts_controller.rb index 0539d80..63d8d26 100644 --- a/app/controllers/promoter/payouts_controller.rb +++ b/app/controllers/promoter/payouts_controller.rb @@ -1,32 +1,66 @@ class Promoter::PayoutsController < ApplicationController before_action :authenticate_user! - before_action :ensure_promoter + before_action :ensure_promoter! + before_action :set_event, only: [:show, :create] + # List all payouts for the current promoter def index - @events = current_user.events.includes(:earnings).order(start_time: :desc) + @payouts = current_user.payouts + .includes(:event) + .order(created_at: :desc) + .page(params[:page]) end + # Show payout details def show - @event = current_user.events.find(params[:id]) - @earnings = @event.earnings + @payout = @event.payouts.find(params[:id]) end + # Create a new payout request def create - @event = current_user.events.find(params[:event_id] || params[:id]) + # Check if event can request payout + unless @event.can_request_payout? + redirect_to promoter_event_path(@event), alert: "Payout cannot be requested for this event." + return + end - if @event.can_request_payout? - PayoutService.new.process_event_payout(@event) - redirect_to promoter_payout_path(@event), notice: "Payout requested successfully. It will be processed shortly." + # Calculate payout amount + total_earnings_cents = @event.total_earnings_cents + total_fees_cents = @event.total_fees_cents + net_earnings_cents = @event.net_earnings_cents + + # Count orders + total_orders_count = @event.orders.where(status: ['paid', 'completed']).count + refunded_orders_count = @event.tickets.where(status: 'refunded').joins(:order).where(orders: {status: ['paid', 'completed']}).count + + # Create payout record + @payout = @event.payouts.build( + user: current_user, + amount_cents: total_earnings_cents, + fee_cents: total_fees_cents, + total_orders_count: total_orders_count, + refunded_orders_count: refunded_orders_count + ) + + if @payout.save + # Update event payout status + @event.update!(payout_status: :requested, payout_requested_at: Time.current) + + redirect_to promoter_payout_path(@payout), notice: "Payout request submitted successfully." else - redirect_to promoter_payouts_path, alert: "Cannot request payout: #{@event.can_request_payout? ? '' : 'Event not eligible for payout.'}" + redirect_to promoter_event_path(@event), alert: "Failed to submit payout request: #{@payout.errors.full_messages.join(', ')}" end end private - def ensure_promoter + def ensure_promoter! unless current_user.promoter? - redirect_to dashboard_path, alert: "Access denied. Promoter account required." + redirect_to dashboard_path, alert: "Access denied." end end -end + + def set_event + @event = current_user.events.find(params[:event_id]) + end +end \ No newline at end of file diff --git a/app/models/event.rb b/app/models/event.rb index 46090e2..6b31856 100755 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -30,6 +30,7 @@ class Event < ApplicationRecord has_many :tickets, through: :ticket_types has_many :orders has_many :earnings, dependent: :destroy + has_many :payouts, dependent: :destroy # === Callbacks === before_validation :geocode_address, if: :should_geocode_address? @@ -73,6 +74,7 @@ class Event < ApplicationRecord end def total_earnings_cents + # Only count earnings from non-refunded tickets earnings.pending.sum(:amount_cents) end diff --git a/app/models/order.rb b/app/models/order.rb index d8ab2c5..db4037e 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -160,22 +160,9 @@ class Order < ApplicationRecord self.expires_at = DRAFT_EXPIRY_TIME.from_now if expires_at.blank? end -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 draft? + status == "draft" + end def create_earnings_if_paid return unless event.present? && user.present? diff --git a/app/models/payout.rb b/app/models/payout.rb new file mode 100644 index 0000000..3cca8f3 --- /dev/null +++ b/app/models/payout.rb @@ -0,0 +1,59 @@ +class Payout < ApplicationRecord + # === Relations === + belongs_to :user + belongs_to :event + + # === Enums === + enum :status, { + pending: 0, # Payout requested but not processed + processing: 1, # Payout being processed + completed: 2, # Payout successfully completed + failed: 3 # Payout failed + }, default: :pending + + # === 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 :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 + + # === Scopes === + scope :completed, -> { where(status: :completed) } + scope :pending, -> { where(status: :pending) } + scope :processing, -> { where(status: :processing) } + + # === 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 processed + def can_process? + pending? && amount_cents > 0 + end + + # Process the payout through Stripe + def process_payout! + service = PayoutService.new(self) + service.process! + end +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 2f9590b..b067037 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,6 +24,7 @@ class User < ApplicationRecord has_many :tickets, dependent: :destroy has_many :orders, dependent: :destroy has_many :earnings, dependent: :destroy + has_many :payouts, dependent: :destroy # Validations - allow reasonable name lengths validates :last_name, length: { minimum: 2, maximum: 50, allow_blank: true } diff --git a/app/services/payout_service.rb b/app/services/payout_service.rb index 46af2f1..d407eb8 100644 --- a/app/services/payout_service.rb +++ b/app/services/payout_service.rb @@ -1,92 +1,30 @@ class PayoutService - def initialize(promoter_id = nil) - @promoter_id = promoter_id + def initialize(payout) + @payout = payout end - def process_pending_payouts - scope = Earnings.pending - scope = scope.where(user_id: @promoter_id) if @promoter_id.present? + def process! + return unless @payout.can_process? - 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 + @payout.update!(status: :processing) + # Create Stripe payout begin - event.update!(payout_status: :processing) + stripe_payout = Stripe::Payout.create({ + amount: @payout.amount_cents, + currency: 'eur', + destination: @payout.user.stripe_account_id, + description: "Payout for event: #{@payout.event.name}" + }) - 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 - } + @payout.update!( + status: :completed, + stripe_payout_id: stripe_payout.id ) - - 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}" + @payout.update!(status: :failed) + Rails.logger.error "Stripe payout failed for payout #{@payout.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 +end \ No newline at end of file diff --git a/app/views/admin/payouts/index.html.erb b/app/views/admin/payouts/index.html.erb index 3081bde..6bfde45 100644 --- a/app/views/admin/payouts/index.html.erb +++ b/app/views/admin/payouts/index.html.erb @@ -1,2 +1,78 @@ -

Admin::Payouts#index

-

Find me in app/views/admin/payouts/index.html.erb

+
+
+

Admin Payouts

+
+ + <% if @payouts.any? %> +
+ + + + + + + + + + + + + <% @payouts.each do |payout| %> + + + + + + + + + <% end %> + +
EventPromoterAmountStatusDateActions
+
<%= payout.event.name %>
+
+
<%= payout.user.name.presence || payout.user.email %>
+
+
€<%= payout.amount_euros %>
+
Net: €<%= payout.net_amount_euros %> (Fee: €<%= payout.fee_euros %>)
+
+ <% case payout.status %> + <% when 'pending' %> + + Pending + + <% when 'processing' %> + + Processing + + <% when 'completed' %> + + Completed + + <% when 'failed' %> + + Failed + + <% end %> + + <%= payout.created_at.strftime("%b %d, %Y") %> + + <% if payout.can_process? %> + <%= 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 %> + <%= link_to "View", promoter_payout_path(payout), class: "text-indigo-600 hover:text-indigo-900 ml-2" %> +
+
+ + <% if @payouts.respond_to?(:total_pages) %> +
+ <%= paginate @payouts %> +
+ <% end %> + <% else %> +
+

No payouts found.

+
+ <% end %> +
\ No newline at end of file diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb index 39e0612..9799c13 100755 --- a/app/views/pages/dashboard.html.erb +++ b/app/views/pages/dashboard.html.erb @@ -151,6 +151,15 @@ <%= link_to promoter_event_path(event), class: "text-purple-600 hover:text-purple-800 text-xs font-medium" do %> Gérer → <% end %> + <% if event.event_ended? && event.can_request_payout? %> + <% if event.payout_status == "not_requested" %> + <%= link_to "Demander le paiement", promoter_payouts_path(event_id: event.id), method: :post, + class: "text-green-600 hover:text-green-800 text-xs font-medium" %> + <% else %> + <%= link_to "Voir le paiement", promoter_payouts_path, + class: "text-gray-600 hover:text-gray-800 text-xs font-medium" %> + <% end %> + <% end %> <% end %> @@ -165,41 +174,46 @@ - - <% if @recent_orders.any? %> -
-
-

Commandes Récentes

-

Dernières commandes pour vos événements

+ +
+
+

Recent Orders

+ <%= link_to "View All Payouts", promoter_payouts_path, class: "text-sm font-medium text-indigo-600 hover:text-indigo-500" if current_user.promoter? %>
-
-
- - - - - - - - - - - - <% @recent_orders.each do |order| %> - - - - - - - - <% end %> - -
ÉvénementClientBilletsMontantDate
<%= order.event.name %><%= order.user.email %><%= order.tickets.count %>€<%= order.total_amount_euros %><%= order.created_at.strftime("%d/%m/%Y") %>
+ <% if @recent_orders.any? %> +
+
    + <% @recent_orders.each do |order| %> +
  • +
    +
    +
    +
    +

    <%= order.event.name %>

    +

    for <%= order.user.name.presence || order.user.email %>

    +
    +
    +
    + + + + <%= order.created_at.strftime("%B %d, %Y") %> +
    +
    +
    +
    +

    €<%= order.total_amount_euros %>

    +
    +
    +
    +
  • + <% end %> +
-
+ <% else %> +

No recent orders.

+ <% end %>
- <% end %> <% end %> diff --git a/app/views/promoter/events/index.html.erb b/app/views/promoter/events/index.html.erb index c90571c..44204f2 100644 --- a/app/views/promoter/events/index.html.erb +++ b/app/views/promoter/events/index.html.erb @@ -84,6 +84,36 @@ À la une <% end %> + + <% if event.event_ended? && event.can_request_payout? %> + <% case event.payout_status %> + <% when "not_requested" %> + + + Paiement disponible + + <% when "requested" %> + + + Paiement demandé + + <% when "processing" %> + + + Paiement en cours + + <% when "completed" %> + + + Paiement effectué + + <% when "failed" %> + + + Paiement échoué + + <% end %> + <% end %> <% if event.start_time %> diff --git a/app/views/promoter/events/show.html.erb b/app/views/promoter/events/show.html.erb index ce0b4a1..f5e9b48 100644 --- a/app/views/promoter/events/show.html.erb +++ b/app/views/promoter/events/show.html.erb @@ -290,6 +290,53 @@ <% end %> <% end %> + + <% if @event.event_ended? && @event.can_request_payout? %> +
+
+

Paiement des revenus

+
+

Revenus disponibles : €<%= @event.net_earnings_cents / 100.0 %>

+

Frais de plateforme : €<%= @event.total_fees_cents / 100.0 %>

+
+ + <% if @event.payout_status == "not_requested" %> + <%= button_to promoter_payouts_path(event_id: @event.id), method: :post, + data: { confirm: "Êtes-vous sûr de vouloir demander le paiement des revenus ?" }, + class: "w-full inline-flex items-center justify-center px-4 py-3 bg-green-600 text-white font-medium text-sm rounded-lg hover:bg-green-700 transition-colors duration-200" do %> + + Demander le paiement + <% end %> + <% elsif @event.payout_status == "requested" %> +
+ + Paiement demandé +
+ <% elsif @event.payout_status == "processing" %> +
+ + Paiement en cours +
+ <% elsif @event.payout_status == "completed" %> +
+ + Paiement effectué +
+ <% elsif @event.payout_status == "failed" %> +
+ + Paiement échoué +
+ <%= button_to promoter_payouts_path(event_id: @event.id), method: :post, + data: { confirm: "Êtes-vous sûr de vouloir redemander le paiement des revenus ?" }, + class: "w-full inline-flex items-center justify-center px-4 py-3 bg-green-600 text-white font-medium text-sm rounded-lg hover:bg-green-700 transition-colors duration-200" do %> + + Redemander le paiement + <% end %> + <% end %> +
+ <% end %> +
<%= button_to promoter_event_path(@event), method: :delete, data: { confirm: "Êtes-vous sûr de vouloir supprimer cet événement ? Cette action est irréversible." }, diff --git a/app/views/promoter/payouts/index.html.erb b/app/views/promoter/payouts/index.html.erb index de1ea55..514347c 100644 --- a/app/views/promoter/payouts/index.html.erb +++ b/app/views/promoter/payouts/index.html.erb @@ -1,39 +1,70 @@
-

My Payouts

+
+

Payouts

+
- <% if @events.any? %> -
- <% @events.each do |event| %> -
-

<%= event.name %>

-

Date: <%= event.start_time.strftime('%B %d, %Y at %I:%M %p') %>

-

Status: <%= event.payout_status.humanize %>

- - <% if event.earnings.pending.any? %> -
-

Gross: €<%= (event.total_earnings_cents / 100.0).round(2) %>

-

Fees (10%): €<%= (event.total_fees_cents / 100.0).round(2) %>

-

Net: €<%= (event.net_earnings_cents / 100.0).round(2) %>

-
- - <% if event.can_request_payout? %> - <%= button_to "Request Payout", promoter_payouts_path(event_id: event.id), - method: :post, - class: "w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" %> - <% else %> -

Payout not available yet

- <% end %> - <% else %> -

No pending earnings

+ <% if @payouts.any? %> +
+ + + + + + + + + + + + <% @payouts.each do |payout| %> + + + + + + + <% end %> - - <% end %> + +
EventAmountStatusDateActions
+
<%= payout.event&.name || "Event not found" %>
+
+
€<%= payout.amount_euros %>
+
Net: €<%= payout.net_amount_euros %> (Fee: €<%= payout.fee_euros %>)
+
+ <% case payout.status %> + <% when 'pending' %> + + Pending + + <% when 'processing' %> + + Processing + + <% when 'completed' %> + + Completed + + <% when 'failed' %> + + Failed + + <% end %> + + <%= payout.created_at.strftime("%b %d, %Y") %> + + <%= link_to "View", promoter_payout_path(payout), class: "text-indigo-600 hover:text-indigo-900" %> +
+ + <% if @payouts.respond_to?(:total_pages) %> +
+ <%= paginate @payouts %> +
+ <% end %> <% else %> -
-

No events found

-

You haven't created any events yet.

- <%= link_to "Create Event", new_promoter_event_path, class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %> +
+

No payouts found.

<% end %> -
+
\ No newline at end of file diff --git a/app/views/promoter/payouts/show.html.erb b/app/views/promoter/payouts/show.html.erb index 47db689..b9ba275 100644 --- a/app/views/promoter/payouts/show.html.erb +++ b/app/views/promoter/payouts/show.html.erb @@ -1,58 +1,74 @@
-

<%= @event.name %> - Payout Details

- -
-

Event Summary

-

Date: <%= @event.start_time.strftime('%B %d, %Y at %I:%M %p') %>

-

Venue: <%= @event.venue_name %>

-

Payout Status: <%= @event.payout_status.humanize %>

- <% if @event.payout_requested_at %> -

Requested: <%= @event.payout_requested_at.strftime('%B %d, %Y at %I:%M %p') %>

- <% end %> +
+

Payout Details

+ <%= link_to "Back to Payouts", promoter_payouts_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
- <% if @earnings.any? %> -
-

Earnings Breakdown

-
- - - - - - - - - - - - <% @earnings.each do |earning| %> - - - - - - - +
+
+

Payout Information

+

Details about this payout request.

+
+
+
+
+
Event
+
<%= @payout.event&.name || "Event not found" %>
+
+
+
Gross Amount
+
€<%= @payout.amount_euros %>
+
+
+
Platform Fees
+
€<%= @payout.fee_euros %>
+
+
+
Net Amount
+
€<%= @payout.net_amount_euros %>
+
+
+
Status
+
+ <% case @payout.status %> + <% when 'pending' %> + + Pending + + <% when 'processing' %> + + Processing + + <% when 'completed' %> + + Completed + + <% when 'failed' %> + + Failed + <% end %> -
-
OrderGross AmountFeeNet AmountStatus
#<%= earning.order_id %>€<%= (earning.amount_cents / 100.0).round(2) %>€<%= (earning.fee_cents / 100.0).round(2) %>€<%= (earning.net_amount_cents / 100.0).round(2) %> - - <%= earning.status.humanize %> - -
-
- -
-

Total Summary

-

Gross Total: €<%= (@earnings.sum(:amount_cents) / 100.0).round(2) %>

-

Total Fees: €<%= (@earnings.sum(:fee_cents) / 100.0).round(2) %>

-

Net Total: €<%= (@earnings.sum(:net_amount_cents) / 100.0).round(2) %>

-
+ +
+
+
Total Orders
+
<%= @payout.total_orders_count %>
+
+
+
Refunded Orders
+
<%= @payout.refunded_orders_count %>
+
+
+
Requested Date
+
<%= @payout.created_at.strftime("%B %d, %Y at %I:%M %p") %>
+
+ <% if @payout.stripe_payout_id.present? %> +
+
Stripe Payout ID
+
<%= @payout.stripe_payout_id %>
+
+ <% end %> +
- <% else %> -
-

No earnings recorded for this event.

-
- <% end %> -
+
+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index ea7db4a..65ff184 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -78,9 +78,7 @@ Rails.application.routes.draw do # === Promoter Routes === namespace :promoter do - get "payouts/index" - get "payouts/show" - get "payouts/create" + resources :payouts, only: [:index, :show, :create] resources :events do member do patch :publish diff --git a/db/migrate/20250916220259_fix_payout_system_defaults_and_indexes.rb b/db/migrate/20250916220259_fix_payout_system_defaults_and_indexes.rb deleted file mode 100644 index d21273a..0000000 --- a/db/migrate/20250916220259_fix_payout_system_defaults_and_indexes.rb +++ /dev/null @@ -1,4 +0,0 @@ -class FixPayoutSystemDefaultsAndIndexes < ActiveRecord::Migration[8.0] - def change - end -end diff --git a/db/migrate/20250916221454_create_payouts.rb b/db/migrate/20250916221454_create_payouts.rb new file mode 100644 index 0000000..7c8ea52 --- /dev/null +++ b/db/migrate/20250916221454_create_payouts.rb @@ -0,0 +1,19 @@ +class CreatePayouts < ActiveRecord::Migration[8.0] + def change + create_table :payouts do |t| + t.references :user, null: false, foreign_key: true + t.references :event, null: false, foreign_key: true + t.integer :amount_cents, null: false + t.integer :fee_cents, null: false, default: 0 + t.integer :status, null: false, default: 0 + t.string :stripe_payout_id + t.integer :total_orders_count, null: false, default: 0 + t.integer :refunded_orders_count, null: false, default: 0 + + t.timestamps + end + + add_index :payouts, :status + add_index :payouts, :stripe_payout_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 3cc8abe..8f7144d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_09_16_220259) do +ActiveRecord::Schema[8.0].define(version: 2025_09_16_221454) do create_table "earnings", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t| t.integer "amount_cents" t.integer "fee_cents" @@ -70,6 +70,23 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_16_220259) do t.index ["user_id"], name: "index_orders_on_user_id" end + create_table "payouts", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "event_id", null: false + t.integer "amount_cents", null: false + t.integer "fee_cents", default: 0, null: false + t.integer "status", default: 0, null: false + t.string "stripe_payout_id" + t.integer "total_orders_count", default: 0, null: false + t.integer "refunded_orders_count", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["event_id"], name: "index_payouts_on_event_id" + t.index ["status"], name: "index_payouts_on_status" + t.index ["stripe_payout_id"], name: "index_payouts_on_stripe_payout_id", unique: true + t.index ["user_id"], name: "index_payouts_on_user_id" + end + create_table "ticket_types", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t| t.string "name" t.text "description" @@ -125,4 +142,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_16_220259) do t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["stripe_connected_account_id"], name: "index_users_on_stripe_connected_account_id", unique: true end + + add_foreign_key "payouts", "events" + add_foreign_key "payouts", "users" end diff --git a/test/controllers/promoter/payouts_controller_test.rb b/test/controllers/promoter/payouts_controller_test.rb index fd66a2b..e95675c 100644 --- a/test/controllers/promoter/payouts_controller_test.rb +++ b/test/controllers/promoter/payouts_controller_test.rb @@ -1,18 +1,54 @@ require "test_helper" class Promoter::PayoutsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:one) + @event = events(:concert_event) + @payout = payouts(:one) + end + test "should get index" do - get promoter_payouts_index_url + sign_in @user + # Make the user a promoter + @user.update(is_professionnal: true) + get promoter_payouts_url assert_response :success end test "should get show" do - get promoter_payouts_show_url + sign_in @user + # Make the user a promoter + @user.update(is_professionnal: true) + # Create a payout that belongs to the user + payout = Payout.create!( + user: @user, + event: @event, + amount_cents: 1000, + fee_cents: 100 + ) + get promoter_payout_url(payout) assert_response :success end - test "should get create" do - get promoter_payouts_create_url - assert_response :success + test "should create payout" do + sign_in @user + # Make the user a promoter + @user.update(is_professionnal: true) + # Make the user the owner of the event + @event.update(user: @user) + # Make the event end in the past + @event.update(end_time: 1.day.ago) + # Create some earnings for the event + @event.earnings.create!( + user: @user, + order: orders(:paid_order), + amount_cents: 1000, + fee_cents: 100, + status: :pending + ) + assert_difference('Payout.count', 1) do + post promoter_payouts_url, params: { event_id: @event.id } + end + assert_redirected_to promoter_payout_path(Payout.last) end end diff --git a/test/fixtures/payouts.yml b/test/fixtures/payouts.yml new file mode 100644 index 0000000..7488fca --- /dev/null +++ b/test/fixtures/payouts.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + user: one + event: one + amount_cents: 10000 + fee_cents: 1000 + status: pending + +two: + user: two + event: two + amount_cents: 20000 + fee_cents: 2000 + status: completed \ No newline at end of file