feat(payouts): implement promoter earnings viewing, request flow, and admin Stripe processing with webhooks

Add model methods for accurate net calculations (€0.50 + 1.5% fees), eligibility, refund handling
Update promoter/payouts controller for index (pending events), create (eligibility checks)
Integrate admin processing via Stripe::Transfer, webhook for status sync
Enhance views: index pending cards, events/show preview/form
Add comprehensive tests (models, controllers, service, integration); run migrations
This commit is contained in:
kbe
2025-09-17 02:07:52 +02:00
parent 47f4f50e5b
commit 3c1e17c2af
31 changed files with 1096 additions and 148 deletions

View File

@@ -3,19 +3,25 @@ class Admin::PayoutsController < ApplicationController
before_action :ensure_admin!
def index
@payouts = Payout.includes(:event, :user)
.order(created_at: :desc)
.page(params[:page])
@payouts = Payout.pending.includes(:user, :event).order(created_at: :asc).page(params[:page])
end
def create
def show
@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
def process
@payout = Payout.find(params[:id])
if @payout.pending? && @payout.can_process?
begin
PayoutService.new(@payout).process!
redirect_to admin_payouts_path, notice: "Payout processed successfully."
rescue => e
redirect_to admin_payouts_path, alert: "Failed to process payout: #{e.message}"
end
else
redirect_to admin_payouts_path, alert: "Cannot process this payout."
end
end
@@ -28,4 +34,4 @@ class Admin::PayoutsController < ApplicationController
redirect_to dashboard_path, alert: "Access denied."
end
end
end
end

View File

@@ -1,54 +1,61 @@
class Promoter::PayoutsController < ApplicationController
before_action :authenticate_user!
before_action :ensure_promoter!
before_action :set_event, only: [:show, :create]
before_action :set_event, only: [ :create ]
# List all payouts for the current promoter
def index
@payouts = current_user.payouts
.includes(:event)
.order(created_at: :desc)
.page(params[:page])
@payouts = current_user.payouts.completed.order(created_at: :desc).page(params[:page])
@eligible_events = current_user.events.eligible_for_payout.includes(:earnings).limit(5)
@total_pending_net = @eligible_events.sum(&:net_earnings_cents)
@total_paid_out = current_user.payouts.completed.sum(&:net_amount_cents)
@total_pending = @total_pending_net
@total_payouts_count = current_user.payouts.count
end
# Show payout details
def show
@payout = @event.payouts.find(params[:id])
@payout = current_user.payouts.find(params[:id])
@event = @payout.event
end
# Create a new payout request
def create
# 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."
unless @event.can_request_payout?(current_user)
redirect_to event_path(@event.slug, @event), alert: "Payout cannot be requested for this event."
return
end
# Calculate payout amount
total_earnings_cents = @event.total_earnings_cents
total_fees_cents = @event.total_fees_cents
net_earnings_cents = @event.net_earnings_cents
# Calculate payout amount using model methods
gross = @event.total_gross_cents
fees = @event.total_fees_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
# Count orders using model scope
total_orders_count = @event.orders.paid.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
amount_cents: gross,
fee_cents: fees,
total_orders_count: total_orders_count
)
# refunded_orders_count will be set by model callback
if @payout.save
# Update event payout status
@event.update!(payout_status: :requested, payout_requested_at: Time.current)
# Log notification (mailer can be added later if needed)
Rails.logger.info "Payout request submitted: #{@payout.id} for event #{@event.id}"
redirect_to promoter_payout_path(@payout), notice: "Payout request submitted successfully."
else
redirect_to promoter_event_path(@event), alert: "Failed to submit payout request: #{@payout.errors.full_messages.join(', ')}"
flash.now[:alert] = "Failed to submit payout request: #{@payout.errors.full_messages.join(', ')}"
render "new"
end
end
@@ -63,4 +70,4 @@ class Promoter::PayoutsController < ApplicationController
def set_event
@event = current_user.events.find(params[:event_id])
end
end
end

View File

@@ -0,0 +1,34 @@
class Webhooks::StripeController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, ENV["STRIPE_WEBHOOK_SECRET"]
)
rescue Stripe::SignatureVerificationError => e
# Invalid signature
return head 400
end
case event["type"]
when "transfer.payout.succeeded"
payout_id = event.data.object.metadata["payout_id"]
payout = Payout.find(payout_id)
if payout && payout.processing?
payout.update!(status: :completed, stripe_payout_id: event.data.object.id)
end
when "transfer.payout.failed", "transfer.canceled"
payout_id = event.data.object.metadata["payout_id"]
payout = Payout.find(payout_id)
if payout
payout.update!(status: :failed)
end
end
head 200
end
end

