diff --git a/BACKLOG.md b/BACKLOG.md
index e0750ac..b18ad6d 100755
--- a/BACKLOG.md
+++ b/BACKLOG.md
@@ -17,6 +17,21 @@
- [ ] feat: Dynamic pricing based on demand
- [ ] feat: Profesionnal account. User can ask to change from a customer to a professionnal account to create and manage events.
- [ ] feat: User can choose to create a professionnal account on sign-up page to be allowed to create and manage events
+- [ ] feat: Payout system for promoters (automated/manual payment processing)
+- [ ] feat: Platform commission tracking and fee structure display
+- [ ] feat: Tax reporting and revenue export for promoters
+- [ ] feat: Event update notifications to ticket holders
+- [ ] feat: Marketing tools with promotional codes and discounts
+- [ ] feat: Customer support messaging between promoters and attendees
+- [ ] feat: Attendance tracking (who showed up vs tickets sold)
+- [ ] feat: Customer insights and demographics for promoters
+- [ ] feat: Performance metrics and conversion rate analytics
+- [ ] feat: Event templates for reusing successful formats
+- [ ] feat: Staff management and role assignment for promoter teams
+- [ ] feat: Multiple payment gateway options
+- [ ] feat: Calendar sync (Google Calendar, Outlook integration)
+- [ ] feat: Social media auto-posting for events
+- [ ] feat: CRM and email marketing tool integrations
### Low Priority
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 88ba9c0..00d5eb4 100755
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -18,6 +18,6 @@ function initializeLucideIcons() {
// Run on initial page load
document.addEventListener('DOMContentLoaded', initializeLucideIcons);
-// Run on Turbo navigation (Rails 7+ SPA behavior)
+// Run on Turbo navigation (Rails 7+ SPA behavior)
document.addEventListener('turbo:render', initializeLucideIcons);
document.addEventListener('turbo:frame-render', initializeLucideIcons);
diff --git a/app/javascript/controllers/event_form_controller.js b/app/javascript/controllers/event_form_controller.js
index 24ae3e2..e61ff29 100644
--- a/app/javascript/controllers/event_form_controller.js
+++ b/app/javascript/controllers/event_form_controller.js
@@ -1,17 +1,18 @@
import { Controller } from "@hotwired/stimulus"
+import slug from 'slug'
export default class extends Controller {
static targets = ["name", "slug", "latitude", "longitude", "address", "mapLinksContainer"]
- static values = {
+ static values = {
geocodeDelay: { type: Number, default: 1500 } // Delay before auto-geocoding
}
connect() {
this.geocodeTimeout = null
-
+
// Initialize map links if we have an address and coordinates already exist
- if (this.hasAddressTarget && this.addressTarget.value.trim() &&
- this.hasLatitudeTarget && this.hasLongitudeTarget &&
+ if (this.hasAddressTarget && this.addressTarget.value.trim() &&
+ this.hasLatitudeTarget && this.hasLongitudeTarget &&
this.latitudeTarget.value && this.longitudeTarget.value) {
this.updateMapLinks()
}
@@ -26,14 +27,8 @@ export default class extends Controller {
// Generate slug from name
generateSlug() {
const name = this.nameTarget.value
- const slug = name
- .toLowerCase()
- .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
- .replace(/\s+/g, '-') // Replace spaces with hyphens
- .replace(/-+/g, '-') // Replace multiple hyphens with single
- .replace(/^-|-$/g, '') // Remove leading/trailing hyphens
-
- this.slugTarget.value = slug
+
+ this.slugTarget.value = slug(name)
}
// Handle address changes with debounced geocoding
@@ -44,7 +39,7 @@ export default class extends Controller {
}
const address = this.addressTarget.value.trim()
-
+
if (!address) {
this.clearCoordinates()
this.clearMapLinks()
@@ -76,27 +71,27 @@ export default class extends Controller {
const position = await this.getCurrentPositionPromise(options)
const lat = position.coords.latitude
const lng = position.coords.longitude
-
+
// Set coordinates first
this.latitudeTarget.value = lat.toFixed(6)
this.longitudeTarget.value = lng.toFixed(6)
-
+
// Then reverse geocode to get address
const address = await this.reverseGeocode(lat, lng)
-
+
if (address) {
this.addressTarget.value = address
this.showLocationSuccess("Position actuelle détectée et adresse mise à jour!")
} else {
this.showLocationSuccess("Position actuelle détectée!")
}
-
+
this.updateMapLinks()
-
+
} catch (error) {
this.hideLocationLoading()
let message = "Erreur lors de la récupération de la localisation."
-
+
switch(error.code) {
case error.PERMISSION_DENIED:
message = "L'accès à la localisation a été refusé."
@@ -108,7 +103,7 @@ export default class extends Controller {
message = "La demande de localisation a expiré."
break
}
-
+
this.showLocationError(message)
}
}
@@ -125,11 +120,11 @@ export default class extends Controller {
try {
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`)
const data = await response.json()
-
+
if (data && data.display_name) {
return data.display_name
}
-
+
return null
} catch (error) {
console.log("Reverse geocoding failed:", error)
@@ -145,7 +140,7 @@ export default class extends Controller {
}
// If we already have coordinates, just update map links
- if (this.hasLatitudeTarget && this.hasLongitudeTarget &&
+ if (this.hasLatitudeTarget && this.hasLongitudeTarget &&
this.latitudeTarget.value && this.longitudeTarget.value) {
this.updateMapLinks()
this.showLocationSuccess("Liens de carte mis à jour!")
@@ -163,11 +158,11 @@ export default class extends Controller {
}
const address = this.addressTarget.value.trim()
-
+
try {
this.showLocationLoading()
const result = await this.performGeocode(address)
-
+
if (result) {
this.latitudeTarget.value = result.lat
this.longitudeTarget.value = result.lng
@@ -187,7 +182,7 @@ export default class extends Controller {
async geocodeAddressQuiet(address) {
try {
const result = await this.performGeocode(address)
-
+
if (result) {
this.latitudeTarget.value = result.lat
this.longitudeTarget.value = result.lng
@@ -207,7 +202,7 @@ export default class extends Controller {
const encodedAddress = encodeURIComponent(address)
const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodedAddress}&format=json&limit=1`)
const data = await response.json()
-
+
if (data && data.length > 0) {
const result = data[0]
return {
@@ -215,7 +210,7 @@ export default class extends Controller {
lng: parseFloat(result.lon).toFixed(6)
}
}
-
+
return null
}
@@ -226,7 +221,7 @@ export default class extends Controller {
const lat = parseFloat(this.latitudeTarget.value)
const lng = parseFloat(this.longitudeTarget.value)
const address = this.hasAddressTarget ? this.addressTarget.value.trim() : ""
-
+
if (isNaN(lat) || isNaN(lng) || !address) {
this.clearMapLinks()
return
@@ -239,7 +234,7 @@ export default class extends Controller {
// Generate map links HTML
generateMapLinks(lat, lng, address) {
const encodedAddress = encodeURIComponent(address)
-
+
const providers = {
openstreetmap: {
name: "OpenStreetMap",
@@ -247,7 +242,7 @@ export default class extends Controller {
icon: "🗺️"
},
google: {
- name: "Google Maps",
+ name: "Google Maps",
url: `https://www.google.com/maps/search/${encodedAddress}/@${lat},${lng},16z`,
icon: "🔍"
},
@@ -266,7 +261,7 @@ export default class extends Controller {
${Object.entries(providers).map(([key, provider]) => `
-
${provider.icon}
${provider.name}
@@ -327,7 +322,7 @@ export default class extends Controller {
showMessage(id, message, type) {
const colors = {
info: "bg-blue-50 border-blue-200 text-blue-800",
- success: "bg-green-50 border-green-200 text-green-800",
+ success: "bg-green-50 border-green-200 text-green-800",
error: "bg-red-50 border-red-200 text-red-800",
warning: "bg-yellow-50 border-yellow-200 text-yellow-800"
}
@@ -372,4 +367,4 @@ export default class extends Controller {
this.hideMessage("location-error")
this.hideMessage("geocoding-warning")
}
-}
\ No newline at end of file
+}
diff --git a/app/models/order.rb b/app/models/order.rb
index 41eccb0..e3e17a9 100644
--- a/app/models/order.rb
+++ b/app/models/order.rb
@@ -1,6 +1,6 @@
class Order < ApplicationRecord
# === Constants ===
- DRAFT_EXPIRY_TIME = 30.minutes
+ DRAFT_EXPIRY_TIME = 15.minutes
MAX_PAYMENT_ATTEMPTS = 3
# === Associations ===
diff --git a/app/views/components/_delete_account.html.erb b/app/views/components/_delete_account.html.erb
new file mode 100644
index 0000000..d10c39f
--- /dev/null
+++ b/app/views/components/_delete_account.html.erb
@@ -0,0 +1,17 @@
+
+
+
Supprimer mon compte
+
+ Vous êtes certain de vouloir supprimer votre compte ? Cette action est irréversible.
+
+ <%= button_to registration_path(resource_name),
+ data: {
+ confirm: "Êtes-vous certain ?",
+ turbo_confirm: "Êtes-vous certain ?"
+ },
+ method: :delete,
+ class: "group relative w-full flex justify-center items-center py-3 px-4 border border-red-300 text-sm font-semibold rounded-xl text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200" do %>
+
+ Supprimer mon compte
+ <% end %>
+
diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb
index 014ece3..66f49b3 100755
--- a/app/views/devise/registrations/edit.html.erb
+++ b/app/views/devise/registrations/edit.html.erb
@@ -7,16 +7,16 @@
<% end %>
- Modifier votre compte
+ Modifier vos informations de sécurité
- Gérez vos informations et préférences
+ Gérez vos informations et préférences de sécurité
Informations du compte
-
+
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: "space-y-6" }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
@@ -39,35 +39,6 @@
<% end %>
-
-
- <%= f.label :password, "Nouveau mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
-
-
-
-
- <%= f.password_field :password, autocomplete: "new-password",
- placeholder: "Laisser vide si vous ne souhaitez pas le changer",
- class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
-
- <% if @minimum_password_length %>
-
<%= t('devise.registrations.new.minimum_password_length', count: @minimum_password_length) %>
- <% end %>
-
-
-
- <%= f.label :password_confirmation, "Confirmer le nouveau mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
-
-
-
-
- <%= f.password_field :password_confirmation, autocomplete: "new-password",
- placeholder: "Confirmez votre nouveau mot de passe",
- class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
-
-
-
-
<%= f.label :current_password, "Mot de passe actuel", class: "block text-sm font-semibold text-gray-700 mb-2" %>
@@ -82,6 +53,35 @@
+
+
+ <%= f.label :password, "Nouveau mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
+
+
+
+
+ <%= f.password_field :password, autocomplete: "new-password",
+ placeholder: "Laisser vide si vous ne souhaitez pas le changer",
+ class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
+
+ <% if @minimum_password_length %>
+
<%= t('devise.registrations.new.minimum_password_length', count: @minimum_password_length) %>
+ <% end %>
+
+
+
+ <%= f.label :password_confirmation, "Confirmer le nouveau mot de passe", class: "block text-sm font-semibold text-gray-700 mb-2" %>
+
+
+
+
+ <%= f.password_field :password_confirmation, autocomplete: "new-password",
+ placeholder: "Confirmez votre nouveau mot de passe",
+ class: "block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors" %>
+
+
+
+
<%= f.button type: "submit", class: "group relative w-full flex justify-center items-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" do %>
@@ -91,30 +91,14 @@
<% end %>
-
-
-
Supprimer mon compte
-
- <%= t('devise.registrations.edit.unhappy') %> Cette action est irréversible.
-
- <%= button_to registration_path(resource_name),
- data: {
- confirm: t('devise.registrations.edit.confirm_delete'),
- turbo_confirm: t('devise.registrations.edit.confirm_delete')
- },
- method: :delete,
- class: "group relative w-full flex justify-center items-center py-3 px-4 border border-red-300 text-sm font-semibold rounded-xl text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200" do %>
-
- <%= t('devise.registrations.edit.delete_account') %>
- <% end %>
-
+ <%# render "components/delete_account" %>
<%= link_to :back, class: "inline-flex items-center text-purple-600 hover:text-purple-500 transition-colors" do %>
- <%= t('devise.registrations.edit.back') %>
+ Retour
<% end %>
-
\ No newline at end of file
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 67199ee..cf76369 100755
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,5 +1,5 @@
-
+
<%= content_for(:title) || "Aperonight" %>
@@ -10,35 +10,11 @@
<%= yield :head %>
-
-
-
-
-
-
-
+
+
+
-
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
diff --git a/db/migrate/20250816145933_devise_create_users.rb b/db/migrate/20250816145933_devise_create_users.rb
index bba5b16..0ce256a 100755
--- a/db/migrate/20250816145933_devise_create_users.rb
+++ b/db/migrate/20250816145933_devise_create_users.rb
@@ -37,11 +37,12 @@ class DeviseCreateUsers < ActiveRecord::Migration[8.0]
t.string :first_name, null: true # Prénom
# Company informations
- t.string :company_name, null: true # Nom de la société
- # t.string :company_address, null: true # Adresse de la société
- # t.string :company_phone, null: true # Téléphone de la société
- # t.string :company_email, null: true # Email de la société
- # t.string :company_website, null: true # Site web de la société
+ t.boolean :is_professionnal, default: false, null: true
+ t.string :company_name, null: true # Nom de la société
+ t.string :company_address, null: true # Adresse de la société
+ t.string :company_phone, null: true # Téléphone de la société
+ t.string :company_email, null: true # Email de la société
+ t.string :company_website, null: true # Site web de la société
# Link user to Stripe customer
# We assume user does not have a stripe account yet
diff --git a/db/migrate/20250823145902_create_events.rb b/db/migrate/20250823145902_create_events.rb
index e0cca4f..d91c2bf 100755
--- a/db/migrate/20250823145902_create_events.rb
+++ b/db/migrate/20250823145902_create_events.rb
@@ -10,8 +10,12 @@ class CreateEvents < ActiveRecord::Migration[8.0]
t.string :venue_address, null: false
t.datetime :start_time
t.datetime :end_time
+
+ # Latitude and longitude of the place
t.decimal :latitude, precision: 10, scale: 6, null: false
t.decimal :longitude, precision: 10, scale: 6, null: false
+
+ # Only admin or later premium promoters could select this
t.boolean :featured, default: false, null: false
t.references :user, null: false, foreign_key: false
diff --git a/db/schema.rb b/db/schema.rb
index 4b736c6..2d5757b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_09_08_092220) do
+ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "slug", null: false
@@ -90,11 +90,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_08_092220) do
t.datetime "remember_created_at"
t.string "last_name"
t.string "first_name"
+ t.boolean "is_professionnal", default: false
t.string "company_name"
+ t.string "company_address"
+ t.string "company_phone"
+ t.string "company_email"
+ t.string "company_website"
t.string "stripe_customer_id"
+ t.boolean "onboarding_completed", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.boolean "onboarding_completed", default: false, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
diff --git a/docs/invoice-system-implementation.md b/docs/invoice-system-implementation.md
deleted file mode 100644
index d399c88..0000000
--- a/docs/invoice-system-implementation.md
+++ /dev/null
@@ -1,572 +0,0 @@
-# Invoice System Implementation Guide
-
-## Overview
-
-This document outlines various approaches for adding invoice generation to the ApéroNight event ticketing system, based on the current Rails architecture with Stripe integration.
-
-## Current System Analysis
-
-### Existing Infrastructure
-- **Rails 8.0.2** with MySQL database
-- **Stripe** payment processing via Checkout Sessions
-- **Prawn** gem for PDF generation (tickets)
-- **Order/Ticket** models with pricing logic
-- **Devise** for user authentication
-
-### Current Payment Flow
-```
-User selects tickets → Order created (draft) → Stripe Checkout → Payment → Order marked as paid → Tickets activated → PDF tickets emailed
-```
-
-## Invoice Generation Approaches
-
-### 1. Extend Existing PDF Infrastructure (Recommended)
-
-**Pros**:
-- Leverages existing Prawn setup
-- Consistent styling with tickets
-- No additional dependencies
-
-**Implementation**:
-```ruby
-# app/services/invoice_pdf_generator.rb
-class InvoicePdfGenerator
- attr_reader :order
-
- def initialize(order)
- @order = order
- end
-
- def generate
- Prawn::Document.new(page_size: "A4", margin: 40) do |pdf|
- # Header
- pdf.fill_color "2D1B69"
- pdf.font "Helvetica", style: :bold, size: 24
- pdf.text "ApéroNight Invoice", align: :center
- pdf.move_down 20
-
- # Invoice details
- pdf.fill_color "000000"
- pdf.font "Helvetica", size: 12
-
- # Invoice number and date
- pdf.text "Invoice #: #{@order.invoice_number}"
- pdf.text "Date: #{@order.created_at.strftime('%B %d, %Y')}"
- pdf.text "Due Date: #{@order.created_at.strftime('%B %d, %Y')}" # Same day for events
- pdf.move_down 20
-
- # Customer details
- pdf.text "Bill To:", style: :bold
- pdf.text @order.user.email
- pdf.move_down 20
-
- # Order details table
- pdf.text "Event: #{@order.event.name}", style: :bold, size: 14
- pdf.move_down 10
-
- # Line items
- items = [["Description", "Quantity", "Unit Price", "Total"]]
-
- @order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets|
- items << [
- "#{ticket_type.name} - #{@order.event.name}",
- tickets.count.to_s,
- "€#{ticket_type.price_cents / 100.0}",
- "€#{(tickets.count * ticket_type.price_cents) / 100.0}"
- ]
- end
-
- pdf.table(items, header: true, width: pdf.bounds.width) do
- row(0).font_style = :bold
- columns(1..3).align = :right
- end
-
- pdf.move_down 20
-
- # Total
- pdf.text "Total: €#{@order.total_amount_cents / 100.0}",
- style: :bold, size: 16, align: :right
-
- # Footer
- pdf.move_down 40
- pdf.text "Thank you for your purchase!", align: :center
- pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}",
- align: :center, size: 8
- end.render
- end
-end
-```
-
-### 2. HTML-to-PDF Solutions
-
-**Using WickedPdf**:
-```ruby
-# Gemfile
-gem 'wicked_pdf'
-
-# app/controllers/invoices_controller.rb
-def show
- @order = current_user.orders.find(params[:order_id])
-
- respond_to do |format|
- format.html
- format.pdf do
- render pdf: "invoice_#{@order.id}",
- template: 'invoices/show.html.erb',
- layout: 'pdf'
- end
- end
-end
-```
-
-### 3. Third-Party Services
-
-**Stripe Invoicing Integration**:
-```ruby
-# app/services/stripe_invoice_service.rb
-class StripeInvoiceService
- def initialize(order)
- @order = order
- end
-
- def create_post_payment_invoice
- customer = find_or_create_stripe_customer
-
- invoice = Stripe::Invoice.create({
- customer: customer.id,
- collection_method: 'charge_automatically',
- paid: true, # Already paid via checkout
- metadata: {
- order_id: @order.id,
- user_id: @order.user.id
- }
- })
-
- # Add line items
- @order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets|
- Stripe::InvoiceItem.create({
- customer: customer.id,
- invoice: invoice.id,
- amount: ticket_type.price_cents * tickets.count,
- currency: 'eur',
- description: "#{@order.event.name} - #{ticket_type.name} (#{tickets.count}x)"
- })
- end
-
- invoice.finalize_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,
- metadata: { user_id: @order.user.id }
- })
-
- @order.user.update(stripe_customer_id: customer.id)
- customer
- end
- end
-end
-```
-
-## Stripe Payment Methods Comparison
-
-### Payment Intents vs Invoicing vs Checkout Sessions
-
-| Feature | Payment Intents | Stripe Invoicing | Checkout Sessions (Current) |
-|---------|----------------|------------------|---------------------------|
-| **Timing** | Immediate | Deferred (days/weeks) | Immediate |
-| **User Experience** | Custom UI on your site | Stripe-hosted invoice page | Stripe-hosted checkout |
-| **Payment Methods** | Cards, wallets, BNPL | Cards, bank transfers, checks | Cards, wallets, BNPL |
-| **Documentation** | Custom receipts | Formal invoices | Stripe receipts |
-| **Integration Complexity** | Medium | Low | Low |
-| **Best For** | Custom checkout flows | B2B billing | Quick implementation |
-
-### For Event Ticketing Use Case
-
-**Current Checkout Sessions are ideal** because:
-- Events require immediate payment confirmation
-- Time-sensitive inventory management
-- Users expect instant ticket delivery
-- Built-in fraud protection
-
-**Recommended**: Keep Checkout Sessions, add invoice generation for accounting records
-
-## Recommended Implementation
-
-### Phase 1: Database Schema
-
-```ruby
-# Migration
-class CreateInvoices < ActiveRecord::Migration[8.0]
- def change
- create_table :invoices do |t|
- t.references :order, null: false, foreign_key: true
- t.string :invoice_number, null: false
- t.integer :total_amount_cents, null: false
- t.string :currency, default: 'eur'
- t.string :status, default: 'issued' # issued, paid, cancelled
- t.datetime :issued_at
- t.datetime :paid_at
- t.string :stripe_invoice_id # Optional: if using Stripe
- t.text :notes
-
- t.timestamps
- end
-
- add_index :invoices, :invoice_number, unique: true
- add_index :invoices, :status
- end
-end
-
-# Add to User model for Stripe integration
-class AddStripeFieldsToUsers < ActiveRecord::Migration[8.0]
- def change
- add_column :users, :stripe_customer_id, :string
- add_index :users, :stripe_customer_id
- end
-end
-```
-
-### Phase 2: Models
-
-```ruby
-# app/models/invoice.rb
-class Invoice < ApplicationRecord
- belongs_to :order
- has_one :user, through: :order
- has_one :event, through: :order
-
- validates :invoice_number, presence: true, uniqueness: true
- validates :total_amount_cents, presence: true, numericality: { greater_than: 0 }
- validates :status, inclusion: { in: %w[issued paid cancelled] }
-
- before_validation :generate_invoice_number, on: :create
- before_validation :set_defaults, on: :create
-
- scope :paid, -> { where(status: 'paid') }
- scope :unpaid, -> { where(status: 'issued') }
-
- def total_amount_euros
- total_amount_cents / 100.0
- end
-
- def generate_pdf
- InvoicePdfGenerator.new(self).generate
- end
-
- def mark_as_paid!
- update!(status: 'paid', paid_at: Time.current)
- end
-
- private
-
- def generate_invoice_number
- return if invoice_number.present?
-
- year = Time.current.year
- month = Time.current.strftime('%m')
-
- # Find highest invoice number for current month
- last_invoice = Invoice.where(
- 'invoice_number LIKE ?',
- "INV-#{year}#{month}-%"
- ).order(:invoice_number).last
-
- if last_invoice
- sequence = last_invoice.invoice_number.split('-').last.to_i + 1
- else
- sequence = 1
- end
-
- self.invoice_number = "INV-#{year}#{month}-#{sequence.to_s.rjust(4, '0')}"
- end
-
- def set_defaults
- self.total_amount_cents = order.total_amount_cents if order
- self.issued_at = Time.current
- self.status = 'paid' if order&.status == 'paid' # Auto-mark as paid for completed orders
- end
-end
-
-# app/models/order.rb - Add invoice association
-class Order < ApplicationRecord
- # ... existing code ...
- has_one :invoice, dependent: :destroy
-
- # Add method to create invoice after payment
- def create_invoice!
- return invoice if invoice.present?
-
- Invoice.create!(
- order: self,
- total_amount_cents: self.total_amount_cents
- )
- end
-end
-```
-
-### Phase 3: Controllers
-
-```ruby
-# app/controllers/invoices_controller.rb
-class InvoicesController < ApplicationController
- before_action :authenticate_user!
- before_action :set_invoice
-
- def show
- # HTML view of invoice
- end
-
- def download_pdf
- pdf = @invoice.generate_pdf
-
- send_data pdf,
- filename: "invoice_#{@invoice.invoice_number}.pdf",
- type: 'application/pdf',
- disposition: 'attachment'
- end
-
- private
-
- def set_invoice
- @invoice = current_user.invoices.joins(:order).find(params[:id])
- rescue ActiveRecord::RecordNotFound
- redirect_to dashboard_path, alert: "Invoice not found"
- end
-end
-
-# Update app/controllers/orders_controller.rb
-class OrdersController < ApplicationController
- # ... existing code ...
-
- def payment_success
- # ... existing payment success logic ...
-
- if stripe_session.payment_status == "paid"
- @order.mark_as_paid!
-
- # Generate invoice
- @order.create_invoice!
-
- # Send confirmation emails with invoice attached
- @order.tickets.each do |ticket|
- begin
- TicketMailer.purchase_confirmation(ticket).deliver_now
- rescue => e
- Rails.logger.error "Failed to send confirmation: #{e.message}"
- end
- end
-
- # ... rest of existing code ...
- end
- end
-end
-```
-
-### Phase 4: Mailer Updates
-
-```ruby
-# app/mailers/ticket_mailer.rb - Update to include invoice
-class TicketMailer < ApplicationMailer
- def purchase_confirmation(ticket)
- @ticket = ticket
- @order = ticket.order
- @user = ticket.user
- @event = ticket.event
-
- # Attach ticket PDF
- ticket_pdf = ticket.to_pdf
- attachments["ticket_#{ticket.id}.pdf"] = ticket_pdf
-
- # Attach invoice PDF
- if @order.invoice.present?
- invoice_pdf = @order.invoice.generate_pdf
- attachments["invoice_#{@order.invoice.invoice_number}.pdf"] = invoice_pdf
- end
-
- mail(
- to: @user.email,
- subject: "Your tickets for #{@event.name}"
- )
- end
-end
-```
-
-### Phase 5: Routes
-
-```ruby
-# config/routes.rb
-Rails.application.routes.draw do
- # ... existing routes ...
-
- resources :invoices, only: [:show] do
- member do
- get :download_pdf
- end
- end
-
- resources :orders do
- member do
- get :invoice # Shortcut to order's invoice
- end
- end
-end
-```
-
-### Phase 6: Views
-
-```erb
-
-
-
-
-
-
Invoice
-
Invoice #<%= @invoice.invoice_number %>
-
-
-
Date: <%= @invoice.issued_at.strftime('%B %d, %Y') %>
-
- <%= @invoice.status.capitalize %>
-
-
-
-
-
-
-
-
Bill To:
-
<%= @invoice.user.email %>
-
-
-
Event:
-
<%= @invoice.event.name %>
-
<%= @invoice.event.start_time.strftime('%B %d, %Y at %I:%M %p') %>
-
-
-
-
-
-
-
-
- | Description |
- Qty |
- Unit Price |
- Total |
-
-
-
- <% @invoice.order.tickets.group_by(&:ticket_type).each do |ticket_type, tickets| %>
-
- | <%= ticket_type.name %> |
- <%= tickets.count %> |
- €<%= ticket_type.price_cents / 100.0 %> |
- €<%= (tickets.count * ticket_type.price_cents) / 100.0 %> |
-
- <% end %>
-
-
-
-
-
-
-
- Total: €<%= @invoice.total_amount_euros %>
-
-
-
-
-
- <%= link_to "Download PDF", download_pdf_invoice_path(@invoice),
- class: "bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700" %>
-
-
-
-```
-
-## Integration Guidelines
-
-### 1. Testing Strategy
-
-```ruby
-# test/services/invoice_pdf_generator_test.rb
-require 'test_helper'
-
-class InvoicePdfGeneratorTest < ActiveSupport::TestCase
- setup do
- @order = orders(:paid_order)
- @generator = InvoicePdfGenerator.new(@order.invoice)
- end
-
- test "generates PDF successfully" do
- pdf = @generator.generate
- assert pdf.present?
- assert pdf.is_a?(String)
- end
-
- test "includes order details in PDF" do
- # Test PDF content includes expected information
- end
-end
-```
-
-### 2. Performance Considerations
-
-```ruby
-# app/jobs/invoice_generation_job.rb
-class InvoiceGenerationJob < ApplicationJob
- queue_as :default
-
- def perform(order_id)
- order = Order.find(order_id)
- invoice = order.create_invoice!
-
- # Generate PDF in background
- pdf = invoice.generate_pdf
-
- # Store in cloud storage if needed
- # S3Service.store_invoice_pdf(invoice, pdf)
-
- # Send email notification
- InvoiceMailer.invoice_ready(invoice).deliver_now
- end
-end
-
-# Call from OrdersController#payment_success
-InvoiceGenerationJob.perform_later(@order.id)
-```
-
-### 3. Security Considerations
-
-- Ensure users can only access their own invoices
-- Validate invoice numbers are unique
-- Sanitize user input in PDF generation
-- Use HTTPS for invoice downloads
-- Consider adding invoice access tokens for sharing
-
-### 4. Deployment Checklist
-
-- [ ] Run database migrations
-- [ ] Update mailer templates
-- [ ] Test PDF generation in production environment
-- [ ] Verify email attachments work correctly
-- [ ] Set up monitoring for PDF generation failures
-- [ ] Add invoice generation to payment success flow
-
-## Conclusion
-
-The recommended approach combines the best of both worlds:
-- Keep the existing immediate payment flow for better user experience
-- Generate professional invoices for accounting and user records
-- Use existing Prawn infrastructure for consistent PDF styling
-- Optionally integrate with Stripe invoicing for advanced features
-
-This implementation provides a solid foundation that can be extended with additional features like tax calculations, discounts, or integration with accounting systems.
\ No newline at end of file
diff --git a/docs/order-system-implementation.md b/docs/order-system-implementation.md
deleted file mode 100644
index 51dfe15..0000000
--- a/docs/order-system-implementation.md
+++ /dev/null
@@ -1,414 +0,0 @@
-# Order System Implementation Guide
-
-## Overview
-
-This guide outlines how to implement an Order system in your Rails ticketing application, replacing the current individual ticket-based approach with a more robust order-based system.
-
-## Current System Analysis
-
-Your current system has:
-- Individual tickets directly associated with users
-- Payment attempts tracked per ticket
-- No grouping of related tickets
-- Complex checkout logic in controllers
-
-## Proposed Order System Architecture
-
-### Database Schema Changes
-
-**New `orders` table:**
-```sql
-CREATE TABLE orders (
- id SERIAL PRIMARY KEY,
- user_id INTEGER NOT NULL REFERENCES users(id),
- event_id INTEGER NOT NULL REFERENCES events(id),
- status VARCHAR(255) NOT NULL DEFAULT 'draft',
- total_amount_cents INTEGER NOT NULL DEFAULT 0,
- payment_attempts INTEGER NOT NULL DEFAULT 0,
- expires_at TIMESTAMP,
- last_payment_attempt_at TIMESTAMP,
- created_at TIMESTAMP NOT NULL,
- updated_at TIMESTAMP NOT NULL
-);
-
--- Indexes for performance
-CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-CREATE INDEX idx_orders_event_status ON orders(event_id, status);
-CREATE INDEX idx_orders_expires_at ON orders(expires_at);
-```
-
-**Updated `tickets` table:**
-```sql
--- Add order_id column
-ALTER TABLE tickets ADD COLUMN order_id INTEGER REFERENCES orders(id);
-
--- Update existing tickets (if any)
-UPDATE tickets SET order_id = (SELECT id FROM orders WHERE user_id = tickets.user_id LIMIT 1);
-
--- Make order_id NOT NULL after data migration
-ALTER TABLE tickets ALTER COLUMN order_id SET NOT NULL;
-
--- Remove user_id from tickets (optional, but recommended)
--- ALTER TABLE tickets DROP COLUMN user_id;
-```
-
-## 1. Create Order Model
-
-**File: `app/models/order.rb`**
-```ruby
-class Order < ApplicationRecord
- # === Constants ===
- DRAFT_EXPIRY_TIME = 30.minutes
- MAX_PAYMENT_ATTEMPTS = 3
-
- # === Associations ===
- belongs_to :user
- belongs_to :event
- has_many :tickets, dependent: :destroy
-
- # === Validations ===
- validates :user_id, presence: true
- validates :event_id, presence: true
- validates :status, presence: true, inclusion: {
- in: %w[draft pending_payment paid completed cancelled expired]
- }
- validates :total_amount_cents, presence: true,
- numericality: { greater_than_or_equal_to: 0 }
- validates :payment_attempts, presence: true,
- numericality: { greater_than_or_equal_to: 0 }
-
- # === Scopes ===
- scope :draft, -> { where(status: "draft") }
- scope :active, -> { where(status: %w[paid completed]) }
- scope :expired_drafts, -> { draft.where("expires_at < ?", Time.current) }
- scope :can_retry_payment, -> {
- draft.where("payment_attempts < ? AND expires_at > ?",
- MAX_PAYMENT_ATTEMPTS, Time.current)
- }
-
- before_validation :set_expiry, on: :create
-
- # === Instance Methods ===
-
- # Total amount in euros (formatted)
- def total_amount_euros
- total_amount_cents / 100.0
- end
-
- # Check if order can be retried for payment
- def can_retry_payment?
- draft? && payment_attempts < MAX_PAYMENT_ATTEMPTS && !expired?
- end
-
- # Check if order is expired
- def expired?
- expires_at.present? && expires_at < Time.current
- end
-
- # Mark order as expired if it's past expiry time
- def expire_if_overdue!
- return unless draft? && expired?
- update!(status: "expired")
- end
-
- # Increment payment attempt counter
- def increment_payment_attempt!
- update!(
- payment_attempts: payment_attempts + 1,
- last_payment_attempt_at: Time.current
- )
- end
-
- # Check if draft is about to expire (within 5 minutes)
- def expiring_soon?
- return false unless draft? && expires_at.present?
- expires_at <= 5.minutes.from_now
- end
-
- # Mark order as paid and activate all tickets
- def mark_as_paid!
- transaction do
- update!(status: "paid")
- tickets.update_all(status: "active")
- end
- end
-
- # Calculate total from tickets
- def calculate_total!
- update!(total_amount_cents: tickets.sum(:price_cents))
- end
-
- private
-
- def set_expiry
- return unless status == "draft"
- self.expires_at = DRAFT_EXPIRY_TIME.from_now if expires_at.blank?
- end
-
- def draft?
- status == "draft"
- end
-end
-```
-
-## 2. Update Ticket Model
-
-**File: `app/models/ticket.rb`**
-```ruby
-class Ticket < ApplicationRecord
- # === Constants ===
- DRAFT_EXPIRY_TIME = 30.minutes
- MAX_PAYMENT_ATTEMPTS = 3
-
- # === Associations ===
- belongs_to :order # Changed from belongs_to :user
- belongs_to :ticket_type
- has_one :user, through: :order # Access user through order
- has_one :event, through: :ticket_type
-
- # === Validations ===
- validates :qr_code, presence: true, uniqueness: true
- validates :order_id, presence: true # Changed from user_id
- validates :ticket_type_id, presence: true
- validates :price_cents, presence: true, numericality: { greater_than: 0 }
- validates :status, presence: true,
- inclusion: { in: %w[draft active used expired refunded] }
- validates :first_name, presence: true
- validates :last_name, presence: true
- # Removed payment_attempts validation (now handled by Order)
-
- # === Scopes ===
- scope :draft, -> { where(status: "draft") }
- scope :active, -> { where(status: "active") }
- # Removed payment-related scopes (now in Order)
-
- before_validation :set_price_from_ticket_type, on: :create
- before_validation :generate_qr_code, on: :create
- # Removed set_draft_expiry (now in Order)
-
- # === Instance Methods ===
-
- # Price in euros (formatted)
- def price_euros
- price_cents / 100.0
- end
-
- # Delegate payment methods to order
- def can_retry_payment?
- order.can_retry_payment?
- end
-
- def expired?
- order.expired?
- end
-
- def expiring_soon?
- order.expiring_soon?
- end
-
- # Mark ticket as expired if it's past expiry time
- def expire_if_overdue!
- return unless draft? && expired?
- update!(status: "expired")
- end
-
- # Generate PDF ticket
- def to_pdf
- TicketPdfGenerator.new(self).generate
- end
-
- private
-
- def set_price_from_ticket_type
- return unless ticket_type
- self.price_cents = ticket_type.price_cents
- end
-
- def generate_qr_code
- return if qr_code.present?
- loop do
- self.qr_code = SecureRandom.uuid
- break unless Ticket.exists?(qr_code: qr_code)
- end
- end
-
- def draft?
- status == "draft"
- end
-end
-```
-
-## 3. Update Controllers
-
-### TicketsController Changes
-
-**File: `app/controllers/tickets_controller.rb`**
-
-Key changes needed:
-1. Create orders instead of individual tickets
-2. Update session management to track `order_id` instead of `draft_ticket_ids`
-3. Modify checkout logic to work with orders
-4. Update payment success/cancel handling
-
-**Main changes in `create` action:**
-```ruby
-# OLD: Create individual tickets
-@event = Event.includes(:ticket_types).find(params[:id])
-@cart_data = session[:pending_cart] || {}
-# ... create individual tickets
-
-# NEW: Create order with tickets
-@event = Event.includes(:ticket_types).find(params[:id])
-@cart_data = session[:pending_cart] || {}
-
-ActiveRecord::Base.transaction do
- @order = current_user.orders.create!(event: @event, status: "draft")
-
- ticket_params[:tickets_attributes]&.each do |index, ticket_attrs|
- next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank?
-
- ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id])
- @order.tickets.create!(
- ticket_type: ticket_type,
- first_name: ticket_attrs[:first_name],
- last_name: ticket_attrs[:last_name],
- status: "draft"
- )
- end
-
- if @order.tickets.present?
- @order.calculate_total!
- session[:draft_order_id] = @order.id
- redirect_to order_checkout_path(@order)
- else
- @order.destroy
- # ... handle error
- end
-end
-```
-
-**Updated `checkout` action:**
-```ruby
-def checkout
- @order = current_user.orders.includes(tickets: :ticket_type)
- .find_by(id: params[:id], status: "draft")
-
- return redirect_to event_path(@order.event.slug, @order.event),
- alert: "Commande introuvable" unless @order
-
- # Handle expired orders
- if @order.expired?
- @order.expire_if_overdue!
- return redirect_to event_path(@order.event.slug, @order.event),
- alert: "Votre commande a expiré"
- end
-
- @tickets = @order.tickets
- @total_amount = @order.total_amount_cents
- @expiring_soon = @order.expiring_soon?
-
- # Create Stripe session if configured
- if Rails.application.config.stripe[:secret_key].present?
- begin
- @checkout_session = create_stripe_session
- @order.increment_payment_attempt!
- rescue => e
- # handle error
- end
- end
-end
-```
-
-## 4. Update Routes
-
-**File: `config/routes.rb`**
-```ruby
-# Add order routes
-resources :orders, only: [:show] do
- member do
- get :checkout
- post :retry_payment
- end
-end
-
-# Update existing ticket routes to work with orders
-# ... existing routes
-```
-
-## 5. Update Views
-
-### Checkout View Changes
-**File: `app/views/orders/checkout.html.erb`**
-```erb
-
-Commande pour <%= @order.event.name %>
-
-
-
Récapitulatif de votre commande
- <% @order.tickets.each do |ticket| %>
-
- <%= ticket.ticket_type.name %>
- <%= ticket.first_name %> <%= ticket.last_name %>
- <%= ticket.price_euros %>€
-
- <% end %>
-
-
- Total: <%= @order.total_amount_euros %>€
-
-
-
-
-<% if @checkout_session.present? %>
-
-<% end %>
-```
-
-## 6. Migration Strategy
-
-1. **Create migration for orders table**
-2. **Add order_id to tickets table**
-3. **Create data migration to associate existing tickets with orders**
-4. **Update existing data**
-5. **Remove old payment_attempts from tickets table**
-
-## 7. Testing Strategy
-
-1. **Unit Tests:**
- - Order model validations
- - Order status transitions
- - Payment attempt logic
-
-2. **Integration Tests:**
- - Complete order flow
- - Stripe integration
- - Order expiry handling
-
-3. **Edge Cases:**
- - Order with no tickets
- - Expired orders
- - Payment failures
- - Concurrent order creation
-
-## Benefits of This Implementation
-
-1. **Better Data Organization:** Related tickets grouped logically
-2. **Improved Performance:** Single query for order with tickets
-3. **Enhanced UX:** Clear order summaries and history
-4. **Better Analytics:** Order-level metrics and reporting
-5. **Scalability:** Easier to add features like order management
-6. **Payment Logic:** Centralized payment attempt tracking
-
-## Migration Checklist
-
-- [ ] Create Order model
-- [ ] Create orders migration
-- [ ] Update Ticket model associations
-- [ ] Update TicketsController
-- [ ] Update routes
-- [ ] Update views
-- [ ] Run migrations
-- [ ] Test complete flow
-- [ ] Update documentation
-
-This implementation provides a solid foundation for a scalable ticketing system with proper order management.
\ No newline at end of file
diff --git a/docs/pricing-model-recommendations.md b/docs/pricing-model-recommendations.md
new file mode 100644
index 0000000..d90bee3
--- /dev/null
+++ b/docs/pricing-model-recommendations.md
@@ -0,0 +1,410 @@
+# Pricing Model Recommendations
+
+## Current Problem Analysis
+
+### Current Model: €1 Fixed Fee Per Order
+- **Revenue**: €1.00 per order (regardless of ticket price)
+- **Stripe Costs**: 1.15% + €0.25 per transaction
+- **Result**: Losing money on higher-priced tickets
+
+### Financial Impact Examples
+| Ticket Price | Current Revenue | Stripe Fees | Net Profit/Loss |
+|--------------|----------------|-------------|-----------------|
+| €20 | €1.00 | €0.48 | **+€0.52** ✅ |
+| €50 | €1.00 | €0.83 | **+€0.17** ⚠️ |
+| €100 | €1.00 | €1.40 | **-€0.40** ❌ |
+| €200 | €1.00 | €2.55 | **-€1.55** ❌ |
+
+**Critical Issue**: We lose money on any ticket above €65 (€1.00 - (€65 × 1.15% + €0.25) = -€0.00)
+
+## Recommended Pricing Models
+
+### Model 1: Percentage-Based (Recommended)
+```
+Platform Fee = 3-5% of ticket price
+```
+
+**Advantages:**
+- Always profitable regardless of ticket price
+- Scales naturally with event value
+- Simple for promoters to understand
+- Industry standard approach
+
+**Financial Impact:**
+| Ticket Price | 3.5% Fee | Stripe Costs | Net Profit |
+|--------------|----------|--------------|------------|
+| €20 | €0.70 | €0.48 | €0.22 |
+| €50 | €1.75 | €0.83 | €0.92 |
+| €100 | €3.50 | €1.40 | €2.10 |
+| €200 | €7.00 | €2.55 | €4.45 |
+
+### Model 2: Hybrid Model (Best)
+```
+Platform Fee = €1.50 + 2% of ticket price
+```
+
+**Advantages:**
+- Higher base fee covers Stripe fixed costs
+- Percentage component scales with value
+- Better margins on low-priced tickets
+- Predictable minimum revenue per transaction
+
+**Financial Impact:**
+| Ticket Price | Hybrid Fee | Stripe Costs | Net Profit |
+|--------------|------------|--------------|------------|
+| €20 | €1.90 | €0.48 | €1.42 |
+| €50 | €2.50 | €0.83 | €1.67 |
+| €100 | €3.50 | €1.40 | €2.10 |
+| €200 | €5.50 | €2.55 | €2.95 |
+
+### Model 3: Tiered Flat Fees
+```
+€0-25: €1.50 fee
+€26-75: €2.50 fee
+€76-150: €3.50 fee
+€151+: €5.00 fee
+```
+
+**Advantages:**
+- Predictable fees for promoters
+- Simple pricing tiers
+- Better than current model
+
+**Disadvantages:**
+- Less scalable than percentage
+- Requires tier management
+
+## Industry Benchmarks
+
+| Platform | Fee Structure | Effective Rate (€50 ticket) |
+|-------------|----------------------------|------------------------------|
+| Eventbrite | 3.7% + €0.59 | €2.44 |
+| Universe | 2.5% + €0.49 | €1.74 |
+| Ticketfly | 4% + €0.99 | €2.99 |
+| **Recommended** | **3.5%** | **€1.75** |
+
+## Implementation Strategy
+
+### Phase 1: Immediate Implementation (Week 1-2)
+1. **Switch to 3.5% percentage model** for new events
+2. **Grandfathering**: Existing published events keep €1 pricing
+3. **Communication**: Notify promoters 1 week before change
+4. **Update pricing display** on event creation and checkout pages
+
+### Phase 2: Optimization (Month 2-3)
+1. **A/B Test different rates**: 3%, 3.5%, 4%
+2. **Monitor metrics**:
+ - Promoter signup rate
+ - Event creation volume
+ - Revenue per transaction
+ - Customer complaints
+3. **Adjust based on data**
+
+### Phase 3: Premium Tiers (Month 4-6)
+1. **Volume discounts** for high-performing promoters:
+ - Standard: 3.5%
+ - Bronze (€5K+ monthly): 3%
+ - Silver (€15K+ monthly): 2.5%
+ - Gold (€50K+ monthly): 2%
+2. **Annual contracts** with reduced rates
+3. **White-label options** for enterprise clients
+
+## Revenue Projections
+
+### Current State (€1 fixed)
+- Average ticket price: €35
+- Monthly orders: 1,000
+- Monthly revenue: €1,000
+- Monthly Stripe costs: €628
+- **Net monthly profit: €372**
+
+### With 3.5% Model
+- Monthly revenue: €1,225 (3.5% × €35 × 1,000)
+- Monthly Stripe costs: €628
+- **Net monthly profit: €597** (+60% increase)
+
+### With Growth (3.5% model + 20% more events)
+- Monthly orders: 1,200 (20% growth from competitive pricing)
+- Monthly revenue: €1,470
+- **Net monthly profit: €842** (+126% increase)
+
+## Technical Implementation
+
+### Database Changes
+```ruby
+# Add to events table
+add_column :events, :commission_rate, :decimal, precision: 5, scale: 4, default: 0.035
+
+# Add to users table (for premium tiers)
+add_column :users, :commission_tier, :string, default: 'standard'
+```
+
+### Fee Calculation Logic
+```ruby
+class Order < ApplicationRecord
+ def calculate_platform_fee
+ event = self.event
+ commission_rate = event.commission_rate || 0.035
+ base_amount = total_amount_cents / 100.0
+ (base_amount * commission_rate * 100).to_i # return in cents
+ end
+
+ def platform_fee_euros
+ calculate_platform_fee / 100.0
+ end
+end
+```
+
+### Promoter Dashboard Updates
+- Show fee breakdown on event creation
+- Display projected fees during ticket setup
+- Add revenue vs. fees analytics
+- Export fee reports for tax purposes
+
+## Communication Plan
+
+### Email to Existing Promoters
+**Subject**: "Important Pricing Update - New Fair Fee Structure"
+
+**Key Points**:
+- Current model loses money on higher-priced tickets
+- New model ensures platform sustainability
+- Better features and support with improved revenue
+- Existing events unchanged, new events only
+- Competitive with industry standards
+
+### Website Updates
+- Update pricing page with clear fee calculator
+- Add FAQ about fee structure
+- Transparency about what fees cover (development, support, payment processing)
+
+## Risk Mitigation
+
+### Potential Issues
+1. **Promoter backlash**: Higher fees on expensive tickets
+2. **Competitor advantage**: Other platforms with lower fees
+3. **Reduced event creation**: Promoters may create fewer events
+
+### Mitigation Strategies
+1. **Value communication**: Emphasize platform improvements and reliability
+2. **Competitive analysis**: Position as "fair and sustainable" vs. competitors
+3. **Volume incentives**: Quick path to reduced rates for active promoters
+4. **Feature rollout**: Launch new promoter features alongside pricing change
+
+## Success Metrics
+
+### Financial KPIs
+- Monthly recurring revenue growth
+- Average revenue per transaction
+- Profit margin improvement
+- Customer acquisition cost vs. lifetime value
+
+### Product KPIs
+- Promoter retention rate
+- New event creation volume
+- Average ticket prices
+- Customer satisfaction scores
+
+## Conclusion
+
+The current €1 fixed fee model is financially unsustainable and actually costs us money on higher-value transactions. Moving to a **3.5% commission model** will:
+
+1. **Ensure profitability** at all ticket price levels
+2. **Align with industry standards** for competitiveness
+3. **Scale revenue** naturally with platform growth
+4. **Fund platform improvements** and better promoter tools
+
+**Recommendation**: Implement 3.5% commission model immediately, with grandfathering for existing events and a clear communication plan to promoters.
+
+## Fee Collection Method: Industry Standard
+
+### Current Approach vs Industry Standard
+
+**Current Model (Not Recommended)**:
+```
+Customer pays: €50 + €1 fee = €51
+Promoter receives: €50
+Platform keeps: €1
+```
+
+**Industry Standard (Recommended)**:
+```
+Customer pays: €50 (clean price)
+Platform keeps: €1.75 (3.5% of €50)
+Promoter receives: €48.25
+```
+
+### How Major Platforms Handle Fees
+
+**Eventbrite:**
+- Promoter sets: €50 ticket
+- Customer pays: €50
+- Eventbrite keeps: €1.85 (3.7%)
+- Promoter receives: €48.15
+
+**Ticketmaster:**
+- Promoter sets: €50 ticket
+- Customer pays: €50
+- Ticketmaster keeps: €5-7.50 (10-15%)
+- Promoter receives: €42.50-45
+
+**Universe (by Ticketmaster):**
+- Promoter sets: €50 ticket
+- Customer pays: €50
+- Universe keeps: €1.74 (2.5% + €0.49)
+- Promoter receives: €48.26
+
+### Why Deducting from Payout is Better
+
+#### 1. Customer Experience
+- **Price transparency**: Customer sees exactly what they expect to pay
+- **No surprise fees**: Reduces cart abandonment
+- **Competitive pricing**: Easier to compare with other events
+
+#### 2. Promoter Benefits
+- **Marketing simplicity**: Can advertise clean prices
+- **Psychological pricing**: €50 sounds better than €51.75
+- **Competitive advantage**: Not adding extra fees to customer
+
+#### 3. Platform Benefits
+- **Higher conversion rates**: No fee-shock at checkout
+- **Better promoter adoption**: Easier to sell to event organizers
+- **Industry standard**: Follows established practices
+
+### Psychological Impact
+
+**Adding Fees to Customer (Current)**:
+- Customer thinks: "€50 ticket... oh wait, €51.75 total" 😤
+- Cart abandonment risk
+
+**Deducting from Payout (Recommended)**:
+- Customer thinks: "€50 ticket, €50 total" 😊
+- Smooth purchase experience
+
+### Promoter Dashboard Display
+```
+Ticket Price: €50.00
+Platform Fee (3.5%): -€1.75
+Your Earnings per Ticket: €48.25
+```
+
+### Communication to Promoters
+**Before:** "Platform charges €1 per order to customers"
+**After:** "Set your desired revenue per ticket, we handle the rest"
+
+**Example:**
+- Promoter wants €48.25 net per ticket
+- They should set ticket price at €50
+- Customer pays €50, promoter gets €48.25
+- Clean, simple, industry standard
+
+## Competitive Analysis
+
+### Main Competitor: €1 Flat Fee
+
+Our main competitor charges a simple €1 flat fee per order. Here's how our models compare:
+
+### Hybrid Model vs Competitor Analysis
+
+**Hybrid Model**: €1.50 + 2% of ticket price
+**Competitor**: €1.00 flat fee
+
+| Ticket Price | Competitor Fee | Hybrid Fee | Difference | Competitive Position |
+|--------------|----------------|------------|------------|---------------------|
+| €10 | €1.00 | €1.70 | +€0.70 | More expensive |
+| €25 | €1.00 | €2.00 | +€1.00 | More expensive |
+| €50 | €1.00 | €2.50 | +€1.50 | More expensive |
+| **€75** | **€1.00** | **€3.00** | **+€2.00** | **Break-even point** |
+| €100 | €1.00 | €3.50 | +€2.50 | Much more expensive |
+
+### Alternative Competitive Models
+
+#### Option 1: Low-End Competitive Model
+```
+Platform Fee = €0.50 + 1.5% of ticket price
+```
+
+| Ticket Price | Competitor Fee | Our Fee | Difference | Position |
+|--------------|----------------|---------|------------|----------|
+| €10 | €1.00 | €0.65 | **-€0.35** | ✅ **Cheaper** |
+| €25 | €1.00 | €0.88 | **-€0.12** | ✅ **Cheaper** |
+| €50 | €1.00 | €1.25 | +€0.25 | ⚠️ Slightly higher |
+| €100 | €1.00 | €2.00 | +€1.00 | More expensive |
+
+#### Option 2: Modified Hybrid Model
+```
+Platform Fee = €0.75 + 2.5% of ticket price
+```
+
+| Ticket Price | Competitor Fee | Our Fee | Difference | Position |
+|--------------|----------------|---------|------------|----------|
+| €10 | €1.00 | €1.00 | **Equal** | ✅ Competitive |
+| €25 | €1.00 | €1.38 | +€0.38 | ⚠️ Slightly higher |
+| €40 | €1.00 | €1.75 | +€0.75 | **Break-even** |
+| €75 | €1.00 | €2.63 | +€1.63 | Much more expensive |
+
+### Competitive Strategy Recommendations
+
+#### 1. Value Differentiation Approach
+Since we'll be more expensive on higher-priced tickets, focus on premium positioning:
+
+**Value Proposition:**
+- "We're not the cheapest, we're the most complete"
+- Advanced analytics dashboard
+- Real-time sales tracking
+- Professional promoter tools
+- Superior customer support
+- Platform reliability and sustainability
+
+#### 2. Market Segmentation Strategy
+
+**Target Market Positioning:**
+- **Competitor**: Best for small, simple events (€10-30 tickets)
+- **Us**: Best for professional events (€40+ tickets) with serious promoters
+
+#### 3. Hybrid Competitive Approach
+
+**Tiered Offering:**
+- **Basic Plan**: Match competitor at €1 flat fee (limited features)
+- **Professional Plan**: Hybrid model with premium features
+- **Enterprise Plan**: Custom pricing with full feature set
+
+#### 4. Volume-Based Competitive Response
+
+**Free Tier Strategy:**
+- First 3 events per month at competitor's €1 rate
+- Volume discounts for high-activity promoters
+- Loyalty rewards for long-term customers
+
+### Marketing Positioning
+
+#### "Choose Your Business Model" Campaign
+
+**For Simple Events (Under €40):**
+- "Need basic ticketing? Our competitor works fine"
+- "Pay €1 flat fee for simple events"
+
+**For Professional Events (€40+):**
+- "Serious about your business? You need serious tools"
+- "Fair percentage-based pricing"
+- "Advanced analytics, marketing tools, priority support"
+
+#### Brand Positioning Statement
+**"We're the Shopify of Events - Built for Growth"**
+
+This positions us as the premium option for serious promoters while acknowledging the competitor's advantage on small events.
+
+### Implementation Recommendation
+
+Given the competitive landscape, we recommend **Option 1** (€0.50 + 1.5%):
+
+**Advantages:**
+- Competitive on low-priced tickets
+- Still profitable at all price points
+- Better positioning against main competitor
+- Appeals to both small and large event organizers
+
+**Financial Impact:**
+- Lower fees on tickets under €33
+- Competitive fees on tickets €33-66
+- Premium pricing on high-value tickets justified by features
\ No newline at end of file
diff --git a/docs/test_fixes_summary.md b/docs/test_fixes_summary.md
deleted file mode 100644
index c7f18f2..0000000
--- a/docs/test_fixes_summary.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Test Fixes Summary
-
-This document summarizes the changes made to fix all failing tests in the Aperonight project.
-
-## Issues Fixed
-
-### 1. Onboarding Controller Test Failure
-**Problem**: Test expected "Bienvenue sur AperoNight !" but got "Bienvenue sur Aperonight !"
-
-**Root Cause**: Inconsistent application naming between controller and view templates
-
-**Fixes Applied**:
-- Updated `app/controllers/onboarding_controller.rb` to use `Rails.application.config.app_name` instead of hardcoded "AperoNight"
-- Updated `test/controllers/onboarding_controller_test.rb` to expect "Bienvenue sur Aperonight" instead of "Bienvenue sur AperoNight"
-
-### 2. Ticket Mailer Template Error
-**Problem**: `ActionView::Template::Error: undefined local variable or method 'user'`
-
-**Root Cause**: Template used `user.first_name` instead of `@user.first_name`
-
-**Fix Applied**:
-- Updated `app/views/ticket_mailer/purchase_confirmation.html.erb` line 8 from `user.first_name` to `@user.first_name`
-
-### 3. Event Reminder Template Inconsistency
-**Problem**: Event reminder template used hardcoded "ApéroNight" instead of configurable app name
-
-**Fix Applied**:
-- Updated `app/views/ticket_mailer/event_reminder.html.erb` to use `<%= ENV.fetch("APP_NAME", "Aperonight") %>` instead of hardcoded "ApéroNight"
-
-### 4. Email Content Assertion Issues
-**Problem**: Tests were checking `email.body.to_s` which was empty for multipart emails
-
-**Root Cause**: Multipart emails have content in html_part or text_part, not directly in body
-
-**Fixes Applied**:
-- Updated all tests in `test/mailers/ticket_mailer_test.rb` to properly extract content from multipart emails
-- Added proper content extraction logic that checks html_part, text_part, and body in the correct order
-- Updated assertion methods to use pattern matching with regex instead of strict string matching
-- Made event reminder tests more robust by checking if email object exists before making assertions
-
-### 5. User Name Matching Issues
-**Problem**: Tests expected email username but templates used user's first name
-
-**Fix Applied**:
-- Updated tests to match `@user.first_name` instead of `@user.email.split("@").first`
-
-## Files Modified
-
-1. `app/controllers/onboarding_controller.rb` - Fixed application name consistency
-2. `app/views/ticket_mailer/purchase_confirmation.html.erb` - Fixed template variable name
-3. `app/views/ticket_mailer/event_reminder.html.erb` - Fixed application name consistency
-4. `test/controllers/onboarding_controller_test.rb` - Updated expected text
-5. `test/mailers/ticket_mailer_test.rb` - Completely refactored email content assertions
-
-## Test Results
-
-Before fixes:
-- 240 tests, 6 failures, 2 errors
-
-After fixes:
-- 239 tests, 0 failures, 0 errors
-
-All tests now pass successfully!
-
-## Key Lessons
-
-1. **Consistent Naming**: Always use configuration variables for application names instead of hardcoded values
-2. **Template Variables**: Instance variables in templates must be prefixed with @
-3. **Email Testing**: Multipart emails require special handling to extract content
-4. **Robust Testing**: Use flexible pattern matching instead of strict string comparisons
-5. **Fixture Data**: Ensure test fixtures match the expected data structure and relationships
\ No newline at end of file
diff --git a/docs/test_solutions.md b/docs/test_solutions.md
deleted file mode 100644
index 751b56f..0000000
--- a/docs/test_solutions.md
+++ /dev/null
@@ -1,200 +0,0 @@
-# Test Solutions Document
-
-This document outlines the exact solutions for resolving the failing tests in the Aperonight project.
-
-## 1. Onboarding Controller Test Failure
-
-### Issue
-The test is failing because it expects "Bienvenue sur AperoNight !" but the actual text is "Bienvenue sur Aperonight !".
-
-### Root Cause
-The application name is defined inconsistently:
-- In the controller flash message: "AperoNight" (with capital N)
-- In the view template: Uses `Rails.application.config.app_name` which resolves to "Aperonight" (with lowercase n)
-
-### Solution
-Update the controller to use the same application name as the view:
-
-```ruby
-# In app/controllers/onboarding_controller.rb
-# Change line 12 from:
-flash[:notice] = "Bienvenue sur AperoNight ! Votre profil a été configuré avec succès."
-
-# To:
-flash[:notice] = "Bienvenue sur #{Rails.application.config.app_name} ! Votre profil a été configuré avec succès."
-```
-
-## 2. Ticket Mailer Template Error
-
-### Issue
-The test is failing with `ActionView::Template::Error: undefined local variable or method 'user'`.
-
-### Root Cause
-In the `purchase_confirmation.html.erb` template, line 8 uses `user.first_name` but the instance variable is `@user`.
-
-### Solution
-Update the template to use the correct instance variable name:
-
-```erb
-
-
-<% if user.first_name %>
-
-
-<% if @user.first_name %>
-```
-
-## 3. Event Reminder Email Tests
-
-### Issue
-The event reminder tests are failing because they expect specific text patterns that don't match the actual email content.
-
-### Root Cause
-The email template is not rendering the expected text patterns. Looking at the template, the issue is that the text patterns are not matching exactly.
-
-### Solution
-Update the tests to use more flexible matching:
-
-```ruby
-# In test/mailers/ticket_mailer_test.rb
-# Update the event reminder tests to check for the actual content
-
-test "event reminder email one week before" do
- email = TicketMailer.event_reminder(@user, @event, 7)
-
- if email
- assert_emails 1 do
- email.deliver_now
- end
-
- assert_equal [ "no-reply@aperonight.fr" ], email.from
- assert_equal [ @user.email ], email.to
- assert_match /Rappel.*dans une semaine/, email.subject
- assert_match /une semaine/, email.body.to_s
- assert_match /#{@event.name}/, email.body.to_s
- end
-end
-
-test "event reminder email one day before" do
- email = TicketMailer.event_reminder(@user, @event, 1)
-
- if email
- assert_emails 1 do
- email.deliver_now
- end
-
- assert_match /Rappel.*demain/, email.subject
- assert_match /demain/, email.body.to_s
- end
-end
-
-test "event reminder email day of event" do
- email = TicketMailer.event_reminder(@user, @event, 0)
-
- if email
- assert_emails 1 do
- email.deliver_now
- end
-
- assert_match /aujourd'hui/, email.subject
- assert_match /aujourd'hui/, email.body.to_s
- end
-end
-
-test "event reminder email custom days" do
- email = TicketMailer.event_reminder(@user, @event, 3)
-
- if email
- assert_emails 1 do
- email.deliver_now
- end
-
- assert_match /dans 3 jours/, email.subject
- assert_match /3 jours/, email.body.to_s
- end
-end
-```
-
-## 4. Email Notifications Integration Test
-
-### Issue
-The test `test_sends_purchase_confirmation_email_when_order_is_marked_as_paid` is failing because 0 emails were sent when 1 was expected.
-
-### Root Cause
-Based on the Order model, the `mark_as_paid!` method should send an email, but there might be an issue with the test setup or the email delivery in the test environment.
-
-### Solution
-Update the test to properly set up the conditions for email sending:
-
-```ruby
-# In test/integration/email_notifications_integration_test.rb
-test "sends_purchase_confirmation_email_when_order_is_marked_as_paid" do
- # Ensure the order and tickets are in the correct state
- @order.update(status: "draft")
- @ticket.update(status: "draft")
-
- # Mock PDF generation to avoid QR code issues
- @order.tickets.each do |ticket|
- ticket.stubs(:to_pdf).returns("fake_pdf_content")
- end
-
- # Clear any existing emails
- ActionMailer::Base.deliveries.clear
-
- assert_emails 1 do
- @order.mark_as_paid!
- end
-
- assert_equal "paid", @order.reload.status
- assert_equal "active", @ticket.reload.status
-end
-```
-
-## Implementation Steps
-
-1. **Fix the onboarding controller text inconsistency**:
- ```bash
- # Edit app/controllers/onboarding_controller.rb
- # Change the flash message to use Rails.application.config.app_name
- ```
-
-2. **Fix the mailer template error**:
- ```bash
- # Edit app/views/ticket_mailer/purchase_confirmation.html.erb
- # Change 'user.first_name' to '@user.first_name' on line 8
- ```
-
-3. **Update the mailer tests with more flexible matching**:
- ```bash
- # Edit test/mailers/ticket_mailer_test.rb
- # Update the event reminder tests as shown above
- ```
-
-4. **Fix the integration test setup**:
- ```bash
- # Edit test/integration/email_notifications_integration_test.rb
- # Update the test as shown above
- ```
-
-## Running Tests After Fixes
-
-After implementing these solutions, run the tests to verify the fixes:
-
-```bash
-# Run all tests
-./test.sh
-
-# Or run specific test files
-./test.sh test/controllers/onboarding_controller_test.rb
-./test.sh test/mailers/ticket_mailer_test.rb
-./test.sh test/integration/email_notifications_integration_test.rb
-```
-
-## Summary of Changes Required
-
-1. **Update onboarding controller** (1 line change)
-2. **Fix mailer template** (1 line change)
-3. **Update mailer tests** (4 tests updated)
-4. **Fix integration test setup** (1 test updated)
-
-These changes should resolve all the failing tests in the project.
\ No newline at end of file
diff --git a/docs/theme-rules.md b/docs/theme-rules.md
deleted file mode 100755
index 3177fa1..0000000
--- a/docs/theme-rules.md
+++ /dev/null
@@ -1,280 +0,0 @@
-# Theme Rules for Aperonight
-
-## Core Design System
-
-### Color Palette
-```css
-/* Primary - Purple gradient system */
---color-primary-50: #faf5ff;
---color-primary-100: #f3e8ff;
---color-primary-200: #e9d5ff;
---color-primary-300: #d8b4fe;
---color-primary-400: #c084fc;
---color-primary-500: #a855f7;
---color-primary-600: #9333ea;
---color-primary-700: #7e22ce;
---color-primary-800: #6b21a8;
---color-primary-900: #581c87;
-
-/* Accent - Pink gradient */
---color-accent-400: #f472b6;
---color-accent-500: #ec4899;
---color-accent-600: #db2777;
-
-/* Neutral - Slate system */
---color-neutral-50: #f8fafc;
---color-neutral-100: #f1f5f9;
---color-neutral-200: #e2e8f0;
---color-neutral-300: #cbd5e1;
---color-neutral-400: #94a3b8;
---color-neutral-500: #64748b;
---color-neutral-600: #475569;
---color-neutral-700: #334155;
---color-neutral-800: #1e293b;
---color-neutral-900: #0f172a;
-```
-
-### Typography
-```css
-/* Font families */
---font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
---font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-
-/* Font sizes */
---text-xs: 0.75rem; /* 12px */
---text-sm: 0.875rem; /* 14px */
---text-base: 1rem; /* 16px */
---text-lg: 1.125rem; /* 18px */
---text-xl: 1.25rem; /* 20px */
---text-2xl: 1.5rem; /* 24px */
---text-3xl: 1.875rem; /* 30px */
-```
-
-### Spacing
-```css
---space-1: 0.25rem; /* 4px */
---space-2: 0.5rem; /* 8px */
---space-3: 0.75rem; /* 12px */
---space-4: 1rem; /* 16px */
---space-5: 1.25rem; /* 20px */
---space-6: 1.5rem; /* 24px */
---space-8: 2rem; /* 32px */
---space-10: 2.5rem; /* 40px */
---space-12: 3rem; /* 48px */
-```
-
-## Component Rules
-
-### Buttons
-```css
-/* Primary button */
-.btn-primary {
- @apply bg-gradient-to-r from-purple-600 to-pink-600 text-white font-medium py-2 px-4 rounded-lg shadow-sm hover:shadow-md transition-all duration-200;
-}
-
-/* Secondary button */
-.btn-secondary {
- @apply bg-white text-purple-600 border border-purple-200 font-medium py-2 px-4 rounded-lg hover:bg-purple-50 transition-colors duration-200;
-}
-
-/* Destructive button */
-.btn-destructive {
- @apply bg-red-600 text-white font-medium py-2 px-4 rounded-lg shadow-sm hover:bg-red-700 transition-colors duration-200;
-}
-```
-
-### Cards
-```css
-.card {
- @apply bg-white rounded-lg shadow-sm border border-slate-200 p-6 hover:shadow-md transition-shadow duration-200;
-}
-
-.card-header {
- @apply pb-4 border-b border-slate-200 mb-4;
-}
-
-.card-body {
- @apply space-y-4;
-}
-```
-
-### Forms
-```css
-.form-input {
- @apply block w-full rounded-md border-slate-300 shadow-sm focus:border-purple-500 focus:ring-purple-500 sm:text-sm;
-}
-
-.form-label {
- @apply block text-sm font-medium text-slate-700 mb-1;
-}
-
-.form-error {
- @apply text-sm text-red-600 mt-1;
-}
-```
-
-### Navigation
-```css
-.nav-link {
- @apply text-slate-600 hover:text-purple-600 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200;
-}
-
-.nav-link-active {
- @apply text-purple-600 bg-purple-50;
-}
-```
-
-## Layout Rules
-
-### Grid System
-```css
-.container {
- @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
-}
-
-.grid-responsive {
- @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6;
-}
-
-.grid-cards {
- @apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6;
-}
-```
-
-### Responsive Breakpoints
-```css
-/* Mobile-first approach */
-@media (min-width: 640px) { /* sm */ }
-@media (min-width: 768px) { /* md */ }
-@media (min-width: 1024px) { /* lg */ }
-@media (min-width: 1280px) { /* xl */ }
-```
-
-## Component States
-
-### Hover States
-```css
-.hover-lift {
- @apply transition-transform duration-200 hover:-translate-y-1;
-}
-
-.hover-glow {
- @apply transition-all duration-200 hover:shadow-lg hover:shadow-purple-500/25;
-}
-```
-
-### Focus States
-```css
-.focus-ring {
- @apply focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2;
-}
-```
-
-### Disabled States
-```css
-.disabled {
- @apply opacity-50 cursor-not-allowed;
-}
-```
-
-## Animation Rules
-
-### Transitions
-```css
-.transition-fast {
- @apply transition-all duration-150 ease-in-out;
-}
-
-.transition-normal {
- @apply transition-all duration-200 ease-in-out;
-}
-
-.transition-slow {
- @apply transition-all duration-300 ease-in-out;
-}
-```
-
-### Micro-animations
-```css
-/* Subtle pulse for notifications */
-.animate-pulse-subtle {
- @apply animate-pulse;
- animation-duration: 3s;
-}
-
-/* Gentle fade in */
-.fade-in {
- @apply animate-in fade-in-0 duration-500;
-}
-```
-
-## Dark Mode Rules
-
-### Dark mode color tokens
-```css
-@media (prefers-color-scheme: dark) {
- :root {
- --color-background: #0f172a;
- --color-surface: #1e293b;
- --color-border: #334155;
- --color-text-primary: #f1f5f9;
- --color-text-secondary: #cbd5e1;
- --color-text-muted: #64748b;
- }
-}
-```
-
-### Dark mode components
-```css
-.dark .card {
- @apply bg-slate-800 border-slate-700;
-}
-
-.dark .form-input {
- @apply bg-slate-700 border-slate-600 text-white placeholder-slate-400;
-}
-```
-
-## Accessibility Rules
-
-### Focus Indicators
-```css
-.focus-visible {
- @apply focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2;
-}
-```
-
-### Color Contrast
-```css
-/* Ensure WCAG 2.1 AA compliance */
-.text-primary {
- @apply text-slate-900 dark:text-slate-100;
-}
-
-.text-secondary {
- @apply text-slate-600 dark:text-slate-400;
-}
-```
-
-## Naming Conventions
-
-### CSS Classes
-- Use kebab-case: `btn-primary`, `form-input`
-- Prefix utilities with `u-`: `u-flex`, `u-text-sm`
-- State modifiers: `is-active`, `has-error`
-
-### JavaScript/React
-- Components: PascalCase (`UserProfile.jsx`)
-- Utilities: camelCase (`formatDate.js`)
-- Constants: UPPER_SNAKE_CASE (`API_ENDPOINTS`)
-
-### File Structure
-```
-app/javascript/
-├── components/
-│ ├── ui/ # Reusable UI components
-│ ├── forms/ # Form-specific components
-│ └── layouts/ # Layout components
-├── lib/ # Utilities and helpers
-└── controllers/ # Stimulus controllers
-```
\ No newline at end of file
diff --git a/package.json b/package.json
index 3a4e4c4..83b031a 100755
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"lucide": "^0.542.0",
"qrcode": "^1.5.4",
"react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react-dom": "^18.3.1",
+ "slug": "^11.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.4",
diff --git a/tailwind.config.js b/tailwind.config.js
index 72b3a52..d07238c 100755
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -9,6 +9,21 @@ module.exports = {
"./app/views/**/*.{erb,haml,html,slim}",
"./app/components/**/*.{erb,haml,html,slim,js}",
],
- theme: {},
+ theme: {
+ extend: {
+ colors: {
+ "brand-primary": "#667eea",
+ "brand-secondary": "#764ba2",
+ "brand-accent": "#facc15",
+ },
+ fontFamily: {
+ sans: ["Inter", "system-ui", "sans-serif"],
+ display: ["DM Sans", "system-ui", "sans-serif"],
+ },
+ backgroundImage: {
+ "gradient-primary": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
+ },
+ },
+ },
plugins: [require("tailwindcss-animate")],
};
diff --git a/yarn.lock b/yarn.lock
index 39556a8..df6c938 100755
--- a/yarn.lock
+++ b/yarn.lock
@@ -2046,6 +2046,11 @@ slash@^5.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz"
integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==
+slug@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/slug/-/slug-11.0.0.tgz#026b0b78dcfb5744ed36310598a9e5769534570e"
+ integrity sha512-71pb27F9TII2dIweGr2ybS220IUZo1A9GKZ+e2q8rpUr24mejBb6fTaSStM0SE1ITUUOshilqZze8Yt1BKj+ew==
+
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz"