Add environment variables for invoice company details to allow customization without code changes. Update invoice view and Stripe service to use these configurable values. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
222 lines
6.3 KiB
Ruby
222 lines
6.3 KiB
Ruby
# 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: "#{ENV.fetch('INVOICE_COMPANY_NAME', 'aperonight').downcase}_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: "#{ENV.fetch('INVOICE_COMPANY_NAME', 'aperonight').downcase}_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)
|
|
# Add ticket line items
|
|
@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
|
|
|
|
# Add service fee line item
|
|
service_fee_cents = 100 # 1€ service fee
|
|
Stripe::InvoiceItem.create({
|
|
customer: customer.id,
|
|
invoice: invoice.id,
|
|
amount: service_fee_cents,
|
|
currency: "eur",
|
|
description: "Frais de service - Frais de traitement de la commande",
|
|
metadata: {
|
|
item_type: "service_fee",
|
|
amount_cents: service_fee_cents
|
|
}
|
|
})
|
|
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
|