# Invoice System Implementation Guide ## Overview This document outlines various approaches for adding invoice generation to the ApéroNight event ticketing system, based on the current Rails architecture with Stripe integration. ## Current System Analysis ### Existing Infrastructure - **Rails 8.0.2** with MySQL database - **Stripe** payment processing via Checkout Sessions - **Prawn** gem for PDF generation (tickets) - **Order/Ticket** models with pricing logic - **Devise** for user authentication ### Current Payment Flow ``` User selects tickets → Order created (draft) → Stripe Checkout → Payment → Order marked as paid → Tickets activated → PDF tickets emailed ``` ## Invoice Generation Approaches ### 1. Extend Existing PDF Infrastructure (Recommended) **Pros**: - Leverages existing Prawn setup - Consistent styling with tickets - No additional dependencies **Implementation**: ```ruby # app/services/invoice_pdf_generator.rb class InvoicePdfGenerator attr_reader :order def initialize(order) @order = order end def generate Prawn::Document.new(page_size: "A4", margin: 40) do |pdf| # Header pdf.fill_color "2D1B69" pdf.font "Helvetica", style: :bold, size: 24 pdf.text "ApéroNight Invoice", align: :center pdf.move_down 20 # Invoice details pdf.fill_color "000000" pdf.font "Helvetica", size: 12 # Invoice number and date pdf.text "Invoice #: #{@order.invoice_number}" pdf.text "Date: #{@order.created_at.strftime('%B %d, %Y')}" pdf.text "Due Date: #{@order.created_at.strftime('%B %d, %Y')}" # Same day for events pdf.move_down 20 # Customer details pdf.text "Bill To:", style: :bold pdf.text @order.user.email pdf.move_down 20 # Order details table pdf.text "Event: #{@order.event.name}", style: :bold, size: 14 pdf.move_down 10 # Line items items = [["Description", "Quantity", "Unit Price", "Total"]] @order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets| items << [ "#{ticket_type.name} - #{@order.event.name}", tickets.count.to_s, "€#{ticket_type.price_cents / 100.0}", "€#{(tickets.count * ticket_type.price_cents) / 100.0}" ] end pdf.table(items, header: true, width: pdf.bounds.width) do row(0).font_style = :bold columns(1..3).align = :right end pdf.move_down 20 # Total pdf.text "Total: €#{@order.total_amount_cents / 100.0}", style: :bold, size: 16, align: :right # Footer pdf.move_down 40 pdf.text "Thank you for your purchase!", align: :center pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}", align: :center, size: 8 end.render end end ``` ### 2. HTML-to-PDF Solutions **Using WickedPdf**: ```ruby # Gemfile gem 'wicked_pdf' # app/controllers/invoices_controller.rb def show @order = current_user.orders.find(params[:order_id]) respond_to do |format| format.html format.pdf do render pdf: "invoice_#{@order.id}", template: 'invoices/show.html.erb', layout: 'pdf' end end end ``` ### 3. Third-Party Services **Stripe Invoicing Integration**: ```ruby # app/services/stripe_invoice_service.rb class StripeInvoiceService def initialize(order) @order = order end def create_post_payment_invoice customer = find_or_create_stripe_customer invoice = Stripe::Invoice.create({ customer: customer.id, collection_method: 'charge_automatically', paid: true, # Already paid via checkout metadata: { order_id: @order.id, user_id: @order.user.id } }) # Add line items @order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets| Stripe::InvoiceItem.create({ customer: customer.id, invoice: invoice.id, amount: ticket_type.price_cents * tickets.count, currency: 'eur', description: "#{@order.event.name} - #{ticket_type.name} (#{tickets.count}x)" }) end invoice.finalize_invoice end private def find_or_create_stripe_customer if @order.user.stripe_customer_id.present? Stripe::Customer.retrieve(@order.user.stripe_customer_id) else customer = Stripe::Customer.create({ email: @order.user.email, metadata: { user_id: @order.user.id } }) @order.user.update(stripe_customer_id: customer.id) customer end end end ``` ## Stripe Payment Methods Comparison ### Payment Intents vs Invoicing vs Checkout Sessions | Feature | Payment Intents | Stripe Invoicing | Checkout Sessions (Current) | |---------|----------------|------------------|---------------------------| | **Timing** | Immediate | Deferred (days/weeks) | Immediate | | **User Experience** | Custom UI on your site | Stripe-hosted invoice page | Stripe-hosted checkout | | **Payment Methods** | Cards, wallets, BNPL | Cards, bank transfers, checks | Cards, wallets, BNPL | | **Documentation** | Custom receipts | Formal invoices | Stripe receipts | | **Integration Complexity** | Medium | Low | Low | | **Best For** | Custom checkout flows | B2B billing | Quick implementation | ### For Event Ticketing Use Case **Current Checkout Sessions are ideal** because: - Events require immediate payment confirmation - Time-sensitive inventory management - Users expect instant ticket delivery - Built-in fraud protection **Recommended**: Keep Checkout Sessions, add invoice generation for accounting records ## Recommended Implementation ### Phase 1: Database Schema ```ruby # Migration class CreateInvoices < ActiveRecord::Migration[8.0] def change create_table :invoices do |t| t.references :order, null: false, foreign_key: true t.string :invoice_number, null: false t.integer :total_amount_cents, null: false t.string :currency, default: 'eur' t.string :status, default: 'issued' # issued, paid, cancelled t.datetime :issued_at t.datetime :paid_at t.string :stripe_invoice_id # Optional: if using Stripe t.text :notes t.timestamps end add_index :invoices, :invoice_number, unique: true add_index :invoices, :status end end # Add to User model for Stripe integration class AddStripeFieldsToUsers < ActiveRecord::Migration[8.0] def change add_column :users, :stripe_customer_id, :string add_index :users, :stripe_customer_id end end ``` ### Phase 2: Models ```ruby # app/models/invoice.rb class Invoice < ApplicationRecord belongs_to :order has_one :user, through: :order has_one :event, through: :order validates :invoice_number, presence: true, uniqueness: true validates :total_amount_cents, presence: true, numericality: { greater_than: 0 } validates :status, inclusion: { in: %w[issued paid cancelled] } before_validation :generate_invoice_number, on: :create before_validation :set_defaults, on: :create scope :paid, -> { where(status: 'paid') } scope :unpaid, -> { where(status: 'issued') } def total_amount_euros total_amount_cents / 100.0 end def generate_pdf InvoicePdfGenerator.new(self).generate end def mark_as_paid! update!(status: 'paid', paid_at: Time.current) end private def generate_invoice_number return if invoice_number.present? year = Time.current.year month = Time.current.strftime('%m') # Find highest invoice number for current month last_invoice = Invoice.where( 'invoice_number LIKE ?', "INV-#{year}#{month}-%" ).order(:invoice_number).last if last_invoice sequence = last_invoice.invoice_number.split('-').last.to_i + 1 else sequence = 1 end self.invoice_number = "INV-#{year}#{month}-#{sequence.to_s.rjust(4, '0')}" end def set_defaults self.total_amount_cents = order.total_amount_cents if order self.issued_at = Time.current self.status = 'paid' if order&.status == 'paid' # Auto-mark as paid for completed orders end end # app/models/order.rb - Add invoice association class Order < ApplicationRecord # ... existing code ... has_one :invoice, dependent: :destroy # Add method to create invoice after payment def create_invoice! return invoice if invoice.present? Invoice.create!( order: self, total_amount_cents: self.total_amount_cents ) end end ``` ### Phase 3: Controllers ```ruby # app/controllers/invoices_controller.rb class InvoicesController < ApplicationController before_action :authenticate_user! before_action :set_invoice def show # HTML view of invoice end def download_pdf pdf = @invoice.generate_pdf send_data pdf, filename: "invoice_#{@invoice.invoice_number}.pdf", type: 'application/pdf', disposition: 'attachment' end private def set_invoice @invoice = current_user.invoices.joins(:order).find(params[:id]) rescue ActiveRecord::RecordNotFound redirect_to dashboard_path, alert: "Invoice not found" end end # Update app/controllers/orders_controller.rb class OrdersController < ApplicationController # ... existing code ... def payment_success # ... existing payment success logic ... if stripe_session.payment_status == "paid" @order.mark_as_paid! # Generate invoice @order.create_invoice! # Send confirmation emails with invoice attached @order.tickets.each do |ticket| begin TicketMailer.purchase_confirmation(ticket).deliver_now rescue => e Rails.logger.error "Failed to send confirmation: #{e.message}" end end # ... rest of existing code ... end end end ``` ### Phase 4: Mailer Updates ```ruby # app/mailers/ticket_mailer.rb - Update to include invoice class TicketMailer < ApplicationMailer def purchase_confirmation(ticket) @ticket = ticket @order = ticket.order @user = ticket.user @event = ticket.event # Attach ticket PDF ticket_pdf = ticket.to_pdf attachments["ticket_#{ticket.id}.pdf"] = ticket_pdf # Attach invoice PDF if @order.invoice.present? invoice_pdf = @order.invoice.generate_pdf attachments["invoice_#{@order.invoice.invoice_number}.pdf"] = invoice_pdf end mail( to: @user.email, subject: "Your tickets for #{@event.name}" ) end end ``` ### Phase 5: Routes ```ruby # config/routes.rb Rails.application.routes.draw do # ... existing routes ... resources :invoices, only: [:show] do member do get :download_pdf end end resources :orders do member do get :invoice # Shortcut to order's invoice end end end ``` ### Phase 6: Views ```erb

