Event & Session Management
Complete guide to event management, session scheduling, and reservation systems.
Event System Overview
Lancaster Archery Academy manages diverse event types including classes, tournaments, subscriptions, and private parties through a flexible event system.
Event Types
Regular Classes
- Single or multi-session programs
- Fixed schedule with defined dates
- Seat-based capacity management
- One-time registration and payment
Subscription Events
- Recurring enrollment with monthly billing
- Timeslot-based scheduling (preferred day/time)
- Automatic session and reservation generation
- Enrollment fees (optional)
- Cancellation anytime with period-end or immediate effect
Tournaments
- Division-based registration (age, gender, equipment)
- Team support for mixed events (parent+child)
- IANSEO export for tournament management software
- Waitlist management
- Public registration lists
Private Parties
- Custom scheduling
- Invite-only or open registration
- Merchandise and add-ons
- Flexible pricing
Event Configuration
Nova Resource Fields
Basic Information:
- Name, Short Description
- Event Type, Category, Level
- Environment (Indoor/Outdoor)
- Location Assignment
Capacity & Scheduling:
- Max Seats per session
- Day Span (number of sessions)
- Session Length (duration)
- Price per session
Registration:
- Registration Deadline
- Early Bird Deadline & Pricing
- Registration Policy (open, invite-only, approval required)
- Age Range (min/max)
Advanced Features:
- Enrollment Fee (for subscriptions)
- Volume Discount tool
- Milestones (requirements/achievements)
- Event Options (equipment rental, extras)
- Flexible Content blocks
- Gallery & Downloads (media library)
Session Management
Manual Session Creation
Via Nova:
- Navigate to Event detail page
- Click "Sessions" tab
- Click "Create Session"
- Set start/end dates and times
- Assign location and instructor
- Set seat quantity
- Mark as "User Defined" if custom
Automated Session Generation
Generate Sessions Nova Action:
// Parameters:
- Start Date
- End Date
- Time (start time)
- Duration (session length)
- Recurrence Pattern (daily, weekly, custom days)
- Seat Quantity
- Location
- Exclude Dates (holidays, blackouts)
Example: Generate weekly Monday sessions from Jan 1 - Mar 31, 6:00 PM - 7:30 PM
- Creates ~12 session records automatically
- All sessions linked to parent event
- Configurable seat capacity
- Skip weekends/holidays option
Recurring Sessions
Parent-Child Relationships:
sessions.parent_id → sessions.id
// Parent session defines pattern
// Child sessions inherit configuration
// Modify all children via parent
Timeslots (for subscriptions):
timeslots (
event_id, location_id,
day_of_week, start_time, end_time,
seat_quantity
)
// Subscription selects timeslot preference
// Sessions matching timeslot auto-generate
// Reservations created for matching sessions
Reservation System
Reservation Lifecycle
1. Cart Addition:
// User selects sessions
// Creates pending Order (status=0, expiration=15min)
// Creates pending Reservations linked to Order
2. Cart Expiration:
// 15-minute countdown timer
// If expired: release seats, delete reservations
// Command: app/Console/Commands/ExpireOrders.php
3. Checkout:
// Validate:
// - Session availability (no overbooking)
// - Waiver requirements
// - Payment method exists
// - Age requirements met (if applicable)
4. Payment:
// Process via Authorize.Net CIM
// Create Transaction record
// Update Order status=1
// Update Reservation status=1
5. Confirmation:
// Send confirmation emails
// Fire OrderPaid event
// Generate attendance codes
// Link waivers if signed
Reservation Types
Standard Reservation:
- Account linked to Session
- Seats quantity (usually 1)
- Base cost (captured at time of registration)
- Status (pending/confirmed/canceled)
Tournament Reservation:
- Division selection (age/gender/equipment)
- Team assignment (if team event)
- Stays on line preference
- Practice pass option
Subscription-Generated Reservation:
- Created automatically for subscription
- Linked to subscription_id
- No immediate payment (billed monthly)
- Cannot be individually canceled (cancel subscription instead)
Waitlist System
Waitlist Entry
When Session is Full:
// User can join waitlist
Waitlist::create([
'session_id' => $session->id,
'first_name' => $account->first_name,
'last_name' => $account->last_name,
'email' => $account->email,
'phone' => $account->phone,
'seats_quantity' => 1,
'status' => 0, // pending
]);
Waitlist Notification Flow
1. Space Becomes Available (cancellation or capacity increase):
// Find oldest waitlist entry
$waitlist = Waitlist::where('session_id', $session->id)
->where('status', 0)
->orderBy('created_at')
->first();
2. Send Notification:
Mail::to($waitlist->email)->send(new WaitListNotification($waitlist));
$waitlist->update([
'contacted_at' => now(),
'status' => 1, // contacted
]);
3. User Response:
// User clicks link in email
// Creates reservation
// Marks waitlist entry: reservation_created = true
Nova Management:
- View all waitlists per session
- Manual contact tracking
- Status updates (pending/contacted/replied/registered)
- Bulk notification actions
Session Availability Checking
Real-Time Availability
Model Method (app/Models/Session.php):
public function availableSeats(): int
{
$reserved = $this->reservations()
->whereIn('status', [0, 1]) // pending or confirmed
->sum('seats');
return max(0, $this->seat_quantity - $reserved);
}
public function isFull(): bool
{
return $this->availableSeats() === 0;
}
public function hasSpace(int $seats = 1): bool
{
return $this->availableSeats() >= $seats;
}
Conflict Detection
Reservation Conflict Check:
public static function hasConflict($account, $session): bool
{
// Check if account already registered for overlapping session
return Reservation::where('account_id', $account->id)
->whereHas('session', function($query) use ($session) {
$query->where('start_date', '<', $session->end_date)
->where('end_date', '>', $session->start_date);
})
->whereIn('status', [0, 1])
->exists();
}
Override Option:
// Sessions can have ignore_date_conflicts = true
// Allows overlapping registrations (e.g., open range time)
Volume Pricing
Configuration
Suma Nova Component (nova-components/VolumeDiscount):
// In Event Nova resource
VolumeDiscount::make('Volume Pricing')
->rules('nullable', 'json')
->help('Configure tiered pricing based on session quantity')
JSON Structure:
[
{
"quantity": 5,
"discount_type": "percent",
"amount": 10
},
{
"quantity": 10,
"discount_type": "fixed",
"amount": 50
}
]
Application
Checkout Calculation:
public static function calculateOrderTotal($reservations)
{
$sessionCount = $reservations->count();
$basePrice = $reservations->first()->session->event->price;
$subtotal = $sessionCount * $basePrice;
// Apply volume discount
$discount = self::calculateVolumeDiscount($sessionCount, $basePrice, $volumePricing);
$total = $subtotal - $discount;
return compact('subtotal', 'discount', 'total');
}
Coupons
Coupon Structure
coupons (
code, discount_type, amount,
min_purchase, usage_limit, customer_usage_limit,
start_date, end_date,
event_types (JSON), event_type_exclusions (JSON)
)
Validation
Checkout Application:
public function validateCoupon($code, $order): ?Coupon
{
$coupon = Coupon::where('code', $code)->first();
// Check existence
if (!$coupon) return null;
// Check date validity
if ($coupon->start_date > now() || $coupon->end_date < now()) {
return null;
}
// Check usage limits
if ($coupon->usage_limit && $coupon->redemptions >= $coupon->usage_limit) {
return null;
}
// Check customer usage
$customerUses = CouponRedemption::where('coupon_id', $coupon->id)
->where('user_id', auth()->id())
->count();
if ($coupon->customer_usage_limit && $customerUses >= $coupon->customer_usage_limit) {
return null;
}
// Check minimum purchase
if ($coupon->min_purchase && $order->subtotal < $coupon->min_purchase) {
return null;
}
// Check applicable event types
$eventTypes = $order->reservations->pluck('session.event.event_type_id')->unique();
if ($coupon->event_types && !$eventTypes->intersect($coupon->event_types)->count()) {
return null;
}
if ($coupon->event_type_exclusions && $eventTypes->intersect($coupon->event_type_exclusions)->count()) {
return null;
}
return $coupon;
}
Discount Calculation
public function calculateDiscount($subtotal): float
{
if ($this->discount_type === 'percent') {
return $subtotal * ($this->amount / 100);
}
// Fixed amount
return min($this->amount, $subtotal);
}
Attendance Tracking
Attendance Codes
Generation (on reservation confirmation):
$reservation->update([
'attendance_code' => strtoupper(Str::random(6))
]);
Check-In Process:
- Staff scans attendance code (QR code or manual entry)
- Mark reservation as attended
- Update milestone tracking if applicable
Nova Action: AttendanceSelection on Reservation resource
Reservation Reassignment
Use Case
Move reservation from one account to another (e.g., family member substitute).
Nova Action: ReassignReservation
public function handle(ActionFields $fields, Collection $models)
{
foreach ($models as $reservation) {
$newAccount = Account::find($fields->account_id);
// Validate new account meets requirements
if (!$this->validateAccount($newAccount, $reservation->session)) {
return Action::danger('Account does not meet requirements');
}
// Reassign
$reservation->update([
'account_id' => $newAccount->id,
'reassigned' => true,
]);
// Send notification
Mail::to($newAccount->email)->send(
new ReservationReassigned($reservation)
);
}
return Action::message('Reservation(s) reassigned successfully');
}
Event Insights Dashboard
Suma Nova Component (nova-components/EventInsights):
Dashboard Cards:
- Sessions for today/tomorrow
- Hourly schedule by location
- Calendar view of all sessions
- Revenue projections
- Registration statistics
Metrics:
- Total registrations
- Available seats
- Revenue (actual vs projected)
- Popular events
- Low-enrollment alerts