# Service to create Stripe invoices for accounting records after successful payment # # This service creates post-payment invoices in Stripe for accounting purposes. # Unlike regular Stripe invoices which are used for collection, these are # created after payment via Checkout Sessions as accounting records. class StripeInvoiceService attr_reader :order, :errors def initialize(order) @order = order @errors = [] end # Create a post-payment invoice in Stripe # # Returns the created Stripe invoice object or nil if creation failed def create_post_payment_invoice return nil unless valid_for_invoice_creation? begin customer = find_or_create_stripe_customer return nil unless customer invoice = create_stripe_invoice(customer) return nil unless invoice add_line_items_to_invoice(customer, invoice) finalize_invoice(invoice) Rails.logger.info "Successfully created Stripe invoice #{invoice.id} for order #{@order.id}" invoice rescue Stripe::StripeError => e handle_stripe_error(e) nil rescue => e handle_generic_error(e) nil end end # Get the PDF URL for a Stripe invoice # # @param invoice_id [String] The Stripe invoice ID # @return [String, nil] The invoice PDF URL or nil if not available def self.get_invoice_pdf_url(invoice_id) return nil if invoice_id.blank? begin invoice = Stripe::Invoice.retrieve(invoice_id) invoice.invoice_pdf rescue Stripe::StripeError => e Rails.logger.error "Failed to retrieve Stripe invoice PDF URL: #{e.message}" nil end end private def valid_for_invoice_creation? unless @order.present? @errors << "Order is required" return false end unless @order.status == "paid" @errors << "Order must be paid to create invoice" return false end unless @order.user.present? @errors << "Order must have an associated user" return false end unless @order.tickets.any? @errors << "Order must have tickets to create invoice" return false end true end def find_or_create_stripe_customer if @order.user.stripe_customer_id.present? retrieve_existing_customer else create_new_customer end end def retrieve_existing_customer Stripe::Customer.retrieve(@order.user.stripe_customer_id) rescue Stripe::InvalidRequestError # Customer doesn't exist, create a new one Rails.logger.warn "Stripe customer #{@order.user.stripe_customer_id} not found, creating new customer" @order.user.update(stripe_customer_id: nil) create_new_customer end def create_new_customer customer = Stripe::Customer.create({ email: @order.user.email, name: customer_name, metadata: { user_id: @order.user.id, created_by: "aperonight_system" } }) @order.user.update(stripe_customer_id: customer.id) Rails.logger.info "Created new Stripe customer #{customer.id} for user #{@order.user.id}" customer end def customer_name parts = [] parts << @order.user.first_name if @order.user.first_name.present? parts << @order.user.last_name if @order.user.last_name.present? if parts.empty? @order.user.email.split("@").first.humanize else parts.join(" ") end end def create_stripe_invoice(customer) invoice_data = { customer: customer.id, collection_method: "send_invoice", # Don't auto-charge auto_advance: false, # Don't automatically finalize metadata: { order_id: @order.id, user_id: @order.user.id, event_name: @order.event.name, created_by: "aperonight_system", payment_method: "checkout_session" }, description: "Invoice for #{@order.event.name} - Order ##{@order.id}", footer: "Thank you for your purchase! This invoice is for your records as payment was already processed." } # Add due date (same day since it's already paid) invoice_data[:due_date] = Time.current.to_i Stripe::Invoice.create(invoice_data) end def add_line_items_to_invoice(customer, invoice) @order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets| quantity = tickets.count Stripe::InvoiceItem.create({ customer: customer.id, invoice: invoice.id, amount: ticket_type.price_cents * quantity, currency: "eur", description: build_line_item_description(ticket_type, tickets), metadata: { ticket_type_id: ticket_type.id, ticket_type_name: ticket_type.name, quantity: quantity, unit_price_cents: ticket_type.price_cents } }) end end def build_line_item_description(ticket_type, tickets) quantity = tickets.count unit_price = ticket_type.price_cents / 100.0 description_parts = [ "#{@order.event.name}", "#{ticket_type.name}", "(#{quantity}x €#{unit_price})" ] description_parts.join(" - ") end def finalize_invoice(invoice) # Mark as paid since payment was already processed via checkout finalized_invoice = invoice.finalize_invoice # Mark the invoice as paid finalized_invoice.pay({ paid_out_of_band: true, # Payment was made outside of Stripe invoicing payment_method: nil # No payment method needed for out-of-band payment }) finalized_invoice end def handle_stripe_error(error) error_message = "Stripe invoice creation failed: #{error.message}" @errors << error_message Rails.logger.error "#{error_message} (Order: #{@order.id})" end def handle_generic_error(error) error_message = "Invoice creation failed: #{error.message}" @errors << error_message Rails.logger.error "#{error_message} (Order: #{@order.id})" end end