Files
aperonight/docs/architecture.md
2025-08-28 13:43:05 +02:00

8.5 KiB

Aperonight - Technical Architecture

Overview

Aperonight is a Ruby on Rails web application designed for proposing night parties in Paris and allowing event makers to create their own events. The application serves two primary user groups:

For Customers:

  • View upcoming and past parties
  • Book tickets with customizable bundles (simple entry, VIP, group passes, etc.)
  • Complete secure payments via credit card, PayPal, or bank transfer
  • Access mobile-friendly interface for ticket management
  • Receive unique, scannable tickets (QR codes)

For Promoters:

  • Create and schedule parties
  • Define custom ticket bundles and pricing
  • Aggregate events from external platforms (Shogun, Bizouk, Weezevent)
  • Scan tickets at events using mobile devices

Technical Architecture

1. Database Schema

# User - Handles both customers and promoters
create_table :users do |t|
  t.string :email
  t.string :password_digest
  t.string :role # customer or promoter
  t.timestamps
end

# Event - Events created by promoters
create_table :events do |t|
  t.string :name
  t.text :description
  t.datetime :start_time
  t.datetime :end_time
  t.string :location
  t.integer :promoter_id
  t.timestamps
end

# TicketType - Customizable bundles defined by promoters
create_table :ticket_types do |t|
  t.string :name
  t.text :description
  t.decimal :price
  t.integer :event_id
  t.timestamps
end

# Ticket - Individual ticket instances purchased by customers
create_table :tickets do |t|
  t.string :uuid
  t.string :qr_code
  t.integer :event_id
  t.integer :user_id
  t.integer :ticket_type_id
  t.boolean :used, default: false
  t.timestamps
end

# Payment - Transaction records for ticket purchases
create_table :payments do |t|
  t.string :payment_method # credit_card, paypal, bank_account
  t.string :transaction_id
  t.integer :user_id
  t.integer :ticket_id
  t.decimal :amount
  t.timestamps
end

2. Core Models

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :tickets
  has_many :payments
  has_many :parties, foreign_key: 'promoter_id'
end

class Event < ApplicationRecord
  belongs_to :promoter, class_name: 'User'
  has_many :tickets
  has_many :ticket_types
end

class TicketType < ApplicationRecord
  belongs_to :event
  has_many :tickets
end

class Ticket < ApplicationRecord
  belongs_to :event
  belongs_to :user
  belongs_to :ticket_type
  has_one :payment

  before_create :generate_uuid_and_qr_code

  private

  def generate_uuid_and_qr_code
    self.uuid = SecureRandom.uuid
    self.qr_code = RQRCode::QRCode.new(self.uuid).as_svg
  end
end

class Payment < ApplicationRecord
  belongs_to :user
  belongs_to :ticket

  enum payment_method: { 
    credit_card: 'credit_card', 
    paypal: 'paypal', 
    bank_account: 'bank_account' 
  }

  after_create :process_payment

  private

  def process_payment
    case self.payment_method
    when 'credit_card'
      process_stripe_payment
    when 'paypal'
      process_paypal_payment
    when 'bank_account'
      process_bank_account_payment
    end
  end
end

3. Key Controllers

Parties Controller

class PartiesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_event, only: [:show, :edit, :update, :destroy]

  def index
    @parties = Event.all
  end

  def show
    @ticket_types = @event.ticket_types
  end

  def new
    @event = Event.new
    @event.ticket_types.build
  end

  def create
    @event = current_user.parties.build(event_params)
    if @event.save
      redirect_to @event, notice: 'Event was successfully created.'
    else
      render :new
    end
  end

  private

  def set_event
    @event = Event.find(params[:id])
  end

  def event_params
    params.require(:event).permit(
      :name, :description, :start_time, :end_time, :location,
      ticket_types_attributes: [:id, :name, :description, :price, :_destroy]
    )
  end
end

Tickets Controller

class TicketsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_event, only: [:new, :create]

  def new
    @ticket = Ticket.new
  end

  def create
    @ticket = current_user.tickets.build(ticket_params)
    if @ticket.save
      redirect_to @ticket, notice: 'Ticket was successfully booked.'
    else
      render :new
    end
  end

  def scan
  end

  def validate
    qr_code = params[:qr_code]
    ticket = Ticket.find_by(qr_code: qr_code)
    if ticket && !ticket.used
      ticket.update(used: true)
      render json: { valid: true }
    else
      render json: { valid: false }
    end
  end

  private

  def set_event
    @event = Event.find(params[:event_id])
  end

  def ticket_params
    params.require(:ticket).permit(:ticket_type_id, :event_id)
  end
end

4. Payment Integration

Stripe Configuration

# config/initializers/stripe.rb
Rails.configuration.stripe = {
  publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
  secret_key: ENV['STRIPE_SECRET_KEY']
}

Stripe.api_key = Rails.configuration.stripe[:secret_key]

PayPal Configuration

# config/initializers/paypal.rb
PayPal::SDK.configure({
  mode: ENV['PAYPAL_MODE'], # 'sandbox' or 'live'
  client_id: ENV['PAYPAL_CLIENT_ID'],
  client_secret: ENV['PAYPAL_CLIENT_SECRET']
})

5. Frontend Considerations

Mobile Ticket Scanning

<!-- app/views/tickets/scan.html.erb -->
<!DOCTYPE html>
<html>
<head>
  <title>Scan Ticket</title>
  <script src="https://unpkg.com/html5-qrcode"></script>
</head>
<body>
  <h1>Scan Ticket</h1>
  <div id="reader" width="500"></div>
  <div id="result"></div>
  <script>
    function docReady(fn) {
      if (document.readyState === "complete" || document.readyState === "interactive") {
        setTimeout(fn, 1);
      } else {
        document.addEventListener("DOMContentLoaded", fn);
      }
    }

    docReady(function () {
      var resultContainer = document.getElementById('result');
      var lastResult, countResults = 0;
      function onScanSuccess(qrCodeMessage) {
        if (qrCodeMessage !== lastResult) {
          ++countResults;
          lastResult = qrCodeMessage;
          resultContainer.innerHTML = `<span class="label">Last scanned QR Code: </span> <a href="${qrCodeMessage}">${qrCodeMessage}</a>`;
          fetch('/tickets/validate', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
            },
            body: JSON.stringify({ qr_code: qrCodeMessage })
          })
          .then(response => response.json())
          .then(data => {
            if (data.valid) {
              resultContainer.innerHTML += '<p>Ticket is valid.</p>';
            } else {
              resultContainer.innerHTML += '<p>Ticket is invalid.</p>';
            }
          })
          .catch(error => {
            console.error('Error:', error);
          });
        }
      }

      var html5QrcodeScanner = new Html5QrcodeScanner(
        "reader", { fps: 10, qrbox: 250 });
      html5QrcodeScanner.render(onScanSuccess);
    });
  </script>
</body>
</html>

6. Routes Configuration

# config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  resources :parties do
    resources :ticket_types, only: [:new, :create, :edit, :update, :destroy]
  end
  resources :ticket_types, only: [:index, :show]
  resources :tickets do
    resources :payments, only: [:new, :create]
    collection do
      post 'validate'
    end
  end
  get 'paypal_success', to: 'payments#paypal_success'
  get 'paypal_cancel', to: 'payments#paypal_cancel'
  get 'tickets/scan', to: 'tickets#scan'
  root 'parties#index'
end

Implementation Recommendations

Authentication & Authorization

  • Use Devise for user authentication
  • Implement Pundit or CanCanCan for role-based access control
  • Distinguish clearly between customer and promoter permissions

Payment Processing

  • Integrate Stripe for credit card payments
  • Add PayPal support through official SDK
  • Consider Plaid for bank account integration

Performance & Scalability

  • Implement Redis for caching frequently accessed data
  • Use CDN for static assets (images, CSS, JS)
  • Employ background job processing (Sidekiq) for emails and payments
  • Optimize database queries with proper indexing

Security Considerations

  • Validate all user inputs
  • Sanitize HTML output to prevent XSS
  • Secure payment processing with PCI compliance
  • Implement rate limiting for API endpoints
  • Regular security audits and dependency updates