This fixes the 'data must be a String, QRSegment, or an Array' error that was preventing checkout completion. Changes: - Move email sending outside payment transaction to avoid rollback on email failure - Add error handling around PDF generation in mailers - Improve QR code data building with multiple fallback strategies - Use direct foreign key access instead of through associations for reliability - Add comprehensive logging for debugging QR code issues - Ensure checkout succeeds even if email/PDF generation fails The payment process will now complete successfully regardless of email issues, while still attempting to send confirmation emails with PDF attachments.
138 lines
3.9 KiB
Ruby
138 lines
3.9 KiB
Ruby
class Order < ApplicationRecord
|
|
# === Constants ===
|
|
DRAFT_EXPIRY_TIME = 30.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
|
|
def calculate_total!
|
|
update!(total_amount_cents: tickets.sum(:price_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
|