# TIMC 2027 Registration System - Design Document

## Table of Contents
1. [System Overview](#system-overview)
2. [Architecture](#architecture)
3. [File Structure & Descriptions](#file-structure--descriptions)
4. [Data Model](#data-model)
5. [Data Flow](#data-flow)
6. [Payment Flow](#payment-flow)
7. [Editing Options](#editing-options)
8. [File Interactions](#file-interactions)
9. [Key Features](#key-features)

---

## System Overview

The TIMC 2027 Registration System is a PHP 8+ web application that manages conference registrations with support for:
- **Dynamic registration forms** with conditional field display
- **Token-based editing** (no login system required)
- **Partial registration save/resume** functionality
- **Stripe payment integration** for credit card payments
- **Purchase Order support** for group payments
- **Automated email confirmations**
- **Admin panel** for managing class/level availability
- **Change logging** for audit trails

**Tech Stack:**
- PHP 8+ with PDO (MySQL)
- Stripe API (v10.0+)
- PHPMailer (v6.9+)
- reCAPTCHA integration (Google)
- Vanilla JavaScript (form enhancement)

---

## Architecture

### Layered Architecture

```
┌─────────────────────────────────────────────┐
│        Public/Web Tier                      │
│    (*.php files in /public)                 │
├─────────────────────────────────────────────┤
│        Service Layer                        │
│    (/src/services/*.php)                    │
│    - RegistrationService                    │
│    - PartialRegistrationService             │
│    - EmailService                           │
├─────────────────────────────────────────────┤
│        Data Access Layer                    │
│    (/src/models/*.php)                      │
│    - Group, Participant, Director, etc.    │
├─────────────────────────────────────────────┤
│        Infrastructure Layer                 │
│    (/src/config/*.php)                      │
│    - Database (Singleton PDO)               │
│    - Env (Environment variable loader)      │
├─────────────────────────────────────────────┤
│        Utility Layer                        │
│    (/src/utils/helpers.php)                 │
│    - Cost calculations                      │
│    - Sanitization                           │
│    - Token generation                       │
└─────────────────────────────────────────────┘
```

### Design Patterns Used

- **Singleton**: `Database` class manages single PDO connection
- **Service Pattern**: Business logic separated into service classes
- **Data Mapper**: Model classes handle CRUD operations
- **Facade**: Services provide simplified interfaces to complex operations
- **Static Factory**: Model classes use static methods for queries

---

## File Structure & Descriptions

### Configuration Files

#### [`src/config/Env.php`](src/config/Env.php)
**Purpose:** Environment variable loader
- Loads `.env` file with configuration values
- Supports both `$_ENV` and `$_SERVER` superglobals
- Handles quoted values and fallbacks
- Used for: Database credentials, API keys, email settings, Stripe keys, reCAPTCHA keys

**Methods:**
- `Env::load(string $rootPath)` - Load .env file
- `Env::get(string $key, $default = null)` - Retrieve env value

---

#### [`src/config/Database.php`](src/config/Database.php)
**Purpose:** Database connection management (Singleton pattern)
- Creates and maintains single PDO connection for entire application
- Configured for MySQL with error mode set to ERRMODE_EXCEPTION
- Uses prepared statements and associative array fetch mode
- Disables PDO prepared statement emulation for security

**Methods:**
- `Database::getConnection(): PDO` - Get (or create) PDO instance

**Environment Variables Required:**
- `DB_HOST` - MySQL host
- `DB_NAME` - Database name
- `DB_USER` - Database user
- `DB_PASS` - Database password
- `DB_CHARSET` - Character set (default: utf8mb4)

---

### Model Classes (Data Access Layer)

All models follow a consistent pattern: static CRUD methods operating on specific database tables.

#### [`src/models/Group.php`](src/models/Group.php)
**Purpose:** Represents a registration group (organization, school, or individual registration)
**Database Table:** `Groups`

**Key Fields:**
- `id` - Primary key
- `group_name` - Name of group/organization
- `group_type` - Type: 'Group', 'Individual', 'School'
- `school_name` - School name if applicable
- `total_cost` - Calculated total registration cost
- `paid` - Boolean, whether payment is complete
- `registration_date` - Timestamp of registration
- `competition` - Boolean, participation in competition
- `unique_token` - Token for editing registration (20 hex chars)
- `hotel_name` - Hotel name if booking hotel
- `hotel_duration` - Number of nights
- `canta_tickets` - Count of Canta event tickets
- `garibaldi_tickets` - Count of Garibaldi event tickets
- `espectacular_low_tickets` - Count of lower-priced spectacle tickets
- `espectacular_high_tickets` - Count of higher-priced spectacle tickets
- `showcase_tickets` - Count of showcase performance tickets
- `workshop_type` - Type of workshop selected

**Methods:**
- `create(array $data): int` - Create new group, return ID
- `update(int $id, array $data): bool` - Update existing group
- `findByToken(string $token): ?array` - Find group by unique token
- `findById(int $id): ?array` - Find group by ID

**Relationships:**
- One-to-Many with Participants
- One-to-One with Director (primary)
- One-to-Many with AssistantDirector
- One-to-Many with Songs
- One-to-Many with Payments

---

#### [`src/models/Participant.php`](src/models/Participant.php)
**Purpose:** Represents individual performers in a registration
**Database Table:** `participants`

**Key Fields:**
- `id` - Primary key
- `group_id` - Foreign key to Groups
- `first_name`, `last_name` - Participant name
- `age` - Age
- `gender` - Gender
- `grade` - School grade
- `class` - Instrument/voice type (Violin, Guitar, Vihuela, Trumpet, Harp, Voice, Dance, Other)
- `level` - Proficiency level (1, 2, 3, Master)
- `scholarship` - Boolean, scholarship recipient
- `cost` - Calculated cost for participant ($115 or $165 Master)
- `release` - Boolean, media release form signed
- `no_charge` - Boolean, should not be charged

**Methods:**
- `create(int $groupId, array $data): int` - Create new participant
- `update(int $id, array $data): bool` - Update participant
- `findByGroupId(int $groupId): array` - Get all participants in group
- `deleteByIds(array $ids): bool` - Delete multiple participants

**Cost Calculation:**
- Standard: $115.00
- Master + Voice/Harp: $115.00
- Master + Other: $165.00

---

#### [`src/models/Director.php`](src/models/Director.php)
**Purpose:** Represents primary group director/contact person
**Database Table:** `directors`

**Key Fields:**
- `id` - Primary key
- `group_id` - Foreign key to Groups
- `first_name`, `last_name` - Director name
- `street_address`, `city`, `state`, `zip` - Address
- `phone` - Phone number
- `email` - Email address (used for contact and edit links)
- `is_primary` - Boolean, marks as primary director

**Methods:**
- `create(int $groupId, array $data): int` - Create director
- `update(int $id, array $data): bool` - Update director
- `findPrimaryByGroupId(int $groupId): ?array` - Get primary director

**Purpose in System:**
- Primary contact for registration
- Receives confirmation and edit emails
- Can request administrative corrections or student removals

---

#### [`src/models/AssistantDirector.php`](src/models/AssistantDirector.php)
**Purpose:** Represents optional assistant director
**Database Table:** `assistant_directors`

**Key Fields:**
- Same address/contact fields as Director
- No `is_primary` flag (one per group)

**Methods:**
- `create(int $groupId, array $data): int` - Create assistant
- `update(int $id, array $data): bool` - Update assistant
- `findByGroupId(int $groupId): ?array` - Get assistant for group
- `deleteByGroupId(int $groupId): bool` - Delete assistant

---

#### [`src/models/Song.php`](src/models/Song.php)
**Purpose:** Represents songs in showcase performance
**Database Table:** `songs`

**Key Fields:**
- `id` - Primary key
- `group_id` - Foreign key to Groups
- `song_order` - Order in showcase (1, 2, 3, etc.)
- `title` - Song name
- `length_seconds` - Duration in seconds
- `notes` - Optional notes about song

**Methods:**
- `create(int $groupId, array $data): int` - Create song entry
- `update(int $id, array $data): bool` - Update song
- `findByGroupId(int $groupId): array` - Get all songs for group (ordered)
- `deleteByIds(array $ids): bool` - Delete multiple songs

**Purpose:**
- Track showcase performance setlist
- Used to validate total performance duration
- Only populated if `showcase_tickets > 0`

---

#### [`src/models/Payment.php`](src/models/Payment.php)
**Purpose:** Track payment history for registrations
**Database Table:** `payments`

**Key Fields:**
- `id` - Primary key
- `group_id` - Foreign key to Groups
- `amount` - Payment amount (decimal)
- `payment_date` - Date of payment
- `payment_method` - 'Credit Card' or 'Purchase Order'
- `transaction_id` - Stripe transaction ID (for credit cards)
- `notes` - Optional notes

**Methods:**
- `create(int $groupId, array $data): int` - Record payment
- `findByGroupId(int $groupId): array` - Get all payments for group

**Purpose:**
- Audit trail of payments
- Support for partial payments
- Track which payment method was used

---

#### [`src/models/ChangeLog.php`](src/models/ChangeLog.php)
**Purpose:** Audit trail for all data changes
**Database Table:** `change_log`

**Key Fields:**
- `table_name` - Which table was changed
- `record_id` - Which record ID
- `action` - 'CREATE', 'UPDATE', 'DELETE', 'REQUEST', 'REMOVED'
- `column_name` - Which column changed (null for multi-field actions)
- `old_value` - Previous value
- `new_value` - New value
- `changed_by` - User who made change (null for self-service)
- `ip_address` - Client IP address
- `notes` - Additional context
- `created_at` - Timestamp

**Methods:**
- `record(array $data): bool` - Record a change

**Used For:**
- Tracking registration creation
- Logging edit requests
- Recording student removals
- Admin correction requests

---

### Service Layer

#### [`src/services/RegistrationService.php`](src/services/RegistrationService.php)
**Purpose:** Core business logic for registration lifecycle
**Dependencies:** All Model classes, PartialRegistrationService, EmailService, Helpers

**Key Methods:**

**Preparation & Payment:**
- `prepareCartForPayment(array $formData, ?int $existingGroupId, float $existingTotal): array`
  - Validates and calculates costs
  - Returns cart with total_cost and charge_amount
  - For edits: charge_amount = new_total - old_total

- `processPurchaseOrder(array $formData, ?string $submittedEditAction): array`
  - Create or update registration with Purchase Order payment
  - Returns group_id, token, total_cost

- `processCreditCardPayment(array $formData, float $chargeAmount, string $transactionId, ?string $submittedEditAction): array`
  - Create or update registration with Stripe transaction
  - Handles initial registration or edit flow
  - Returns group_id, token, total_cost

**Data Loading:**
- `loadRegistrationByToken(string $token): ?array`
  - Complete registration data lookup
  - Returns merged group with all related data
  - Includes: director, assistant_director, participants, songs, payments

**Edit Requests:**
- `processSubmittedEditRequest(int $groupId, string $editAction, ?string $requestText, ?string $note): void`
  - Handles 'request_admin_correction' action
  - Handles 'remove_student' action
  - Records to change log
  - Sends email to admin

**Private Methods (Internal Logic):**
- `createRegistration()` - Create new group + all related records
- `updateRegistration()` - Update existing group + apply changes
- `filterSubmittedEditFormData()` - Filter form data for specific edit action
- `applyGroupChanges()` - Update group with new values, log changes
- `applyDirectorChanges()` - Update director info
- `applyAssistantDirectorChanges()` - Update assistant director
- `applyParticipantChanges()` - Add, update, remove participants
- `applySongChanges()` - Update showcase songs
- `buildGroupPayload()` - Extract group fields from form
- `buildDirectorPayload()` - Extract director fields from form
- `buildAssistantDirectorPayload()` - Extract assistant director fields from form
- `upsertDirector()` - Create or update director
- `upsertAssistantDirector()` - Create or update assistant director
- `upsertParticipants()` - Create/update participant records
- `upsertSongs()` - Create/update song records
- `recordPayment()` - Log payment
- `logChange()` - Write to change log
- `getClientIp()` - Get client IP for logging
- `loadExistingSongsAsFormData()` - Convert DB songs to form format
- `sendConfirmationEmails()` - Delegate to EmailService

---

#### [`src/services/PartialRegistrationService.php`](src/services/PartialRegistrationService.php)
**Purpose:** Manage incomplete registrations for save/resume functionality
**Database Table:** `partial_registrations`

**Key Methods:**
- `savePartial(array $formData): array`
  - Saves form state as JSON
  - Generates or reuses resume token
  - Sets 5-year expiration
  - Returns: token, email, group_name

- `resumePartial(string $token): ?array`
  - Retrieves saved form data by token
  - Decodes JSON back to array
  - Returns form data or null if not found

- `deletePartialByToken(string $token): bool`
  - Removes partial registration
  - Called after successful full registration
  - Called when resuming and converting to full registration

**Database Schema:**
```
partial_registrations
├── id (primary key)
├── token (unique, 12 hex chars minimum)
├── data_json (JSON serialized form data)
├── email (director's email)
├── group_name (registration group name)
├── created_at (timestamp)
├── expires_at (timestamp, 5 years out)
```

---

#### [`src/services/EmailService.php`](src/services/EmailService.php)
**Purpose:** Send all transactional emails using PHPMailer
**Dependencies:** PHPMailer, Env

**Configuration (from .env):**
- `MAIL_HOST` - SMTP server
- `MAIL_PORT` - SMTP port (default 587)
- `MAIL_USERNAME` - SMTP username
- `MAIL_PASSWORD` - SMTP password
- `MAIL_FROM` - From email address
- `MAIL_FROM_NAME` - From display name

**Public Methods:**

- `sendRegistrationConfirmationEmail(int $groupId, array $formData, string $token): bool`
  - Sent after successful registration (new or edit)
  - Includes edit token and edit link
  - Body includes group name, workshop, registration type, total cost

- `sendResumeLinkEmail(string $email, string $token, ?string $groupName, bool $isEdit): bool`
  - Sent when user requests resume/edit link via resume.php
  - Links to resume.php or edit-registration.php
  - Differentiated messages for resume vs. edit

- `sendAdminCorrectionRequestEmail(string $recipient, string $groupName, string $directorEmail, string $requestText): bool`
  - Sent to admin when director submits correction request
  - Admin email: info@tucsonmariachi.org
  - Includes group name and request details

- `sendStudentRemovalRequestEmail(string $recipient, string $groupName, string $directorEmail, array $participantIds, ?string $note): bool`
  - Sent to admin when director requests student removal
  - Lists participant IDs to remove
  - Includes optional note from director

**Private Methods:**
- `sendEmail()` - Core email dispatch using PHPMailer
- `buildConfirmationBody()` - Format confirmation email
- `buildResumeBody()` - Format resume/edit link email
- `getBaseUrl()` - Get site URL for links

---

### Utility Layer

#### [`src/utils/helpers.php`](src/utils/helpers.php)
**Purpose:** Utility functions for cost calculations, sanitization, validation

**Sanitization:**
- `sanitizeFieldValue($value)` - Trim string or recursively sanitize arrays
- `sanitizePost(array $post): array` - Sanitize entire $_POST array
- `generateUniqueToken(int $length = 20): string` - Generate random hex token

**Cost Calculations:**
- `getParticipantCost(array $participant): float`
  - Returns $115 for standard participants
  - Returns $115 for Master Voice/Harp
  - Returns $165 for Master other instruments

- `calculateTicketCost(array $formData): float`
  - Canta: $10 each
  - Garibaldi: $10 each
  - Espectacular Low: $45 each
  - Espectacular High: $25 each
  - Showcase: $20 each

- `calculateParticipantCostTotal(array $performers): float`
  - Sum cost for all participants in array

- `calculateShowcaseSeconds(array $songs): int`
  - Total duration of all showcase songs

- `calculateTotalCost(array $formData): float`
  - Combined participant + ticket costs

**Validation:**
- `validateRecaptcha(string $token, string $secret): bool`
  - Verify Google reCAPTCHA token
  - POST to Google API
  - Return success status

---

### Bootstrap & Configuration

#### [`public/_bootstrap.php`](public/_bootstrap.php)
**Purpose:** Initialize application for all page requests
**Loaded by:** Every public page

**Responsibilities:**
- Load environment variables
- Start PHP session
- Initialize Database connection
- Load class availability from database
- Prepare JSON structures for JavaScript

**Sets Variables:**
- `$classOptions` - Available instrument classes
- `$levelOptions` - Available proficiency levels
- `$availability` - 2D array of [class][level] => available boolean
- `$classOptionsJson` - JSON for JavaScript
- `$levelOptionsJson` - JSON for JavaScript
- `$availabilityJson` - JSON for JavaScript
- `$formData` - Restored from session
- `$editing` - Boolean, are we editing?
- `$editToken` - Token for editing
- `$editingGroupId` - Group ID being edited
- `$submittedEditAction` - Edit action type (add_student, edit_student_class_level, etc.)

**Helper Functions (for templates):**
- `old($keys, $default = '')` - Get form field value from session
- `arrayValue($key, $default = [])` - Get array field from form data
- Initializes performer array (minimum 3 empty rows)
- Initializes showcase songs array (minimum 3 empty rows)

---

### Public Pages (Web Interface)

#### [`public/index.php`](public/index.php)
**Purpose:** Landing page and main registration form
**Entry Point:** `/` or `/?page=register`

**Views:**
- Landing page (default) with options to:
  - Register (new) - `?page=register&new=1`
  - Continue registration - link to resume.php
- Registration form (when `?page=register`)

**Form Structure:**

**Group Information (Section 1)**
- Group Type: Radio (Group / Individual)
- Registration Type: Conditional (appears based on group type)
- Group Name: Text input
- School Name: Text input (conditional for Group type)
- Workshop Type: Select dropdown

**Participants (Section 2)**
- Dynamic table with at least 3 rows
- Each row: First Name, Last Name, Age, Gender, Grade, Class, Level
- Add/Remove buttons per row
- Validation: Group = 2+ participants, Individual = exactly 1

**Director Information (Section 3)**
- First Name, Last Name
- Street Address, City, State, Zip Code
- Email (primary contact)
- Cell Phone, Daytime Phone

**Assistant Director (Section 4)**
- Optional: First Name, Last Name
- Address fields
- Phone, Email

**Event Registration (Section 5)**
- Competition Exclusion: Yes/No (checkbox)
- Event Tickets:
  - Canta Tickets (qty)
  - Garibaldi Tickets (qty)
  - Espectacular Tickets - Low ($45) and High ($25) (qty each)
  - Showcase Tickets (qty) - conditional
- Hotel: Yes/No (checkbox)
  - Hotel Name (text, conditional)
  - Hotel Nights (number, conditional)

**Showcase Performance (Section 6, conditional if showcase_tickets > 0)**
- 3+ song entries (Title, Duration in seconds)
- Add/Remove buttons per song
- Total duration display

**Participant Cost Summary (Section 7)**
- Per-participant costs listed
- Ticket costs listed
- Total cost calculated and displayed

**Payment Method (Section 8)**
- Radio: Credit Card or Purchase Order
- Conditional form fields appear based on selection

**Credit Card Payment (if selected)**
- Not processed on this page
- Proceeds to invoice.php

**Purchase Order (if selected)**
- Purchase Order Number: Text field
- Special instructions: Textarea
- Proceeds to confirmation-po.php

**Form Controls:**
- Save Partial (button) - Saves to partial_registrations
- Back to Home (button)
- Submit Registration (button) - Validates and POSTs to process.php

**Client-Side Validation:**
- JavaScript enforces field visibility
- Minimum participants validation
- reCAPTCHA verification

---

#### [`public/process.php`](public/process.php)
**Purpose:** Handle form submissions, route to payment or completion
**Request Method:** POST only

**Flow:**
1. Sanitize all POST data
2. Validate reCAPTCHA token
3. Handle special actions:
   - `action=save_partial` → Save to partial_registrations, redirect to index
   - Regular submission → Continue below

4. Validate participant count:
   - Individual: must have exactly 1
   - Group: must have 2+
   - Individual + Purchase Order: not allowed

5. Determine if creating or editing:
   - `editing_group_id` > 0 → Edit flow
   - Otherwise → Create flow

6. For edits: load existing group and validate edit token

7. For different payment methods:
   - **Credit Card:** Call `RegistrationService::prepareCartForPayment()`
     - Calculate charge_amount (new_total - existing_total)
     - Store form data in session
     - Redirect to invoice.php
   - **Purchase Order:** Call `RegistrationService::processPurchaseOrder()`
     - Create/update registration
     - Send confirmation email
     - Redirect to confirmation-po.php

8. Handle submitted edit actions:
   - `submitted_edit_action=add_student` - Adding new students
   - `submitted_edit_action=edit_student_class_level` - Changing student class/level
   - `submitted_edit_action=edit_group_details` - General registration edits

---

#### [`public/invoice.php`](public/invoice.php)
**Purpose:** Display payment summary before Stripe charge
**Accessed from:** process.php (for Credit Card payments)

**Session Requirements:**
- Form data must be in session from process.php
- Cart data with total_cost and charge_amount

**Display:**
- Group name and details
- List of participants with costs
- List of event tickets with costs
- Total cost breakdown
- Charge amount (for edits: new total - previous total)
- Disclaimer/confirmation text

**Controls:**
- "Back" button → returns to form (preserves session)
- "Pay with Stripe" button → Submits to charge.php

---

#### [`public/charge.php`](public/charge.php)
**Purpose:** Process Stripe credit card payment
**Request Method:** POST from invoice.php

**Stripe Integration Flow:**
1. Get Stripe API key from environment
2. Initialize Stripe client
3. Create payment intent with charge_amount
4. Submit to Stripe (server-side or client-side via token)
5. Validate transaction success
6. If successful:
   - Call `RegistrationService::processCreditCardPayment()`
   - Pass transaction ID from Stripe
   - Create/update registration
   - Send confirmation email
   - Redirect to confirmation-po.php or show success

7. If failed:
   - Store error message in session
   - Redirect back to invoice.php

---

#### [`public/save-partial.php`](public/save-partial.php)
**Purpose:** AJAX endpoint for saving partial registrations
**Request Method:** POST

**Payload:**
- Form data as JSON or form-encoded
- Can be called from JavaScript

**Response:**
- JSON with token, email, group_name
- Or error message

**Alternative:** process.php handles partial save with `action=save_partial`

---

#### [`public/resume.php`](public/resume.php)
**Purpose:** Retrieve and resume partial registration
**URL Parameters:**
- `token` - Resume token from URL or manual entry

**Flow:**
1. Display form for manual token entry (if needed)
2. If token provided:
   - Call `PartialRegistrationService::resumePartial(token)`
   - Retrieve form data
   - Merge into session `form_data`
   - Redirect to index.php?page=register
3. If not found:
   - Display error message
   - Show token entry form

---

#### [`public/edit-registration.php`](public/edit-registration.php)
**Purpose:** Resume/edit existing completed registration
**Similar to:** resume.php but for completed registrations

**Flow:**
1. Display form for token entry
2. If token provided:
   - Call `RegistrationService::loadRegistrationByToken(token)`
   - Merge registration data into session
   - Redirect to index.php?page=register with `editing=true`
3. Form shows as editing mode with current data

**Edit Actions:**
When form submitted with active edit:
- `add_student` - Adds new participants to existing group
- `edit_student_class_level` - Changes class/level for existing participants
- `edit_group_details` - Updates group and event ticket information

---

#### [`public/confirmation-po.php`](public/confirmation-po.php)
**Purpose:** Show confirmation after registration completion
**Accessed from:**
- process.php (Purchase Order)
- charge.php (Credit Card success)

**Display:**
- Confirmation message
- Group details summary
- Participant list
- Payment method and status
- Edit token and edit link
- Email receipt sent confirmation

---

#### [`public/registration-closed.php`](public/registration-closed.php)
**Purpose:** Display when registration period is closed
**Accessed by:** Conditional redirect from index.php

**Display:**
- Registration closed message
- Date period closed
- Contact information

---

#### [`public/admin.php`](public/admin.php)
**Purpose:** Admin dashboard for managing availability
**Access:** Typically requires password or IP authentication (not shown in code)

**Functions:**
- Display current class/level availability matrix
- Toggle availability status for each combination
- Save changes to `class_availability` table

**Database Table:**
```
class_availability
├── id
├── class (Violin, Guitar, etc.)
├── level (1, 2, 3, Master)
├── is_available (1/0)
```

---

## Data Model

### Entity Relationship Diagram

```
Groups (1)
├─ has (1) → Directors (Primary)
├─ has (0..1) → AssistantDirectors
├─ has (N) → Participants
├─ has (N) → Songs
├─ has (N) → Payments
└─ has (N) → ChangeLog entries

Partial Registrations (isolated)
└─ Stores JSON form data for save/resume
```

### Database Tables Summary

| Table | Purpose | Key |
|-------|---------|-----|
| `Groups` | Registration group/entry | `unique_token` for editing |
| `participants` | Individual performers | `group_id` FK |
| `directors` | Primary contact (address, phone, email) | `group_id` FK, `is_primary` |
| `assistant_directors` | Optional secondary contact | `group_id` FK |
| `songs` | Showcase performance songs | `group_id` FK |
| `payments` | Payment history | `group_id` FK |
| `partial_registrations` | Incomplete registrations | `token` unique |
| `class_availability` | Admin-managed class/level availability | (`class`, `level`) composite key |
| `change_log` | Audit trail of all changes | `table_name`, `record_id` |

---

## Data Flow

### New Registration Flow

```
1. User Visits index.php?page=register
   ↓
2. Form loaded with empty fields (or from session)
   ├─ Class/level availability loaded from DB
   └─ JavaScript manages conditional field display
   ↓
3. User fills form and clicks "Submit Registration"
   ↓
4. POST to process.php
   ├─ Sanitize all input
   ├─ Validate reCAPTCHA
   ├─ Validate participant counts
   └─ Select payment method path
   ↓
5a. CREDIT CARD PATH:
    ├─ RegistrationService::prepareCartForPayment()
    ├─ Calculate total_cost and charge_amount
    ├─ Store in session
    └─ Redirect to invoice.php
        ↓
    ├─ User reviews summary
    └─ Click "Pay with Stripe" → charge.php
        ↓
    ├─ Stripe payment processing
    ├─ If success:
    │   ├─ RegistrationService::processCreditCardPayment()
    │   ├─ Create Group record with unique_token
    │   ├─ Create Director record
    │   ├─ Create AssistantDirector (if provided)
    │   ├─ Create Participant records
    │   ├─ Create Song records (if showcase)
    │   ├─ Create Payment record with Stripe transaction ID
    │   ├─ Log to ChangeLog
    │   ├─ Delete PartialRegistration (if existed)
    │   ├─ EmailService::sendRegistrationConfirmationEmail()
    │   └─ Redirect to confirmation-po.php
    └─ If failed:
        └─ Return to invoice.php with error

5b. PURCHASE ORDER PATH:
    ├─ RegistrationService::processPurchaseOrder()
    ├─ Create all records (same as credit card)
    ├─ Create Payment record with 'Purchase Order' method
    ├─ Log to ChangeLog
    ├─ EmailService::sendRegistrationConfirmationEmail()
    └─ Redirect to confirmation-po.php
   ↓
6. Confirmation page displays:
   ├─ Success message
   ├─ Group details
   ├─ Edit token sent to email
   └─ Edit link: edit-registration.php?token=...
```

### Save Partial Registration Flow

```
1. User partially fills form on index.php?page=register
   ↓
2. Clicks "Save Partial Registration" button
   ↓
3. process.php (action=save_partial)
   ├─ PartialRegistrationService::savePartial()
   ├─ Generate or reuse resume_token (12 hex chars)
   ├─ JSON encode form data
   ├─ Store in partial_registrations table
   ├─ Set 5-year expiration
   └─ Return token to user
   ↓
4. Display token: "Your resume token: abc123def456"
   ├─ User can save token
   └─ Or receive via email
```

### Resume Partial Registration Flow

```
1. User visits resume.php
   ↓
2. Display token entry form
   ├─ Or: ?token=abc123def456 in URL
   ↓
3. Submit token
   ↓
4. PartialRegistrationService::resumePartial(token)
   ├─ Query partial_registrations table
   ├─ JSON decode stored data_json
   └─ Return array
   ↓
5. Merge into session['form_data']
   ↓
6. Redirect to index.php?page=register
   ↓
7. Form displays with previously saved values
   ↓
8. User continues filling and submits
   ↓
9. After successful registration:
   ├─ PartialRegistrationService::deletePartialByToken()
   └─ Clean up partial record
```

### Edit Registration Flow

```
1. User receives email with edit link:
   edit-registration.php?token=[unique_token]
   ↓
2. Click link → edit-registration.php
   ↓
3. Display token entry form (or auto-fill if in URL)
   ↓
4. Submit token
   ↓
5. RegistrationService::loadRegistrationByToken(token)
   ├─ Find Group by unique_token
   ├─ Load Director, AssistantDirector, Participants, Songs, Payments
   └─ Merge all into single array
   ↓
6. Store in session with flags:
   ├─ edit_token = token
   ├─ editing_group_id = group.id
   └─ editing = true
   ↓
7. Redirect to index.php?page=register
   ↓
8. Form displays with current registration data
   ├─ Fields show existing values
   └─ Submit button changes label (e.g., "Update Registration")
   ↓
9. User makes changes and submits
   ↓
10. process.php detects editing_group_id > 0
    ├─ Determine submitted_edit_action (if provided)
    ├─ RegistrationService::updateRegistration()
    ├─ Apply field changes with logging:
    │   ├─ applyGroupChanges() - Log changed fields
    │   ├─ applyDirectorChanges() - Update director
    │   ├─ applyParticipantChanges() - Add/update/remove participants
    │   └─ applySongChanges() - Update showcase songs
    ├─ If Credit Card selected:
    │   ├─ Calculate charge_amount = new_total - old_total
    │   └─ Process new charge if > 0
    ├─ Send confirmation email with updated details
    └─ Redirect to confirmation-po.php
    ↓
11. Confirmation shows update confirmation
```

### Admin Correction Request Flow

```
1. User on edit registration page
   ↓
2. Clicks "Request Administrative Correction"
   ↓
3. Modal/form appears asking for details
   ↓
4. User enters request text, submits
   ↓
5. process.php (submitted_edit_action=request_admin_correction)
   ↓
6. RegistrationService::processSubmittedEditRequest()
   ├─ Validate request_text not empty
   ├─ ChangeLog::record() entry with action='REQUEST'
   ├─ EmailService::sendAdminCorrectionRequestEmail()
   │   └─ To: info@tucsonmariachi.org
   │   └─ With: group name, director email, request text
   └─ Redirect back to form with success message
```

### Student Removal Request Flow

```
1. User on edit registration page
   ↓
2. Clicks "Remove Student" or selects students to remove
   ↓
3. Modal appears with participant list
   ↓
4. User selects student(s) and optional note, submits
   ↓
5. process.php (submitted_edit_action=remove_student)
   ↓
6. RegistrationService::processSubmittedEditRequest()
   ├─ Validate at least 1 student selected
   ├─ ChangeLog::record() entry with:
   │   ├─ action='REMOVED'
   │   ├─ old_value=JSON of participant IDs
   │   └─ notes with log message
   ├─ EmailService::sendStudentRemovalRequestEmail()
   │   └─ To: info@tucsonmariachi.org
   │   └─ With: group name, participant IDs, optional note
   └─ Redirect back with success message
```

---

## Payment Flow

### Payment Method Options

The system supports two payment methods with different workflows:

### Credit Card (Stripe)

```
Flow Timeline:
index.php → process.php → invoice.php → charge.php → confirmation-po.php

Key Points:
- Handled entirely within system
- Immediate payment processing
- Transaction ID stored in payment record
- Group marked as paid immediately
```

**Process:**

1. **Form Submission** (index.php → process.php)
   - User selects "Credit Card" payment method
   - Form posts to process.php

2. **Cart Preparation** (process.php)
   - `RegistrationService::prepareCartForPayment()`
   - Calculate `charge_amount`:
     - New registration: charge_amount = total_cost
     - Edit: charge_amount = new_total - old_total
   - Store cart in session with costs

3. **Invoice Review** (invoice.php)
   - Display payment summary
   - Show breakdown of participants and tickets
   - Display charge_amount to be charged
   - User can go back to edit or proceed

4. **Stripe Processing** (charge.php)
   - POST from invoice.php with Stripe token or payment method
   - Stripe library creates payment intent
   - Process charge for `charge_amount`
   - Return success/failure

5. **Success Handler** (charge.php → RegistrationService)
   - Extract Stripe transaction ID
   - `processCreditCardPayment(formData, chargeAmount, transactionId)`
   - Create/update all database records
   - Mark group as `paid = 1`
   - Create Payment record with Stripe transaction ID
   - Send confirmation email
   - Redirect to confirmation-po.php

6. **Failure Handler** (charge.php)
   - Store error message in session
   - Redirect to invoice.php for retry
   - Display error to user

### Purchase Order

```
Flow Timeline:
index.php → process.php → confirmation-po.php

Key Points:
- No payment processing in system
- Manual follow-up required
- Group marked as NOT paid (paid = 0)
- Used primarily for group registrations
- Individual registrations must use Credit Card
```

**Process:**

1. **Form Submission** (index.php → process.php)
   - User selects "Purchase Order" payment method
   - Enters PO number and optional instructions
   - Form posts to process.php

2. **Validation** (process.php)
   - Ensure registration_type is not "Individual"
   - If Individual + PO: redirect with error message

3. **Registration Creation** (process.php → RegistrationService)
   - `processPurchaseOrder(formData)`
   - Create/update all database records
   - Mark group as `paid = 0`
   - Create Payment record:
     - `payment_method = 'Purchase Order'`
     - `amount = 0` (not yet paid)
     - `transaction_id = null`
     - `notes = PO number + instructions`
   - Send confirmation email (same template)
   - Return group_id, token, total_cost

4. **Confirmation** (confirmation-po.php)
   - Show PO confirmation
   - Display group details
   - Instruct to send PO to: accounting@tucsonmariachi.org
   - Display total amount due
   - Edit token for future modifications

5. **Admin Follow-up** (Manual Process)
   - Admin receives confirmation email
   - Checks incoming PO against form
   - Once PO received and processed:
     - Admin updates payment record
     - Mark group as paid in system (or leave for manual reconciliation)

### Edit with Additional Payment

When a registered group edits and increases total cost:

```
Old total: $500
New total: $650
Charge amount: $150

- Only charge difference
- Create new Payment record for $150
- Update Group with new total_cost
- Log all changes
- Send updated confirmation email
```

---

## Editing Options

The system provides multiple edit workflows, all accessed via unique token:

### 1. Full Registration Edit
**Token-Based Access:** edit-registration.php?token=[unique_token]
**Who:** Original director (via email link)
**What Can Change:**
- Group details (name, school, workshop)
- Director/Assistant director contact info
- Participant details (name, age, gender, class, level)
- Event tickets
- Hotel information
- Showcase songs (if applicable)
- Anything except registration type

**Payment:**
- If cost increased: charge for difference
- If cost decreased: no refund (stored as credit)
- Can always switch payment method (PO ↔ Credit Card)

**What's Locked:**
- Registration type (Individual vs Group)
- Cannot change individual → group or vice versa

### 2. Add Student
**Form Action:** submitted_edit_action=add_student
**Workflow:**
- Director accesses edit form
- Participant form shows current students
- Can add new rows with new participant info
- Submit creates new participant records
- Recalculates total cost
- Charges for new participants only

**Added Participant Costs:**
- $115 per standard participant
- $165 per Master (non-voice/harp)
- Contributes to new charge_amount

### 3. Edit Student Class/Level
**Form Action:** submitted_edit_action=edit_student_class_level
**Workflow:**
- Director accesses edit form
- Participant table shows current students
- Can change class and level for existing students
- Submit updates participant records
- Recalculates costs based on new class/level
- Charges/refunds based on cost difference

**Cost Changes:**
- Standard → Master: +$50 charge
- Master (non-voice/harp) → Standard: -$50 refund (as credit)

### 4. Edit Group Details
**Form Action:** submitted_edit_action=edit_group_details
**Workflow:**
- Director can change:
  - Event ticket quantities
  - Hotel information
  - Showcase songs
  - Competition participation
- Creates new charge if tickets increased
- Sends confirmation email

### 5. Request Admin Correction
**Button/Action:** Request Administrative Correction
**Workflow:**
- Not a data edit but a support request
- Director enters description of what needs correcting
- ChangeLog records with action='REQUEST'
- Email sent to info@tucsonmariachi.org with details
- Admin handles manually (e.g., fixes typos, corrects participant info)
- No automatic data change

**Logged As:**
```
ChangeLog entry:
- table_name: 'Groups'
- action: 'REQUEST'
- notes: 'Administrative correction requested: [request text]'
```

### 6. Remove Student Request
**Button/Action:** Remove Student
**Workflow:**
- Director selects student(s) to remove
- Optional note explaining removal (e.g., "student no longer participating")
- Submit sends request to admin
- ChangeLog records with action='REMOVED'
- Email sent to info@tucsonmariachi.org with student IDs and note
- Admin manually removes students and adjusts invoice

**Logged As:**
```
ChangeLog entry:
- table_name: 'participants'
- action: 'REMOVED'
- old_value: JSON array of participant IDs
- notes: 'Removal request submitted for participant IDs: [ids]'
```

---

## File Interactions

### Request → Response Flow

```
HTTP Request
    ↓
Route to public/*.php page
    ↓
Load _bootstrap.php
    ├─ Load environment vars (Env.php)
    ├─ Connect to database (Database.php)
    ├─ Load class availability
    └─ Initialize session variables
    ↓
Execute page logic
    ├─ Read from Models (if GET/view)
    ├─ Call Services (if POST/action)
    │   ├─ Service calls Models for CRUD
    │   ├─ Service calls other Services
    │   ├─ Service uses Helpers for calculations
    │   └─ Service logs changes to ChangeLog
    ├─ Call EmailService (notifications)
    └─ Render response
        ├─ Load data from session
        ├─ Use Helpers for display (htmlspecialchars, etc.)
        └─ Return HTML/JSON
    ↓
HTTP Response
```

### Service Dependency Graph

```
RegistrationService
├─ depends on → All Model classes
├─ depends on → PartialRegistrationService
├─ depends on → EmailService
└─ depends on → Helpers

PartialRegistrationService
├─ depends on → Database (indirectly)
└─ depends on → Helpers

EmailService
├─ depends on → Env (for SMTP config)
└─ depends on → PHPMailer (external)

All Models
└─ depend on → Database
```

### Database Access Pattern

```
Public Page
    ↓
Calls Service method
    ↓
Service calls Model static methods
    ↓
Model.create(data)
├─ Get PDO from Database.getConnection()
├─ Prepare INSERT statement
├─ Execute with parameterized query
└─ Return lastInsertId()
    ↓
Model.update(id, data)
├─ Get PDO from Database.getConnection()
├─ Prepare UPDATE statement
├─ Execute with parameterized query
└─ Return success boolean
    ↓
Model.find*(where)
├─ Get PDO from Database.getConnection()
├─ Prepare SELECT statement
├─ Execute with parameterized query
└─ Return PDO::FETCH_ASSOC result(s)
```

### Cost Calculation Flow

```
Form Submission
    ↓
process.php calls:
Helpers::sanitizePost()
    ↓
Helpers::calculateTotalCost(formData)
├─ calculateParticipantCostTotal(performers)
│   ├─ foreach performer
│   └─ getParticipantCost(performer)
│       ├─ if Master + (Voice|Harp): $115
│       ├─ if Master + Other: $165
│       └─ else: $115
├─ calculateTicketCost(formData)
│   ├─ Canta: qty * $10
│   ├─ Garibaldi: qty * $10
│   ├─ Espectacular Low: qty * $45
│   ├─ Espectacular High: qty * $25
│   └─ Showcase: qty * $20
└─ return participant_total + ticket_total
    ↓
RegistrationService::prepareCartForPayment()
├─ Gets totalCost from above
├─ For new: charge_amount = total_cost
├─ For edit: charge_amount = total_cost - existing_total
└─ return cart with both costs
```

### Email Sending Flow

```
RegistrationService::createRegistration() or updateRegistration()
    ↓
Calls: sendConfirmationEmails(groupId, formData, token)
    ↓
EmailService::sendRegistrationConfirmationEmail()
├─ buildConfirmationBody(formData, token)
├─ Extract email from formData
└─ sendEmail(email, subject, body)
    ├─ Create PHPMailer instance
    ├─ Load SMTP config from Env
    ├─ Set from, to, subject, body
    ├─ Call mail->send()
    └─ return boolean or catch exception
        ↓
    User receives email with:
    ├─ Group details
    ├─ Edit token
    └─ Edit link: edit-registration.php?token=...
```

---

## Key Features

### 1. Token-Based Registration Editing (No Login Required)

**Implementation:**
- `unique_token` generated as 40-character hex string (20 random bytes)
- Stored in Groups table
- Sent to director via email after registration
- Used to retrieve full registration for editing
- No user accounts, no authentication system

**Security Considerations:**
- Token length makes brute force impractical
- Tokens generated server-side (cryptographically secure)
- Recommend HTTPS for token transmission
- Consider token rotation or expiration for very old registrations

### 2. Partial Registration Save/Resume

**Implementation:**
- Separate `partial_registrations` table
- Stores entire form data as JSON
- 5-year expiration (tokens never auto-delete)
- Resume token (12 hex chars) different from edit token
- Can resume multiple times without consuming token
- Deleted after successful registration

**Use Cases:**
- User interrupted mid-registration
- Large group with many participants (build over time)
- Network interruption
- User wants to share URL with group for collaborative filling

### 3. Dynamic Cost Calculation

**Real-Time Updates:**
- JavaScript on form recalculates as user types
- Participant count → checks minimum (1 for individual, 2+ for group)
- Per-participant cost varies by level and class
- Ticket quantities and prices hardcoded
- Total displayed prominently

**Server-Side Recalculation:**
- Always recalculate on submission (never trust client math)
- Helpers::calculateTotalCost() validates all costs
- Prevents tampering with cost data

### 4. Change Logging (Audit Trail)

**Tracked Changes:**
- All registration creates: action='CREATE'
- All registration updates: action='UPDATE' with field details
- Admin correction requests: action='REQUEST'
- Student removal requests: action='REMOVED'
- Includes: table name, record ID, old/new values, IP address, timestamp

**Use Cases:**
- Admin audits who changed what and when
- Troubleshooting registration discrepancies
- Compliance/legal documentation

### 5. Class/Level Availability Management

**Admin Control:**
- admin.php displays availability matrix
- Admin toggles each class/level combination
- Saved in `class_availability` table
- JavaScript on registration form uses JSON to hide unavailable options
- Prevents over-registration in specific class/levels

**Database Schema:**
```
class_availability (class, level, is_available)
Examples:
('Violin', 'I', 1)
('Violin', 'II', 0)  ← Unavailable
('Master', 'Harp', 1)
```

### 6. Flexible Payment Methods

**Credit Card (Stripe):**
- Immediate processing
- Supports partial payments (charge only the difference on edits)
- Transaction ID stored for reconciliation
- Secure through Stripe PCI compliance

**Purchase Order:**
- For groups/organizations
- Not allowed for individuals
- Manual reconciliation required
- Provides time for budget approval

### 7. Multi-Level Editing Workflows

**Complete Edit:** Full registration modification with cost recalculation
**Incremental Edit:** Add students, change classes, update tickets separately
**Support Requests:** Correction requests and removal requests routed to admin
**Role:** All edits originate from director (via token), no role/permission system

### 8. Email Notifications

**Types Sent:**
- Registration confirmation (after new or edit)
- Resume/edit link (on-demand)
- Admin correction request (routed to admin email)
- Student removal request (routed to admin email)

**PHPMailer Integration:**
- SMTP configuration via environment variables
- Fallback to mail() if SMTP not configured
- Error logging on send failures
- Plain text format (no HTML formatting currently)

### 9. reCAPTCHA Integration

**Purpose:** Prevent bot registrations
**Implementation:**
- Google reCAPTCHA v3 token verified server-side
- Helpers::validateRecaptcha() makes API call to Google
- Validates score/success status
- Error handling if validation fails

**Configuration:**
- `RECAPTCHA_SITE_KEY` for JavaScript
- `RECAPTCHA_SECRET` for server validation
- Optional (skipped if no secret configured)

---

## Deployment Considerations

### Environment Variables Required

```env
# Database
DB_HOST=localhost
DB_NAME=timc_registration
DB_USER=root
DB_PASS=password
DB_CHARSET=utf8mb4

# Email (PHPMailer SMTP)
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=app-password
MAIL_FROM=noreply@tucsonmariachi.org
MAIL_FROM_NAME=TIMC Registration

# Stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLIC_KEY=pk_live_...

# reCAPTCHA
RECAPTCHA_SITE_KEY=6Lc...
RECAPTCHA_SECRET=6Lc...
```

### Database Schema Required

The system expects these tables to exist (not created by application):
- Groups
- participants
- directors
- assistant_directors
- songs
- payments
- partial_registrations
- class_availability
- change_log

### Web Server Requirements

- PHP 8.0+ with PDO MySQL
- Composer for dependency management
- HTTPS recommended (for tokens and payment processing)
- Write permissions to session directory
- PHP mail() or SMTP access for emails

---

## Summary

This registration system provides a complete, token-based registration flow with flexible editing, multiple payment methods, and comprehensive change logging. The layered architecture separates concerns between web pages, business logic, data access, and utilities, making the code maintainable and testable. The use of database models as static facades simplifies CRUD operations while the service layer orchestrates complex workflows like registration creation, editing, and payment processing.
