I added the features for users to use promotion code and for promoters to create on their events. May be rewrite to discount code?
192 lines
5.3 KiB
Ruby
192 lines
5.3 KiB
Ruby
class Order < ApplicationRecord
|
|
# === Constants ===
|
|
DRAFT_EXPIRY_TIME = 15.minutes
|
|
MAX_PAYMENT_ATTEMPTS = 3
|
|
|
|
# === Associations ===
|
|
belongs_to :user
|
|
belongs_to :event
|
|
has_many :tickets, dependent: :destroy
|
|
has_many :order_promotion_codes, dependent: :destroy
|
|
has_many :promotion_codes, through: :order_promotion_codes
|
|
|
|
# === Validations ===
|
|
validates :user_id, presence: true
|
|
validates :event_id, presence: true
|
|
validates :status, presence: true, inclusion: {
|
|
in: %w[draft pending_payment paid completed cancelled expired]
|
|
}
|
|
validates :total_amount_cents, presence: true,
|
|
numericality: { greater_than_or_equal_to: 0 }
|
|
validates :payment_attempts, presence: true,
|
|
numericality: { greater_than_or_equal_to: 0 }
|
|
|
|
# Stripe invoice ID for accounting records
|
|
attr_accessor :stripe_invoice_id
|
|
|
|
# === Scopes ===
|
|
scope :draft, -> { where(status: "draft") }
|
|
scope :active, -> { where(status: %w[paid completed]) }
|
|
scope :expired_drafts, -> { draft.where("expires_at < ?", Time.current) }
|
|
scope :can_retry_payment, -> {
|
|
draft.where("payment_attempts < ? AND expires_at > ?",
|
|
MAX_PAYMENT_ATTEMPTS, Time.current)
|
|
}
|
|
|
|
before_validation :set_expiry, on: :create
|
|
|
|
# === Instance Methods ===
|
|
|
|
# Total amount in euros (formatted)
|
|
def total_amount_euros
|
|
total_amount_cents / 100.0
|
|
end
|
|
|
|
# Check if order can be retried for payment
|
|
def can_retry_payment?
|
|
draft? && payment_attempts < MAX_PAYMENT_ATTEMPTS && !expired?
|
|
end
|
|
|
|
# Check if order is expired
|
|
def expired?
|
|
expires_at.present? && expires_at < Time.current
|
|
end
|
|
|
|
# Mark order as expired if it's past expiry time
|
|
def expire_if_overdue!
|
|
return unless draft? && expired?
|
|
update!(status: "expired")
|
|
end
|
|
|
|
# Increment payment attempt counter
|
|
def increment_payment_attempt!
|
|
update!(
|
|
payment_attempts: payment_attempts + 1,
|
|
last_payment_attempt_at: Time.current
|
|
)
|
|
end
|
|
|
|
# Check if draft is about to expire (within 5 minutes)
|
|
def expiring_soon?
|
|
return false unless draft? && expires_at.present?
|
|
expires_at <= 5.minutes.from_now
|
|
end
|
|
|
|
# Mark order as paid and activate all tickets
|
|
def mark_as_paid!
|
|
transaction do
|
|
update!(status: "paid")
|
|
tickets.update_all(status: "active")
|
|
end
|
|
|
|
# Send purchase confirmation email outside the transaction
|
|
# so that payment completion isn't affected by email failures
|
|
begin
|
|
TicketMailer.purchase_confirmation_order(self).deliver_now
|
|
rescue StandardError => e
|
|
Rails.logger.error "Failed to send purchase confirmation email for order #{id}: #{e.message}"
|
|
Rails.logger.error e.backtrace.join("\n")
|
|
# Don't re-raise the error - payment should still succeed
|
|
end
|
|
end
|
|
|
|
# Calculate total from ticket prices minus promotion code discounts
|
|
def calculate_total!
|
|
ticket_total = tickets.sum(:price_cents)
|
|
discount_total = promotion_codes.sum(:discount_amount_cents)
|
|
|
|
# Ensure total doesn't go below zero
|
|
final_total = [ticket_total - discount_total, 0].max
|
|
update!(total_amount_cents: final_total)
|
|
end
|
|
|
|
# Subtotal amount before discounts
|
|
def subtotal_amount_cents
|
|
tickets.sum(:price_cents)
|
|
end
|
|
|
|
# Subtotal amount in euros
|
|
def subtotal_amount_euros
|
|
subtotal_amount_cents / 100.0
|
|
end
|
|
|
|
# Total discount amount from all promotion codes
|
|
def discount_amount_cents
|
|
promotion_codes.sum(:discount_amount_cents)
|
|
end
|
|
|
|
# Discount amount in euros
|
|
def discount_amount_euros
|
|
discount_amount_cents / 100.0
|
|
end
|
|
|
|
# Platform fee: €0.50 fixed + 1.5% of ticket price, per ticket
|
|
def platform_fee_cents
|
|
tickets.sum do |ticket|
|
|
fixed_fee = 50 # €0.50 in cents
|
|
percentage_fee = (ticket.price_cents * 0.015).to_i
|
|
fixed_fee + percentage_fee
|
|
end
|
|
end
|
|
|
|
# Promoter payout amount after platform fee deduction
|
|
def promoter_payout_cents
|
|
total_amount_cents - platform_fee_cents
|
|
end
|
|
|
|
def platform_fee_euros
|
|
platform_fee_cents / 100.0
|
|
end
|
|
|
|
def promoter_payout_euros
|
|
promoter_payout_cents / 100.0
|
|
end
|
|
|
|
# Check if order contains only free tickets
|
|
def free?
|
|
total_amount_cents == 0
|
|
end
|
|
|
|
# Create Stripe invoice for accounting records
|
|
#
|
|
# This method creates a post-payment invoice in Stripe for accounting purposes
|
|
# It should only be called after the order has been paid
|
|
#
|
|
# @return [String, nil] The Stripe invoice ID or nil if creation failed
|
|
def create_stripe_invoice!
|
|
return nil unless status == "paid"
|
|
return @stripe_invoice_id if @stripe_invoice_id.present?
|
|
|
|
service = StripeInvoiceService.new(self)
|
|
stripe_invoice = service.create_post_payment_invoice
|
|
|
|
if stripe_invoice
|
|
@stripe_invoice_id = stripe_invoice.id
|
|
Rails.logger.info "Created Stripe invoice #{stripe_invoice.id} for order #{id}"
|
|
stripe_invoice.id
|
|
else
|
|
Rails.logger.error "Failed to create Stripe invoice for order #{id}: #{service.errors.join(', ')}"
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Get the Stripe invoice PDF URL if available
|
|
#
|
|
# @return [String, nil] The PDF URL or nil if not available
|
|
def stripe_invoice_pdf_url
|
|
return nil unless @stripe_invoice_id.present?
|
|
StripeInvoiceService.get_invoice_pdf_url(@stripe_invoice_id)
|
|
end
|
|
|
|
private
|
|
|
|
def set_expiry
|
|
return unless status == "draft"
|
|
self.expires_at = DRAFT_EXPIRY_TIME.from_now if expires_at.blank?
|
|
end
|
|
|
|
def draft?
|
|
status == "draft"
|
|
end
|
|
end
|