- Add promoter_id reference to parties table - Add start_time and end_time datetime fields - Replace venue fields with single location string - Update database schema and migration files - Add port configuration to database.yml - Update architecture documentation This commit enhances the party model to support better event tracking and management, including promoter information and precise timing.
364 lines
8.5 KiB
Markdown
364 lines
8.5 KiB
Markdown
# Aperonight - Technical Architecture
|
|
|
|
## Overview
|
|
|
|
Aperonight is a Ruby on Rails web application designed for proposing night parties in Paris and allowing party 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
|
|
|
|
# Party - Events created by promoters
|
|
create_table :parties 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 :party_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 :party_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 Party < ApplicationRecord
|
|
belongs_to :promoter, class_name: 'User'
|
|
has_many :tickets
|
|
has_many :ticket_types
|
|
end
|
|
|
|
class TicketType < ApplicationRecord
|
|
belongs_to :party
|
|
has_many :tickets
|
|
end
|
|
|
|
class Ticket < ApplicationRecord
|
|
belongs_to :party
|
|
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_party, only: [:show, :edit, :update, :destroy]
|
|
|
|
def index
|
|
@parties = Party.all
|
|
end
|
|
|
|
def show
|
|
@ticket_types = @party.ticket_types
|
|
end
|
|
|
|
def new
|
|
@party = Party.new
|
|
@party.ticket_types.build
|
|
end
|
|
|
|
def create
|
|
@party = current_user.parties.build(party_params)
|
|
if @party.save
|
|
redirect_to @party, notice: 'Party was successfully created.'
|
|
else
|
|
render :new
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def set_party
|
|
@party = Party.find(params[:id])
|
|
end
|
|
|
|
def party_params
|
|
params.require(:party).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_party, 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_party
|
|
@party = Party.find(params[:party_id])
|
|
end
|
|
|
|
def ticket_params
|
|
params.require(:ticket).permit(:ticket_type_id, :party_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
|
|
|