diff --git a/AGENTS.md b/AGENTS.md index 6a2fdb1..b30317b 100755 --- a/AGENTS.md +++ b/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 @@ -148,10 +284,10 @@ class TicketPdfGenerator pdf.fill_color "2D1B69" pdf.font "Helvetica", style: :bold, size: 24 pdf.text "ApéroNight", align: :center - + # Event details pdf.text ticket.event.name, align: :center - + # QR Code generation qr_code_data = { ticket_id: ticket.id, @@ -159,7 +295,7 @@ class TicketPdfGenerator event_id: ticket.event.id, user_id: ticket.user.id }.to_json - + qrcode = RQRCode::QRCode.new(qr_code_data) pdf.print_qr_code(qrcode, extent: 120, align: :center) end.render @@ -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 @@ -180,11 +324,21 @@ end ```ruby # Event validations validates :name, presence: true, length: { minimum: 3, maximum: 100 } -validates :latitude, numericality: { - greater_than_or_equal_to: -90, - less_than_or_equal_to: 90 +validates :latitude, numericality: { + greater_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 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb index f6a6e29..b3f572c 100644 --- a/app/controllers/orders_controller.rb +++ b/app/controllers/orders_controller.rb @@ -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 - # 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}" + # 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 } diff --git a/app/models/order.rb b/app/models/order.rb index 8fea6d7..7034161 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -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