- Promoter can now create an event in draft mode - Place is found based on address and long/lat are automatically deducted from it - Slug is forged using the *slug* npm package instead of custom code
140 lines
4.0 KiB
Ruby
140 lines
4.0 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
|
|
|
|
# === 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 tickets plus 1€ service fee
|
|
def calculate_total!
|
|
ticket_total = tickets.sum(:price_cents)
|
|
fee_cents = 100 # 1€ in cents
|
|
update!(total_amount_cents: ticket_total + fee_cents)
|
|
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
|