Files
aperonight/docs/architecture.md
Kevin BATAILLE 30f3ecc6ad refactor(events): replace parties concept with events throughout the application
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

This commit refactors the entire application to replace the 'parties' concept with 'events'. All controllers, models, views, and related files have been updated to reflect this change. The parties table has been replaced with an events table, and all related functionality has been updated accordingly.
2025-08-28 13:20:51 +02:00

8.5 KiB
Executable File

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