View File

@@ -1,4 +1,22 @@
class Earning < ApplicationRecord
def self.create_from_order(order)
return unless order.paid? || order.completed?
gross_cents = order.tickets.active.sum(:price_cents)
fee_cents = order.tickets.active.sum do |ticket|
50 + (ticket.price_cents * 0.015).to_i
end
amount_cents = gross_cents - fee_cents
create!(
event: order.event,
user: order.event.user,
order: order,
amount_cents: amount_cents,
fee_cents: fee_cents,
status: :pending
)
end
# === Relations ===
belongs_to :event
belongs_to :user
@@ -13,4 +31,24 @@ class Earning < ApplicationRecord
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
# Recalculate earning based on active tickets in the order
def recalculate!
return unless order.present?
active_tickets = order.tickets.active
if active_tickets.empty?
update!(amount_cents: 0, fee_cents: 0)
else
gross_cents = active_tickets.sum(:price_cents)
fee_cents = active_tickets.sum do |ticket|
50 + (ticket.price_cents * 0.015).to_i
end
update!(amount_cents: gross_cents - fee_cents, fee_cents: fee_cents)
end
end
def recalculate_on_refund(order)
recalculate!
end
end

View File

@@ -66,24 +66,26 @@ class Event < ApplicationRecord
# Scope for published events ordered by start time
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
# Scope for events eligible for payout
scope :eligible_for_payout, -> { where("end_time <= ?", Time.current).joins(:earnings).group("events.id").having("SUM(earnings.amount_cents) > 0") }
# === Instance Methods ===
# Payout methods
def can_request_payout?
event_ended? && earnings.pending.any? && user.can_receive_payouts?
end
def total_earnings_cents
# Only count earnings from non-refunded tickets
earnings.pending.sum(:amount_cents)
def total_gross_cents
tickets.active.sum(:price_cents)
end
def total_fees_cents
(total_earnings_cents * 0.1).to_i # 10% platform fee
earnings.pending.sum(:fee_cents)
end
def net_earnings_cents
total_earnings_cents - total_fees_cents
total_gross_cents - total_fees_cents
end
def can_request_payout?(user = self.user)
event_ended? && (net_earnings_cents > 0) && user.is_professionnal? && payouts.pending.empty?
end
# Check if coordinates were successfully geocoded or are fallback coordinates

View File

