Files
aperonight/app/models/order.rb
kbe 87ccebf229 feat(promotion code): Promotion code system done
I added the features for users to use promotion code
and for promoters to create on their events.
May be rewrite to discount code?
2025-09-29 15:25:52 +02:00

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