Merge pull request 'fix(promotion code): Cap the minimum invoice for Stripe' (#6) from feat/promotion-code into develop
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
378
AGENTS.md
378
AGENTS.md
@@ -10,38 +10,64 @@ This document provides technical details for AI agents working on the Aperonight
|
|||||||
|
|
||||||
#### 1. User Management (`app/models/user.rb`)
|
#### 1. User Management (`app/models/user.rb`)
|
||||||
- **Devise Integration**: Complete authentication system with registration, login, password reset
|
- **Devise Integration**: Complete authentication system with registration, login, password reset
|
||||||
- **Relationships**: Users can create events and purchase tickets
|
- **Professional Users**: `is_professionnal` field for event promoters with enhanced permissions
|
||||||
- **Validations**: Email format, password strength, optional name fields
|
- **Onboarding System**: Multi-step onboarding process with `onboarding_completed` tracking
|
||||||
|
- **Stripe Integration**: `stripe_customer_id` for accounting and invoice management
|
||||||
|
- **Relationships**: Users can create events, purchase tickets, and manage promotion codes
|
||||||
|
- **Validations**: Email format, password strength, optional name fields, company information
|
||||||
|
|
||||||
#### 2. Event System (`app/models/event.rb`)
|
#### 2. Event System (`app/models/event.rb`)
|
||||||
- **States**: `draft`, `published`, `canceled`, `sold_out` with enum management
|
- **States**: `draft`, `published`, `canceled`, `sold_out` with enum management
|
||||||
- **Geographic Data**: Latitude/longitude for venue mapping
|
- **Geographic Data**: Latitude/longitude for venue mapping
|
||||||
- **Relationships**: Belongs to user, has many ticket types and tickets through ticket types
|
- **Relationships**: Belongs to user, has many ticket types, tickets through ticket types, and orders
|
||||||
- **Scopes**: Featured events, published events, upcoming events with proper ordering
|
- **Scopes**: Featured events, published events, upcoming events with proper ordering
|
||||||
|
- **Duplication**: Event duplication functionality for similar events
|
||||||
|
|
||||||
#### 3. Ticket Management
|
#### 3. Order Management (`app/models/order.rb`)
|
||||||
|
- **Order States**: `draft`, `pending_payment`, `paid`, `completed`, `cancelled`, `expired`
|
||||||
|
- **Payment Processing**: Stripe integration with payment attempt tracking
|
||||||
|
- **Platform Fees**: €0.50 fixed + 1.5% per ticket automatic calculation
|
||||||
|
- **Expiration**: 15-minute draft order expiration with automatic cleanup
|
||||||
|
- **Promotion Integration**: Support for discount code application
|
||||||
|
- **Invoice Generation**: Automatic Stripe invoice creation for accounting
|
||||||
|
|
||||||
|
#### 4. Promotion Code System (`app/models/promotion_code.rb`)
|
||||||
|
- **Discount Management**: Fixed amount discounts (stored in cents, displayed in euros)
|
||||||
|
- **Usage Controls**: Per-event and per-user association with usage limits
|
||||||
|
- **Expiration**: Date-based expiration with active/inactive status management
|
||||||
|
- **Validation**: Real-time validation during checkout process
|
||||||
|
- **Tracking**: Complete usage tracking and analytics
|
||||||
|
|
||||||
|
#### 5. Ticket Management
|
||||||
- **TicketType** (`app/models/ticket_type.rb`): Defines ticket categories with pricing, quantity, sale periods
|
- **TicketType** (`app/models/ticket_type.rb`): Defines ticket categories with pricing, quantity, sale periods
|
||||||
- **Ticket** (`app/models/ticket.rb`): Individual tickets with unique QR codes, status tracking, price storage
|
- **Ticket** (`app/models/ticket.rb`): Individual tickets with unique QR codes, status tracking, price storage
|
||||||
|
- **Order Association**: Tickets now belong to orders for better transaction management
|
||||||
|
|
||||||
#### 4. Payment Processing (`app/controllers/events_controller.rb`)
|
#### 6. Payment Processing (`app/controllers/orders_controller.rb`)
|
||||||
|
- **Order-Based Workflow**: Complete shift from direct ticket purchase to order-based system
|
||||||
- **Stripe Integration**: Complete checkout session creation and payment confirmation
|
- **Stripe Integration**: Complete checkout session creation and payment confirmation
|
||||||
- **Session Management**: Proper handling of payment success/failure with ticket generation
|
- **Session Management**: Proper handling of payment success/failure with order and ticket generation
|
||||||
- **Security**: Authentication required, cart validation, availability checking
|
- **Security**: Authentication required, cart validation, availability checking
|
||||||
|
- **Invoice Service**: Post-payment invoice generation with StripeInvoiceService
|
||||||
|
|
||||||
### Database Schema Key Points
|
### Database Schema Key Points
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Users table (managed by Devise)
|
-- Users table (enhanced with professional features)
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id bigint PRIMARY KEY,
|
id bigint PRIMARY KEY,
|
||||||
email varchar(255) UNIQUE NOT NULL,
|
email varchar(255) UNIQUE NOT NULL,
|
||||||
encrypted_password varchar(255) NOT NULL,
|
encrypted_password varchar(255) NOT NULL,
|
||||||
first_name varchar(255),
|
first_name varchar(255),
|
||||||
last_name varchar(255),
|
last_name varchar(255),
|
||||||
|
is_professionnal boolean DEFAULT false,
|
||||||
|
onboarding_completed boolean DEFAULT false,
|
||||||
|
stripe_customer_id varchar(255),
|
||||||
|
company_name varchar(255),
|
||||||
-- Devise fields: confirmation, reset tokens, etc.
|
-- Devise fields: confirmation, reset tokens, etc.
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Events table
|
-- Events table (enhanced with order management)
|
||||||
CREATE TABLE events (
|
CREATE TABLE events (
|
||||||
id bigint PRIMARY KEY,
|
id bigint PRIMARY KEY,
|
||||||
user_id bigint REFERENCES users(id),
|
user_id bigint REFERENCES users(id),
|
||||||
@@ -59,6 +85,40 @@ CREATE TABLE events (
|
|||||||
image varchar(500)
|
image varchar(500)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Order management system (new core table)
|
||||||
|
CREATE TABLE orders (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
user_id bigint REFERENCES users(id),
|
||||||
|
event_id bigint REFERENCES events(id),
|
||||||
|
status varchar(255) DEFAULT 'draft',
|
||||||
|
total_amount_cents integer DEFAULT 0,
|
||||||
|
platform_fee_cents integer DEFAULT 0,
|
||||||
|
payment_attempts integer DEFAULT 0,
|
||||||
|
expires_at timestamp,
|
||||||
|
last_payment_attempt_at timestamp,
|
||||||
|
stripe_checkout_session_id varchar(255),
|
||||||
|
stripe_invoice_id varchar(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Promotion codes table (new discount system)
|
||||||
|
CREATE TABLE promotion_codes (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
code varchar(255) UNIQUE NOT NULL,
|
||||||
|
discount_amount_cents integer DEFAULT 0,
|
||||||
|
expires_at datetime,
|
||||||
|
active boolean DEFAULT true,
|
||||||
|
usage_limit integer,
|
||||||
|
uses_count integer DEFAULT 0,
|
||||||
|
user_id bigint REFERENCES users(id),
|
||||||
|
event_id bigint REFERENCES events(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Order-promotion code join table
|
||||||
|
CREATE TABLE order_promotion_codes (
|
||||||
|
order_id bigint REFERENCES orders(id),
|
||||||
|
promotion_code_id bigint REFERENCES promotion_codes(id)
|
||||||
|
);
|
||||||
|
|
||||||
-- Ticket types define pricing and availability
|
-- Ticket types define pricing and availability
|
||||||
CREATE TABLE ticket_types (
|
CREATE TABLE ticket_types (
|
||||||
id bigint PRIMARY KEY,
|
id bigint PRIMARY KEY,
|
||||||
@@ -73,10 +133,11 @@ CREATE TABLE ticket_types (
|
|||||||
minimum_age integer
|
minimum_age integer
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Individual tickets with QR codes
|
-- Individual tickets with QR codes (enhanced with order association)
|
||||||
CREATE TABLE tickets (
|
CREATE TABLE tickets (
|
||||||
id bigint PRIMARY KEY,
|
id bigint PRIMARY KEY,
|
||||||
user_id bigint REFERENCES users(id),
|
user_id bigint REFERENCES users(id),
|
||||||
|
order_id bigint REFERENCES orders(id),
|
||||||
ticket_type_id bigint REFERENCES ticket_types(id),
|
ticket_type_id bigint REFERENCES ticket_types(id),
|
||||||
qr_code varchar(255) UNIQUE NOT NULL,
|
qr_code varchar(255) UNIQUE NOT NULL,
|
||||||
price_cents integer NOT NULL,
|
price_cents integer NOT NULL,
|
||||||
@@ -107,38 +168,113 @@ CREATE TABLE tickets (
|
|||||||
.limit(5)
|
.limit(5)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Stripe Payment Flow
|
### 2. Order Management Flow (`app/controllers/orders_controller.rb`)
|
||||||
|
|
||||||
#### Checkout Initiation (`events#checkout`)
|
#### Order Creation and Payment
|
||||||
1. **Cart Validation**: Parse JSON cart data, validate ticket types and quantities
|
1. **Cart-to-Order Conversion**: Convert shopping cart to draft order with 15-minute expiration
|
||||||
2. **Availability Check**: Ensure sufficient tickets available before payment
|
2. **Platform Fee Calculation**: Automatic calculation of €0.50 fixed + 1.5% per ticket
|
||||||
3. **Stripe Session**: Create checkout session with line items, success/cancel URLs
|
3. **Promotion Code Application**: Real-time discount validation and application
|
||||||
4. **Metadata Storage**: Store order details in Stripe session metadata for later retrieval
|
4. **Stripe Checkout Session**: Create payment session with order metadata
|
||||||
|
5. **Payment Retry**: Support for multiple payment attempts with proper tracking
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# Key Stripe configuration
|
# Order creation with platform fees
|
||||||
session = Stripe::Checkout::Session.create({
|
def create
|
||||||
payment_method_types: ['card'],
|
@order = Order.new(order_params)
|
||||||
line_items: line_items,
|
@order.user = current_user
|
||||||
mode: 'payment',
|
@order.calculate_platform_fee
|
||||||
success_url: payment_success_url(event_id: @event.id, session_id: '{CHECKOUT_SESSION_ID}'),
|
@order.set_expiration
|
||||||
cancel_url: event_url(@event.slug, @event),
|
|
||||||
customer_email: current_user.email,
|
if @order.save
|
||||||
metadata: {
|
session = create_stripe_checkout_session(@order)
|
||||||
event_id: @event.id,
|
redirect_to session.url, allow_other_host: true
|
||||||
user_id: current_user.id,
|
else
|
||||||
order_items: order_items.to_json
|
render :new, status: :unprocessable_entity
|
||||||
}
|
end
|
||||||
})
|
end
|
||||||
|
|
||||||
|
# Platform fee calculation
|
||||||
|
def calculate_platform_fee
|
||||||
|
ticket_count = order_items.sum(:quantity)
|
||||||
|
self.platform_fee_cents = 50 + (total_amount_cents * 0.015).to_i
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Payment Confirmation (`events#payment_success`)
|
#### Payment Confirmation and Invoice Generation
|
||||||
1. **Session Retrieval**: Get Stripe session with payment status
|
1. **Order Status Update**: Transition from pending_payment to paid
|
||||||
2. **Ticket Creation**: Generate tickets based on order items from metadata
|
2. **Ticket Generation**: Create tickets associated with the order
|
||||||
3. **QR Code Generation**: Automatic unique QR code creation via model callbacks
|
3. **Stripe Invoice Creation**: Async invoice generation for accounting
|
||||||
4. **Success Page**: Display tickets with download links
|
4. **Promotion Code Usage**: Increment usage counters for applied codes
|
||||||
|
|
||||||
### 3. PDF Ticket Generation (`app/services/ticket_pdf_generator.rb`)
|
### 3. Enhanced Stripe Integration
|
||||||
|
|
||||||
|
#### StripeInvoiceService (`app/services/stripe_invoice_service.rb`)
|
||||||
|
- Post-payment invoice creation with customer management
|
||||||
|
- Line item processing with promotion discounts
|
||||||
|
- PDF invoice URL generation for download
|
||||||
|
- Accounting record synchronization
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class StripeInvoiceService
|
||||||
|
def initialize(order)
|
||||||
|
@order = order
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_invoice
|
||||||
|
customer = find_or_create_stripe_customer
|
||||||
|
invoice_items = create_invoice_items(customer)
|
||||||
|
|
||||||
|
invoice = Stripe::Invoice.create({
|
||||||
|
customer: customer.id,
|
||||||
|
auto_advance: true,
|
||||||
|
collection_method: 'charge_automatically'
|
||||||
|
})
|
||||||
|
|
||||||
|
@order.update(stripe_invoice_id: invoice.id)
|
||||||
|
invoice.finalize_invoice
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Promotion Code System (`app/models/promotion_code.rb`)
|
||||||
|
|
||||||
|
#### Code Validation and Application
|
||||||
|
- **Real-time Validation**: Check code validity, expiration, and usage limits
|
||||||
|
- **Discount Calculation**: Apply fixed amount discounts to order totals
|
||||||
|
- **Usage Tracking**: Increment usage counters and prevent overuse
|
||||||
|
- **Event-Specific Codes**: Support for both global and event-specific codes
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
def valid_for_use?(user = nil, event = nil)
|
||||||
|
return false unless active?
|
||||||
|
return false if expired?
|
||||||
|
return false if usage_limit_reached?
|
||||||
|
return false if user.present? && !valid_for_user?(user)
|
||||||
|
return false if event.present? && !valid_for_event?(event)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_discount(total_amount)
|
||||||
|
[total_amount - discount_amount_cents, 0].max
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Background Job Architecture
|
||||||
|
|
||||||
|
#### StripeInvoiceGenerationJob
|
||||||
|
- Async invoice creation after successful payment
|
||||||
|
- Retry logic with exponential backoff
|
||||||
|
- Error handling and logging
|
||||||
|
|
||||||
|
#### ExpiredOrdersCleanupJob
|
||||||
|
- Automatic cleanup of expired draft orders
|
||||||
|
- Database maintenance and hygiene
|
||||||
|
|
||||||
|
#### EventReminderJob & EventReminderSchedulerJob
|
||||||
|
- Automated event reminder emails
|
||||||
|
- Scheduled notifications for upcoming events
|
||||||
|
|
||||||
|
### 6. PDF Ticket Generation (`app/services/ticket_pdf_generator.rb`)
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class TicketPdfGenerator
|
class TicketPdfGenerator
|
||||||
@@ -148,10 +284,10 @@ class TicketPdfGenerator
|
|||||||
pdf.fill_color "2D1B69"
|
pdf.fill_color "2D1B69"
|
||||||
pdf.font "Helvetica", style: :bold, size: 24
|
pdf.font "Helvetica", style: :bold, size: 24
|
||||||
pdf.text "ApéroNight", align: :center
|
pdf.text "ApéroNight", align: :center
|
||||||
|
|
||||||
# Event details
|
# Event details
|
||||||
pdf.text ticket.event.name, align: :center
|
pdf.text ticket.event.name, align: :center
|
||||||
|
|
||||||
# QR Code generation
|
# QR Code generation
|
||||||
qr_code_data = {
|
qr_code_data = {
|
||||||
ticket_id: ticket.id,
|
ticket_id: ticket.id,
|
||||||
@@ -159,7 +295,7 @@ class TicketPdfGenerator
|
|||||||
event_id: ticket.event.id,
|
event_id: ticket.event.id,
|
||||||
user_id: ticket.user.id
|
user_id: ticket.user.id
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
qrcode = RQRCode::QRCode.new(qr_code_data)
|
qrcode = RQRCode::QRCode.new(qr_code_data)
|
||||||
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
||||||
end.render
|
end.render
|
||||||
@@ -167,12 +303,20 @@ class TicketPdfGenerator
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Frontend Cart Management (`app/javascript/controllers/ticket_cart_controller.js`)
|
### 7. Frontend Architecture
|
||||||
|
|
||||||
- **Stimulus Controller**: Manages cart state and interactions
|
#### Enhanced Stimulus Controllers
|
||||||
- **Authentication Check**: Validates user login before checkout
|
- **ticket_selection_controller.js**: Advanced cart management with real-time updates
|
||||||
- **Session Storage**: Preserves cart when redirecting to login
|
- **event_form_controller.js**: Dynamic event creation with location services
|
||||||
- **Dynamic Updates**: Real-time cart total and ticket count updates
|
- **countdown_controller.js**: Order expiration countdown timers
|
||||||
|
- **event_duplication_controller.js**: Event copying functionality
|
||||||
|
- **qr_code_controller.js**: QR code display and scanning
|
||||||
|
|
||||||
|
#### Order-Based Cart Management
|
||||||
|
- **Session Storage**: Preserves cart state during authentication flows
|
||||||
|
- **Real-time Updates**: Dynamic total calculation with promotion codes
|
||||||
|
- **Validation**: Client-side validation with server-side verification
|
||||||
|
- **Payment Flow**: Seamless integration with Stripe checkout
|
||||||
|
|
||||||
## 🔧 Development Patterns
|
## 🔧 Development Patterns
|
||||||
|
|
||||||
@@ -180,11 +324,21 @@ end
|
|||||||
```ruby
|
```ruby
|
||||||
# Event validations
|
# Event validations
|
||||||
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||||
validates :latitude, numericality: {
|
validates :latitude, numericality: {
|
||||||
greater_than_or_equal_to: -90,
|
greater_than_or_equal_to: -90,
|
||||||
less_than_or_equal_to: 90
|
less_than_or_equal_to: 90
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Order validations with state management
|
||||||
|
validates :status, presence: true, inclusion: { in: %w[draft pending_payment paid completed cancelled expired] }
|
||||||
|
validate :order_not_expired, on: :create
|
||||||
|
before_validation :set_expiration, on: :create
|
||||||
|
|
||||||
|
# Promotion code validations
|
||||||
|
validates :code, presence: true, uniqueness: true
|
||||||
|
validates :discount_amount_cents, numericality: { greater_than_or_equal_to: 0 }
|
||||||
|
validate :expiration_date_cannot_be_in_the_past
|
||||||
|
|
||||||
# Ticket QR code generation
|
# Ticket QR code generation
|
||||||
before_validation :generate_qr_code, on: :create
|
before_validation :generate_qr_code, on: :create
|
||||||
def generate_qr_code
|
def generate_qr_code
|
||||||
@@ -200,11 +354,47 @@ end
|
|||||||
# Authentication for sensitive actions
|
# Authentication for sensitive actions
|
||||||
before_action :authenticate_user!, only: [:checkout, :payment_success, :download_ticket]
|
before_action :authenticate_user!, only: [:checkout, :payment_success, :download_ticket]
|
||||||
|
|
||||||
# Strong parameters
|
# Professional user authorization
|
||||||
|
before_action :authenticate_professional!, only: [:create_promotion_code]
|
||||||
|
|
||||||
|
# Strong parameters with nested attributes
|
||||||
private
|
private
|
||||||
def event_params
|
def order_params
|
||||||
params.require(:event).permit(:name, :description, :venue_name, :venue_address,
|
params.require(:order).permit(:promotion_code, order_items_attributes: [:ticket_type_id, :quantity])
|
||||||
:latitude, :longitude, :start_time, :image)
|
end
|
||||||
|
|
||||||
|
# Platform fee calculation
|
||||||
|
def calculate_platform_fee
|
||||||
|
ticket_count = order_items.sum(:quantity)
|
||||||
|
self.platform_fee_cents = 50 + (total_amount_cents * 0.015).to_i
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Layer Patterns
|
||||||
|
```ruby
|
||||||
|
# Service for complex business logic
|
||||||
|
class StripeInvoiceService
|
||||||
|
def initialize(order)
|
||||||
|
@order = order
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
customer = find_or_create_stripe_customer
|
||||||
|
create_invoice_items(customer)
|
||||||
|
generate_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)
|
||||||
|
@order.user.update(stripe_customer_id: customer.id)
|
||||||
|
customer
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -212,6 +402,7 @@ end
|
|||||||
- **Metric Cards**: Reusable component for dashboard statistics
|
- **Metric Cards**: Reusable component for dashboard statistics
|
||||||
- **Event Items**: Consistent event display across pages
|
- **Event Items**: Consistent event display across pages
|
||||||
- **Flash Messages**: Centralized notification system
|
- **Flash Messages**: Centralized notification system
|
||||||
|
- **Order Components**: Reusable order display and management components
|
||||||
|
|
||||||
## 🚀 Deployment Considerations
|
## 🚀 Deployment Considerations
|
||||||
|
|
||||||
@@ -223,14 +414,28 @@ STRIPE_SECRET_KEY=sk_live_...
|
|||||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||||
DATABASE_URL=mysql2://user:pass@host/db
|
DATABASE_URL=mysql2://user:pass@host/db
|
||||||
RAILS_MASTER_KEY=...
|
RAILS_MASTER_KEY=...
|
||||||
|
|
||||||
|
# Rails 8 Solid Stack
|
||||||
|
SOLID_QUEUE_IN_PUMA=true
|
||||||
|
SOLID_CACHE_URL=redis://localhost:6379/0
|
||||||
|
SOLID_CABLE_URL=redis://localhost:6379/1
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
PLATFORM_FEE_FIXED_CENTS=50
|
||||||
|
PLATFORM_FEE_PERCENTAGE=1.5
|
||||||
|
ORDER_EXPIRATION_MINUTES=15
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Indexes
|
### Database Indexes
|
||||||
```sql
|
```sql
|
||||||
-- Performance indexes for common queries
|
-- Performance indexes for common queries
|
||||||
CREATE INDEX idx_events_published_start_time ON events (state, start_time);
|
CREATE INDEX idx_events_published_start_time ON events (state, start_time);
|
||||||
|
CREATE INDEX idx_orders_user_status ON orders (user_id, status);
|
||||||
|
CREATE INDEX idx_orders_expires_at ON orders (expires_at) WHERE status = 'draft';
|
||||||
CREATE INDEX idx_tickets_user_status ON tickets (user_id, status);
|
CREATE INDEX idx_tickets_user_status ON tickets (user_id, status);
|
||||||
CREATE INDEX idx_ticket_types_event ON ticket_types (event_id);
|
CREATE INDEX idx_ticket_types_event ON ticket_types (event_id);
|
||||||
|
CREATE INDEX idx_promotion_codes_code ON promotion_codes (code);
|
||||||
|
CREATE INDEX idx_promotion_codes_active_expires ON promotion_codes (active, expires_at);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Security Considerations
|
### Security Considerations
|
||||||
@@ -238,22 +443,68 @@ CREATE INDEX idx_ticket_types_event ON ticket_types (event_id);
|
|||||||
- **Strong Parameters**: All user inputs filtered
|
- **Strong Parameters**: All user inputs filtered
|
||||||
- **Authentication**: Devise handles session security
|
- **Authentication**: Devise handles session security
|
||||||
- **Payment Security**: Stripe handles sensitive payment data
|
- **Payment Security**: Stripe handles sensitive payment data
|
||||||
|
- **Professional User Authorization**: Role-based access control for event promoters
|
||||||
|
- **Order Expiration**: Automatic cleanup of abandoned orders
|
||||||
|
- **Promotion Code Validation**: Server-side validation with usage limits
|
||||||
|
|
||||||
|
### Background Jobs
|
||||||
|
```ruby
|
||||||
|
# Async invoice generation
|
||||||
|
StripeInvoiceGenerationJob.perform_later(order_id)
|
||||||
|
|
||||||
|
# Cleanup expired orders
|
||||||
|
ExpiredOrdersCleanupJob.perform_later
|
||||||
|
|
||||||
|
# Event reminders
|
||||||
|
EventReminderSchedulerJob.set(wait_until: event.start_time - 2.hours).perform_later(event_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 API Layer
|
||||||
|
|
||||||
|
### RESTful Endpoints
|
||||||
|
```ruby
|
||||||
|
# API Namespacing for external integrations
|
||||||
|
namespace :api do
|
||||||
|
namespace :v1 do
|
||||||
|
resources :events, only: [:index, :show] do
|
||||||
|
resources :ticket_types, only: [:index]
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :carts, only: [:create, :show, :update]
|
||||||
|
resources :orders, only: [:create, :show, :update]
|
||||||
|
|
||||||
|
post '/promotion_codes/validate', to: 'promotion_codes#validate'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Authentication
|
||||||
|
- **Token-based authentication**: API tokens for external integrations
|
||||||
|
- **Rate limiting**: Request throttling for API endpoints
|
||||||
|
- **Versioning**: Versioned API namespace for backward compatibility
|
||||||
|
|
||||||
## 🧪 Testing Strategy
|
## 🧪 Testing Strategy
|
||||||
|
|
||||||
### Key Test Cases
|
### Key Test Cases
|
||||||
1. **User Authentication**: Registration, login, logout flows
|
1. **User Authentication**: Registration, login, logout flows
|
||||||
2. **Event Creation**: Validation, state management, relationships
|
2. **Professional User Onboarding**: Multi-step onboarding process
|
||||||
3. **Booking Process**: Cart validation, payment processing, ticket generation
|
3. **Event Creation**: Validation, state management, relationships
|
||||||
4. **PDF Generation**: QR code uniqueness, ticket format
|
4. **Order Management**: Cart-to-order conversion, payment processing, expiration
|
||||||
5. **Dashboard Metrics**: Query accuracy, performance
|
5. **Promotion Code System**: Code validation, discount application, usage tracking
|
||||||
|
6. **PDF Generation**: QR code uniqueness, ticket format
|
||||||
|
7. **Stripe Integration**: Payment processing, invoice generation
|
||||||
|
8. **Background Jobs**: Async processing, error handling, retry logic
|
||||||
|
9. **API Endpoints**: RESTful API functionality and authentication
|
||||||
|
10. **Dashboard Metrics**: Query accuracy, performance
|
||||||
|
|
||||||
### Seed Data Structure
|
### Seed Data Structure
|
||||||
```ruby
|
```ruby
|
||||||
# Creates test users, events, and ticket types
|
# Creates comprehensive test data
|
||||||
users = User.create!([...])
|
users = User.create!([...])
|
||||||
events = Event.create!([...])
|
events = Event.create!([...])
|
||||||
ticket_types = TicketType.create!([...])
|
ticket_types = TicketType.create!([...])
|
||||||
|
promotion_codes = PromotionCode.create!([...])
|
||||||
|
orders = Order.create!([...])
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Available Development Tools
|
## 🛠️ Available Development Tools
|
||||||
@@ -280,6 +531,9 @@ ast-grep --pattern 'validates :$FIELD, presence: true' --lang ruby
|
|||||||
|
|
||||||
# Mass rename across multiple files
|
# Mass rename across multiple files
|
||||||
ast-grep --pattern 'old_method_name($$$ARGS)' --rewrite 'new_method_name($$$ARGS)' --lang ruby --update-all
|
ast-grep --pattern 'old_method_name($$$ARGS)' --rewrite 'new_method_name($$$ARGS)' --lang ruby --update-all
|
||||||
|
|
||||||
|
# Find all order-related validations
|
||||||
|
ast-grep --pattern 'validates :status, inclusion: { in: \%w[...] }' --lang ruby
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Best Practices:
|
#### Best Practices:
|
||||||
@@ -288,13 +542,25 @@ ast-grep --pattern 'old_method_name($$$ARGS)' --rewrite 'new_method_name($$$ARGS
|
|||||||
- Test changes in a branch before applying to main codebase
|
- Test changes in a branch before applying to main codebase
|
||||||
- Particularly useful for Rails conventions and ActiveRecord pattern updates
|
- Particularly useful for Rails conventions and ActiveRecord pattern updates
|
||||||
|
|
||||||
|
### Modern Rails 8 Stack
|
||||||
|
- **Solid Queue**: Background job processing
|
||||||
|
- **Solid Cache**: Fast caching layer
|
||||||
|
- **Solid Cable**: Action Cable over Redis
|
||||||
|
- **Propshaft**: Asset pipeline
|
||||||
|
- **Kamal**: Deployment tooling
|
||||||
|
- **Thruster**: Performance optimization
|
||||||
|
|
||||||
## 📝 Code Style & Conventions
|
## 📝 Code Style & Conventions
|
||||||
|
|
||||||
- **Ruby Style**: Follow Rails conventions and Rubocop rules
|
- **Ruby Style**: Follow Rails conventions and Rubocop rules
|
||||||
- **Database**: Use Rails migrations for all schema changes
|
- **Database**: Use Rails migrations for all schema changes
|
||||||
- **JavaScript**: Stimulus controllers for interactive behavior
|
- **JavaScript**: Stimulus controllers for interactive behavior
|
||||||
- **CSS**: Tailwind utility classes with custom components
|
- **CSS**: Tailwind utility classes with custom components
|
||||||
|
- **Service Layer**: Complex business logic in service objects
|
||||||
|
- **Background Jobs**: Async processing for long-running tasks
|
||||||
|
- **API Design**: RESTful principles with versioning
|
||||||
- **Documentation**: Inline comments for complex business logic
|
- **Documentation**: Inline comments for complex business logic
|
||||||
- **Mass Changes**: Use `ast-grep` for structural code replacements instead of simple find/replace
|
- **Mass Changes**: Use `ast-grep` for structural code replacements instead of simple find/replace
|
||||||
|
- **Testing**: Comprehensive test coverage for all business logic
|
||||||
|
|
||||||
This architecture provides a solid foundation for a scalable ticket selling platform with proper separation of concerns, security, and user experience.
|
This architecture provides a solid foundation for a scalable ticket selling platform with proper separation of concerns, security, and user experience, featuring modern Rails 8 capabilities and a comprehensive order management system.
|
||||||
@@ -130,11 +130,16 @@ class OrdersController < ApplicationController
|
|||||||
if params[:promotion_code].present?
|
if params[:promotion_code].present?
|
||||||
promotion_code = PromotionCode.valid.find_by(code: params[:promotion_code].upcase)
|
promotion_code = PromotionCode.valid.find_by(code: params[:promotion_code].upcase)
|
||||||
if promotion_code
|
if promotion_code
|
||||||
# Apply the promotion code to the order
|
# Check if promotion code is already applied to this order
|
||||||
@order.promotion_codes << promotion_code
|
if @order.promotion_codes.include?(promotion_code)
|
||||||
@order.calculate_total!
|
flash.now[:alert] = "Ce code promotionnel est déjà appliqué à cette commande"
|
||||||
@total_amount = @order.total_amount_cents
|
else
|
||||||
flash.now[:notice] = "Code promotionnel appliqué: #{promotion_code.code}"
|
# Apply the promotion code to the order
|
||||||
|
@order.promotion_codes << promotion_code
|
||||||
|
@order.calculate_total!
|
||||||
|
@total_amount = @order.total_amount_cents
|
||||||
|
flash.now[:notice] = "Code promotionnel appliqué: #{promotion_code.code}"
|
||||||
|
end
|
||||||
else
|
else
|
||||||
flash.now[:alert] = "Code promotionnel invalide"
|
flash.now[:alert] = "Code promotionnel invalide"
|
||||||
end
|
end
|
||||||
@@ -302,7 +307,14 @@ class OrdersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_stripe_session
|
def create_stripe_session
|
||||||
|
# Calculate the discount amount per ticket to distribute the promotion evenly
|
||||||
|
total_tickets = @order.tickets.count
|
||||||
|
discount_per_ticket = @order.discount_amount_cents / total_tickets if total_tickets > 0
|
||||||
|
|
||||||
line_items = @order.tickets.map do |ticket|
|
line_items = @order.tickets.map do |ticket|
|
||||||
|
# Apply discount proportionally to each ticket
|
||||||
|
discounted_price = [ticket.price_cents - discount_per_ticket.to_i, 0].max
|
||||||
|
|
||||||
{
|
{
|
||||||
price_data: {
|
price_data: {
|
||||||
currency: "eur",
|
currency: "eur",
|
||||||
@@ -310,7 +322,7 @@ class OrdersController < ApplicationController
|
|||||||
name: "#{@order.event.name} - #{ticket.ticket_type.name}",
|
name: "#{@order.event.name} - #{ticket.ticket_type.name}",
|
||||||
description: ticket.ticket_type.description
|
description: ticket.ticket_type.description
|
||||||
},
|
},
|
||||||
unit_amount: ticket.price_cents
|
unit_amount: discounted_price
|
||||||
},
|
},
|
||||||
quantity: 1
|
quantity: 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class Order < ApplicationRecord
|
|||||||
validates :payment_attempts, presence: true,
|
validates :payment_attempts, presence: true,
|
||||||
numericality: { greater_than_or_equal_to: 0 }
|
numericality: { greater_than_or_equal_to: 0 }
|
||||||
|
|
||||||
|
# Custom validation to prevent duplicate promotion codes
|
||||||
|
validate :no_duplicate_promotion_codes
|
||||||
|
|
||||||
# Stripe invoice ID for accounting records
|
# Stripe invoice ID for accounting records
|
||||||
attr_accessor :stripe_invoice_id
|
attr_accessor :stripe_invoice_id
|
||||||
|
|
||||||
@@ -188,4 +191,12 @@ class Order < ApplicationRecord
|
|||||||
def draft?
|
def draft?
|
||||||
status == "draft"
|
status == "draft"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Prevent duplicate promotion codes on the same order
|
||||||
|
def no_duplicate_promotion_codes
|
||||||
|
promotion_code_ids = promotion_codes.map(&:id)
|
||||||
|
if promotion_code_ids.size != promotion_code_ids.uniq.size
|
||||||
|
errors.add(:promotion_codes, "ne peuvent pas contenir de codes en double")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user