Invoice

Invoice #<%= @invoice.invoice_number %>

Date: <%= @invoice.issued_at.strftime('%B %d, %Y') %>

<%= @invoice.status.capitalize %>

Bill To:

<%= @invoice.user.email %>

Event:

<%= @invoice.event.name %>

<%= @invoice.event.start_time.strftime('%B %d, %Y at %I:%M %p') %>

<% @invoice.order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets| %> <% end %>
Description Qty Unit Price Total
<%= ticket_type.name %> <%= tickets.count %> €<%= ticket_type.price_cents / 100.0 %> €<%= (tickets.count * ticket_type.price_cents) / 100.0 %>

Total: €<%= @invoice.total_amount_euros %>

<%= link_to "Download PDF", download_pdf_invoice_path(@invoice), class: "bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700" %>
``` ## Integration Guidelines ### 1. Testing Strategy ```ruby # test/services/invoice_pdf_generator_test.rb require 'test_helper' class InvoicePdfGeneratorTest < ActiveSupport::TestCase setup do @order = orders(:paid_order) @generator = InvoicePdfGenerator.new(@order.invoice) end test "generates PDF successfully" do pdf = @generator.generate assert pdf.present? assert pdf.is_a?(String) end test "includes order details in PDF" do # Test PDF content includes expected information end end ``` ### 2. Performance Considerations ```ruby # app/jobs/invoice_generation_job.rb class InvoiceGenerationJob < ApplicationJob queue_as :default def perform(order_id) order = Order.find(order_id) invoice = order.create_invoice! # Generate PDF in background pdf = invoice.generate_pdf # Store in cloud storage if needed # S3Service.store_invoice_pdf(invoice, pdf) # Send email notification InvoiceMailer.invoice_ready(invoice).deliver_now end end # Call from OrdersController#payment_success InvoiceGenerationJob.perform_later(@order.id) ``` ### 3. Security Considerations - Ensure users can only access their own invoices - Validate invoice numbers are unique - Sanitize user input in PDF generation - Use HTTPS for invoice downloads - Consider adding invoice access tokens for sharing ### 4. Deployment Checklist - [ ] Run database migrations - [ ] Update mailer templates - [ ] Test PDF generation in production environment - [ ] Verify email attachments work correctly - [ ] Set up monitoring for PDF generation failures - [ ] Add invoice generation to payment success flow ## Conclusion The recommended approach combines the best of both worlds: - Keep the existing immediate payment flow for better user experience - Generate professional invoices for accounting and user records - Use existing Prawn infrastructure for consistent PDF styling - Optionally integrate with Stripe invoicing for advanced features This implementation provides a solid foundation that can be extended with additional features like tax calculations, discounts, or integration with accounting systems.