@@ -3,6 +3,16 @@ class Order < ApplicationRecord
DRAFT_EXPIRY_TIME = 15.minutes
MAX_PAYMENT_ATTEMPTS = 3
# === Enums ===
enum :status, {
draft: 0,
pending_payment: 1,
paid: 2,
completed: 3,
cancelled: 4,
expired: 5
}, default: :draft
# === Associations ===
belongs_to :user
belongs_to :event
@@ -23,8 +33,9 @@ class Order < ApplicationRecord
attr_accessor :stripe_invoice_id
# === Scopes ===
scope :draft, -> { where(status: "draft") }
scope :active, -> { where(status: %w[paid completed]) }
scope :draft, -> { where(status: :draft) }
scope :active, -> { where(status: [ :paid, :completed ]) }
scope :paid, -> { where(status: :paid) }
scope :expired_drafts, -> { draft.where("expires_at < ?", Time.current) }
scope :can_retry_payment, -> {
draft.where("payment_attempts < ? AND expires_at > ?",

View File

@@ -12,18 +12,44 @@ class Payout < ApplicationRecord
}, default: :pending
# === Validations ===
validates :amount_cents, presence: true, numericality: { greater_than_or_equal_to: 0 }
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
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 :processing, -> { where(status: :processing) }
# === Callbacks ===
after_create :calculate_refunded_orders_count
# === Instance Methods ===
# Amount in euros (formatted)
@@ -56,4 +82,14 @@ class Payout < ApplicationRecord
service = PayoutService.new(self)
service.process!
end
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
end

View File

@@ -22,6 +22,8 @@ class Ticket < ApplicationRecord
before_validation :set_price_from_ticket_type, on: :create
before_validation :generate_qr_code, on: :create
after_update :recalculate_earning_if_refunded, if: :saved_change_to_status?
# Generate PDF ticket
def to_pdf
TicketPdfGenerator.new(self).generate
@@ -73,4 +75,12 @@ class Ticket < ApplicationRecord
def draft?
status == "draft"
end
private
def recalculate_earning_if_refunded
if status == "refunded"
order.earning&.recalculate!
end
end
end

View File

@@ -65,6 +65,19 @@ class User < ApplicationRecord
end
def can_receive_payouts?
has_stripe_account? && promoter?
stripe_connected_account_id.present? && stripe_connect_verified?
end
private
def stripe_connect_verified?
return false unless stripe_connected_account_id.present?
begin
account = Stripe::Account.retrieve(stripe_connected_account_id)
account.charges_enabled
rescue Stripe::StripeError => e
Rails.logger.error "Failed to verify Stripe account #{stripe_connected_account_id}: #{e.message}"
false
end
end
end

View File

@@ -8,23 +8,32 @@ class PayoutService
@payout.update!(status: :processing)
# Create Stripe payout
begin
stripe_payout = Stripe::Payout.create({
amount: @payout.amount_cents,
currency: 'eur',
destination: @payout.user.stripe_account_id,
description: "Payout for event: #{@payout.event.name}"
})
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.update!(
status: :completed,
stripe_payout_id: stripe_payout.id
stripe_payout_id: transfer.id
)
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
end
end
private
def update_earnings_status
@payout.event.earnings.where(status: 0).update_all(status: 1) # pending to paid
end
end

View File

@@ -0,0 +1,47 @@
<% if @event.can_request_payout? %>
<div class="space-y-4">
<h4 class="text-lg font-medium text-gray-900">Aperçu des Revenus</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<!-- Gross -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500">Revenus Bruts</p>
<p class="text-2xl font-bold text-gray-900">
<%= number_to_currency(@event.total_earnings_cents / 100.0, unit: '€', separator: ',', delimiter: '.') %>
</p>
</div>
<!-- Fees -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500">Frais Plateforme</p>
<p class="text-2xl font-bold text-gray-900">
-<%= number_to_currency(@event.total_fees_cents / 100.0, unit: '€', separator: ',', delimiter: '.') %>
</p>
</div>
<!-- Net -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500">Revenus Nets</p>
<p class="text-2xl font-bold text-gray-900">
<%= number_to_currency(@event.net_earnings_cents / 100.0, unit: '€', separator: ',', delimiter: '.') %>
</p>
</div>
</div>
<% if @event.payout.present? %>
<%= link_to "Voir les Détails du Paiement", promoter_payout_path(@event.payout),
class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50" %>
<% else %>
<%= form_with model: Payout.new, url: promoter_payouts_path, local: true, class: "inline-block" do |f| %>
<%= f.hidden_field :event_id, value: @event.id %>
<%= f.submit "Demander le Paiement Maintenant",
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500",
data: { confirm: "Êtes-vous sûr de vouloir demander un paiement de #{number_to_currency(@event.net_earnings_cents / 100.0, unit: '€', separator: ',', delimiter: '.') } ? Cette action ne peut pas être annulée." } %>
<% end %>
<% end %>
</div>
<% else %>
<div class="bg-yellow-50 border border-yellow-200 rounded-xl p-4">
<p class="text-sm text-yellow-800">Non éligible à la demande de paiement. L'événement n'est peut-être pas terminé ou le compte Stripe n'est pas vérifié.</p>
</div>
<% end %>

View File

@@ -277,7 +277,7 @@
<i data-lucide="ticket" class="w-4 h-4 mr-2"></i>
Gérer les types de billets
<% end %>
<% if @event.sold_out? %>
<%= button_to mark_available_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-blue-50 text-blue-700 font-medium text-sm rounded-lg hover:bg-blue-100 transition-colors duration-200" do %>
<i data-lucide="refresh-ccw" class="w-4 h-4 mr-2"></i>
@@ -289,76 +289,9 @@
Marquer comme complet
<% end %>
<% end %>
<!-- Payout section -->
<% if @event.event_ended? && @event.can_request_payout? %>
<hr class="border-gray-200">
<div class="space-y-4">
<h4 class="text-lg font-medium text-gray-900">Paiement des Revenus</h4>
<!-- Earnings Summary -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm text-gray-500">Revenus Bruts</p>
<p class="text-lg font-bold text-gray-900">€<%= @event.total_earnings_cents / 100.0 %></p>
</div>
<%= render 'earnings_preview' %>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm text-gray-500">Frais Plateforme</p>
<p class="text-lg font-bold text-gray-900">-€<%= @event.total_fees_cents / 100.0 %></p>
</div>
<div class="payout-summary-card">
<p class="payout-summary-label">Revenus Nets</p>
<p class="payout-summary-amount">€<%= @event.net_earnings_cents / 100.0 %></p>
</div>
</div>
<!-- Payout Status -->
<% if @event.payout_status != "not_requested" %>
<div class="bg-blue-50 rounded-lg p-4 border border-blue-200">
<div class="flex items-center">
<% case @event.payout_status %>
<% when "requested" %>
<i data-lucide="clock" class="w-5 h-5 text-blue-500 mr-2"></i>
<span class="font-medium text-blue-800">Paiement Demandé</span>
<% when "processing" %>
<i data-lucide="refresh-cw" class="w-5 h-5 text-blue-500 mr-2"></i>
<span class="font-medium text-blue-800">Paiement en Traitement</span>
<% when "completed" %>
<i data-lucide="check-circle" class="w-5 h-5 text-green-500 mr-2"></i>
<span class="font-medium text-green-800">Paiement Complété</span>
<% when "failed" %>
<i data-lucide="x-circle" class="w-5 h-5 text-red-500 mr-2"></i>
<span class="font-medium text-red-800">Paiement Échoué</span>
<% end %>
</div>
<p class="text-sm text-gray-600 mt-1">Votre demande de paiement est en cours de traitement. Vous recevrez un email quand elle sera terminée.</p>
</div>
<% end %>
<!-- Payout Action -->
<% 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 un paiement de €#{@event.net_earnings_cents / 100.0} ? Cette action ne peut pas être annulée." },
class: "payout-action-button primary" do %>
<i data-lucide="dollar-sign" class="w-5 h-5 mr-2"></i>
Demander le Paiement de €<%= @event.net_earnings_cents / 100.0 %>
<% end %>
<% elsif @event.payout_status == "failed" %>
<%= button_to promoter_payouts_path(event_id: @event.id), method: :post,
data: { confirm: "Êtes-vous sûr de vouloir demander un nouveau paiement de €#{@event.net_earnings_cents / 100.0} ?" },
class: "payout-action-button warning" do %>
<i data-lucide="refresh-ccw" class="w-5 h-5 mr-2"></i>
Réessayer le Paiement
<% end %>
<% else %>
<%= link_to "Voir les Détails du Paiement", promoter_payouts_path,
class: "payout-action-button secondary" %>
<% end %>
</div>
<% end %>
<hr class="border-gray-200">
<%= 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." },

View File

@@ -53,6 +53,65 @@
</div>
<% end %>
<!-- Pending Earnings Section -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-8">
<h2 class="text-lg font-medium text-gray-900 mb-4">Pending Earnings</h2>
<% if @total_pending_net && @total_pending_net > 0 %>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 rounded-lg">
<i data-lucide="dollar-sign" class="w-6 h-6 text-yellow-600"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500">Total Pending Net</p>
<p class="text-2xl font-bold text-gray-900">
<%= number_to_currency(@total_pending_net / 100.0, unit: '€', separator: ',', delimiter: '.') %>
</p>
</div>
</div>
</div>
</div>
<% end %>
<% if @eligible_events.present? && @eligible_events.any? %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @eligible_events.limit(5).each do |event| %>
<div class="bg-white p-6 rounded-lg shadow border border-gray-200">
<div class="flex items-center mb-3">
<div class="flex-shrink-0 h-10 w-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center">
<i data-lucide="calendar" class="h-5 w-5 text-white"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-bold text-gray-900"><%= event.name %></h3>
<p class="text-sm text-gray-500"><%= event.start_time.strftime("%d %b %Y") %></p>
</div>
</div>
<div class="space-y-2 text-sm">
<p><span class="font-medium">Gross:</span> <%= number_to_currency(event.total_earnings_cents / 100.0, unit: '€', separator: ',', delimiter: '.') %></p>
<p><span class="font-medium">Net:</span> <%= number_to_currency(event.net_earnings_cents / 100.0, unit: '€', separator: ',', delimiter: '.') %></p>
</div>
<%= link_to "Request Payout", promoter_event_path(event),
class: "mt-4 w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
</div>
<% end %>
</div>
<% if @eligible_events.size > 5 %>
<div class="text-center mt-4">
<%= link_to "View All Eligible Events", promoter_events_path, class: "text-indigo-600 hover:text-indigo-500 text-sm font-medium" %>
</div>
<% end %>
<% else %>
<div class="text-center py-12">
<i data-lucide="inbox" class="mx-auto h-12 w-12 text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 mb-2">No pending earnings</h3>
<p class="text-gray-500">Check your events to see if any are eligible for payout requests.</p>
<%= link_to "View My Events", promoter_events_path, class: "mt-4 inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50" %>
</div>
<% end %>
</div>
<!-- Payouts Table -->
<% if @payouts.any? %>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
@@ -139,4 +198,4 @@
<%= link_to "View My Events", promoter_events_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
</div>
<% end %>
</div>
</div>

View File

@@ -26,7 +26,7 @@
<p class="payout-status-step-label">Requested</p>
<p class="payout-status-step-date"><%= @payout.created_at.strftime("%b %d, %Y") %></p>
</div>
<div class="payout-status-step">
<div class="payout-status-step-icon <%= @payout.status == 'processing' ? 'processing' : (@payout.status == 'completed' || @payout.status == 'failed') ? 'completed' : 'incomplete' %>">
<% if @payout.status == 'processing' %>
@@ -39,7 +39,7 @@
</div>
<p class="payout-status-step-label">Processing</p>
</div>
<div class="payout-status-step">
<div class="payout-status-step-icon <%= @payout.status == 'completed' ? 'completed' : (@payout.status == 'failed' ? 'failed' : 'incomplete') %>">
<% if @payout.status == 'completed' %>
@@ -59,17 +59,23 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500">Gross Amount</p>
<p class="mt-1 text-2xl font-bold text-gray-900">€<%= @payout.amount_euros %></p>
<p class="mt-1 text-2xl font-bold text-gray-900">
<%= number_to_currency(@payout.amount_euros, unit: '€', separator: ',', delimiter: '.') %>
</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500">Platform Fees</p>
<p class="mt-1 text-2xl font-bold text-gray-900">-€<%= @payout.fee_euros %></p>
<p class="mt-1 text-2xl font-bold text-gray-900">
-<%= number_to_currency(@payout.fee_euros, unit: '€', separator: ',', delimiter: '.') %>
</p>
</div>
<div class="payout-summary-card">
<p class="payout-summary-label">Net Amount</p>
<p class="payout-summary-amount">€<%= @payout.net_amount_euros %></p>
<p class="payout-summary-amount">
<%= number_to_currency(@payout.net_amount_euros, unit: '€', separator: ',', delimiter: '.') %>
</p>
</div>
</div>
@@ -79,7 +85,7 @@
<h3 class="text-lg font-medium text-gray-900">Payout Information</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Details about this payout request</p>
</div>
<div class="divide-y divide-gray-200">
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Event</dt>
@@ -95,7 +101,7 @@
</div>
</dd>
</div>
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Status</dt>
<dd class="payout-detail-value">
@@ -123,37 +129,37 @@
<% end %>
</dd>
</div>
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Gross Amount</dt>
<dd class="payout-detail-value amount">€<%= @payout.amount_euros %></dd>
</div>
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Platform Fees</dt>
<dd class="payout-detail-value amount">-€<%= @payout.fee_euros %></dd>
</div>
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Net Amount</dt>
<dd class="payout-detail-value net-amount">€<%= @payout.net_amount_euros %></dd>
</div>
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Total Orders</dt>
<dd class="payout-detail-value"><%= @payout.total_orders_count %></dd>
</div>
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Refunded Orders</dt>
<dd class="payout-detail-value"><%= @payout.refunded_orders_count %></dd>
</div>
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Requested Date</dt>
<dd class="payout-detail-value"><%= @payout.created_at.strftime("%B %d, %Y at %I:%M %p") %></dd>
</div>
<% if @payout.stripe_payout_id.present? %>
<div class="px-4 py-5 sm:px-6 payout-detail-item">
<dt class="payout-detail-label">Stripe Payout ID</dt>
@@ -162,4 +168,4 @@
<% end %>
</div>
</div>
</div>
</div>