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:
366
AGENTS.md
366
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`)
|
||||
- **Devise Integration**: Complete authentication system with registration, login, password reset
|
||||
- **Relationships**: Users can create events and purchase tickets
|
||||
- **Validations**: Email format, password strength, optional name fields
|
||||
- **Professional Users**: `is_professionnal` field for event promoters with enhanced permissions
|
||||
- **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`)
|
||||
- **States**: `draft`, `published`, `canceled`, `sold_out` with enum management
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **Invoice Service**: Post-payment invoice generation with StripeInvoiceService
|
||||
|
||||
### Database Schema Key Points
|
||||
|
||||
```sql
|
||||
-- Users table (managed by Devise)
|
||||
-- Users table (enhanced with professional features)
|
||||
CREATE TABLE users (
|
||||
id bigint PRIMARY KEY,
|
||||
email varchar(255) UNIQUE NOT NULL,
|
||||
encrypted_password varchar(255) NOT NULL,
|
||||
first_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.
|
||||
);
|
||||
|
||||
-- Events table
|
||||
-- Events table (enhanced with order management)
|
||||
CREATE TABLE events (
|
||||
id bigint PRIMARY KEY,
|
||||
user_id bigint REFERENCES users(id),
|
||||
@@ -59,6 +85,40 @@ CREATE TABLE events (
|
||||
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
|
||||
CREATE TABLE ticket_types (
|
||||
id bigint PRIMARY KEY,
|
||||
@@ -73,10 +133,11 @@ CREATE TABLE ticket_types (
|
||||
minimum_age integer
|
||||
);
|
||||
|
||||
-- Individual tickets with QR codes
|
||||
-- Individual tickets with QR codes (enhanced with order association)
|
||||
CREATE TABLE tickets (
|
||||
id bigint PRIMARY KEY,
|
||||
user_id bigint REFERENCES users(id),
|
||||
order_id bigint REFERENCES orders(id),
|
||||
ticket_type_id bigint REFERENCES ticket_types(id),
|
||||
qr_code varchar(255) UNIQUE NOT NULL,
|
||||
price_cents integer NOT NULL,
|
||||
@@ -107,38 +168,113 @@ CREATE TABLE tickets (
|
||||
.limit(5)
|
||||
```
|
||||
|
||||
### 2. Stripe Payment Flow
|
||||
### 2. Order Management Flow (`app/controllers/orders_controller.rb`)
|
||||
|
||||
#### Checkout Initiation (`events#checkout`)
|
||||
1. **Cart Validation**: Parse JSON cart data, validate ticket types and quantities
|
||||
2. **Availability Check**: Ensure sufficient tickets available before payment
|
||||
3. **Stripe Session**: Create checkout session with line items, success/cancel URLs
|
||||
4. **Metadata Storage**: Store order details in Stripe session metadata for later retrieval
|
||||
#### Order Creation and Payment
|
||||
1. **Cart-to-Order Conversion**: Convert shopping cart to draft order with 15-minute expiration
|
||||
2. **Platform Fee Calculation**: Automatic calculation of €0.50 fixed + 1.5% per ticket
|
||||
3. **Promotion Code Application**: Real-time discount validation and application
|
||||
4. **Stripe Checkout Session**: Create payment session with order metadata
|
||||
5. **Payment Retry**: Support for multiple payment attempts with proper tracking
|
||||
|
||||
```ruby
|
||||
# Key Stripe configuration
|
||||
session = Stripe::Checkout::Session.create({
|
||||
payment_method_types: ['card'],
|
||||
line_items: line_items,
|
||||
mode: 'payment',
|
||||
success_url: payment_success_url(event_id: @event.id, session_id: '{CHECKOUT_SESSION_ID}'),
|
||||
cancel_url: event_url(@event.slug, @event),
|
||||
customer_email: current_user.email,
|
||||
metadata: {
|
||||
event_id: @event.id,
|
||||
user_id: current_user.id,
|
||||
order_items: order_items.to_json
|
||||
}
|
||||
})
|
||||
# Order creation with platform fees
|
||||
def create
|
||||
@order = Order.new(order_params)
|
||||
@order.user = current_user
|
||||
@order.calculate_platform_fee
|
||||
@order.set_expiration
|
||||
|
||||
if @order.save
|
||||
session = create_stripe_checkout_session(@order)
|
||||
redirect_to session.url, allow_other_host: true
|
||||
else
|
||||
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`)
|
||||
1. **Session Retrieval**: Get Stripe session with payment status
|
||||
2. **Ticket Creation**: Generate tickets based on order items from metadata
|
||||
3. **QR Code Generation**: Automatic unique QR code creation via model callbacks
|
||||
4. **Success Page**: Display tickets with download links
|
||||
#### Payment Confirmation and Invoice Generation
|
||||
1. **Order Status Update**: Transition from pending_payment to paid
|
||||
2. **Ticket Generation**: Create tickets associated with the order
|
||||
3. **Stripe Invoice Creation**: Async invoice generation for accounting
|
||||
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
|
||||
class TicketPdfGenerator
|
||||
@@ -167,12 +303,20 @@ class TicketPdfGenerator
|
||||
end
|
||||
```
|
||||
|
||||
### 4. Frontend Cart Management (`app/javascript/controllers/ticket_cart_controller.js`)
|
||||
### 7. Frontend Architecture
|
||||
|
||||
- **Stimulus Controller**: Manages cart state and interactions
|
||||
- **Authentication Check**: Validates user login before checkout
|
||||
- **Session Storage**: Preserves cart when redirecting to login
|
||||
- **Dynamic Updates**: Real-time cart total and ticket count updates
|
||||
#### Enhanced Stimulus Controllers
|
||||
- **ticket_selection_controller.js**: Advanced cart management with real-time updates
|
||||
- **event_form_controller.js**: Dynamic event creation with location services
|
||||
- **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
|
||||
|
||||
@@ -185,6 +329,16 @@ validates :latitude, numericality: {
|
||||
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
|
||||
before_validation :generate_qr_code, on: :create
|
||||
def generate_qr_code
|
||||
@@ -200,11 +354,47 @@ end
|
||||
# Authentication for sensitive actions
|
||||
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
|
||||
def event_params
|
||||
params.require(:event).permit(:name, :description, :venue_name, :venue_address,
|
||||
:latitude, :longitude, :start_time, :image)
|
||||
def order_params
|
||||
params.require(:order).permit(:promotion_code, order_items_attributes: [:ticket_type_id, :quantity])
|
||||
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
|
||||
```
|
||||
|
||||
@@ -212,6 +402,7 @@ end
|
||||
- **Metric Cards**: Reusable component for dashboard statistics
|
||||
- **Event Items**: Consistent event display across pages
|
||||
- **Flash Messages**: Centralized notification system
|
||||
- **Order Components**: Reusable order display and management components
|
||||
|
||||
## 🚀 Deployment Considerations
|
||||
|
||||
@@ -223,14 +414,28 @@ STRIPE_SECRET_KEY=sk_live_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
DATABASE_URL=mysql2://user:pass@host/db
|
||||
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
|
||||
```sql
|
||||
-- Performance indexes for common queries
|
||||
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_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
|
||||
@@ -238,22 +443,68 @@ CREATE INDEX idx_ticket_types_event ON ticket_types (event_id);
|
||||
- **Strong Parameters**: All user inputs filtered
|
||||
- **Authentication**: Devise handles session security
|
||||
- **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
|
||||
|
||||
### Key Test Cases
|
||||
1. **User Authentication**: Registration, login, logout flows
|
||||
2. **Event Creation**: Validation, state management, relationships
|
||||
3. **Booking Process**: Cart validation, payment processing, ticket generation
|
||||
4. **PDF Generation**: QR code uniqueness, ticket format
|
||||
5. **Dashboard Metrics**: Query accuracy, performance
|
||||
2. **Professional User Onboarding**: Multi-step onboarding process
|
||||
3. **Event Creation**: Validation, state management, relationships
|
||||
4. **Order Management**: Cart-to-order conversion, payment processing, expiration
|
||||
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
|
||||
```ruby
|
||||
# Creates test users, events, and ticket types
|
||||
# Creates comprehensive test data
|
||||
users = User.create!([...])
|
||||
events = Event.create!([...])
|
||||
ticket_types = TicketType.create!([...])
|
||||
promotion_codes = PromotionCode.create!([...])
|
||||
orders = Order.create!([...])
|
||||
```
|
||||
|
||||
## 🛠️ Available Development Tools
|
||||
@@ -280,6 +531,9 @@ ast-grep --pattern 'validates :$FIELD, presence: true' --lang ruby
|
||||
|
||||
# Mass rename across multiple files
|
||||
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:
|
||||
@@ -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
|
||||
- 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
|
||||
|
||||
- **Ruby Style**: Follow Rails conventions and Rubocop rules
|
||||
- **Database**: Use Rails migrations for all schema changes
|
||||
- **JavaScript**: Stimulus controllers for interactive behavior
|
||||
- **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
|
||||
- **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?
|
||||
promotion_code = PromotionCode.valid.find_by(code: params[:promotion_code].upcase)
|
||||
if promotion_code
|
||||
# Check if promotion code is already applied to this order
|
||||
if @order.promotion_codes.include?(promotion_code)
|
||||
flash.now[:alert] = "Ce code promotionnel est déjà appliqué à cette commande"
|
||||
else
|
||||
# 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
|
||||
flash.now[:alert] = "Code promotionnel invalide"
|
||||
end
|
||||
@@ -302,7 +307,14 @@ class OrdersController < ApplicationController
|
||||
end
|
||||
|
||||
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|
|
||||
# Apply discount proportionally to each ticket
|
||||
discounted_price = [ticket.price_cents - discount_per_ticket.to_i, 0].max
|
||||
|
||||
{
|
||||
price_data: {
|
||||
currency: "eur",
|
||||
@@ -310,7 +322,7 @@ class OrdersController < ApplicationController
|
||||
name: "#{@order.event.name} - #{ticket.ticket_type.name}",
|
||||
description: ticket.ticket_type.description
|
||||
},
|
||||
unit_amount: ticket.price_cents
|
||||
unit_amount: discounted_price
|
||||
},
|
||||
quantity: 1
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ class Order < ApplicationRecord
|
||||
validates :payment_attempts, presence: true,
|
||||
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
|
||||
attr_accessor :stripe_invoice_id
|
||||
|
||||
@@ -188,4 +191,12 @@ class Order < ApplicationRecord
|
||||
def draft?
|
||||
status == "draft"
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user