364 lines
8.5 KiB
Markdown
Executable File
364 lines
8.5 KiB
Markdown
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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
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
|
|
```ruby
|
|
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
|
|
```ruby
|
|
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
|
|
```ruby
|
|
# 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
|
|
```ruby
|
|
# 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
|
|
```erb
|
|
<!-- 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
|
|
|
|
```ruby
|
|
# 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
|
|
|