# Block System Case Study: Tuffy Appointment Form (Block 122)
## Migration Blueprint for Rails Views Refactor
**Date**: 2025-10-24
**Block ID**: 122 (Tuffy Appt Main)
**Account ID**: 11187 (Account-specific block)
**Status**: Real production block analyzed
---
## Executive Summary
Block 122 is **THE complex appointment scheduling form** - representing the highest complexity in PISTN's Block system. It demonstrates:
- ✅ 43KB of mixed HTML, Handlebars templating, and inline JavaScript
- ✅ 4-step wizard workflow with client-side state management
- ✅ Complex conditional rendering based on account settings
- ✅ Inline JavaScript with jQuery and event handlers
- ✅ Form validation with business logic
- ✅ Multiple child block yields
- ✅ Data binding between form fields and confirmation display
- ✅ Phone verification modal workflow
- ✅ Google Tag Manager event tracking
- ✅ Existing vehicle lookup and selection
This block is a **stress test** for the migration strategy - if we can migrate Block 122 to Rails, we can migrate anything.
---
## Part 1: Block 122 Architecture Analysis
### Database Record
```
ID: 122
Name: Tuffy Appt Main
Tag: (empty)
Role: (empty)
Account ID: 11187 (Account-specific - not universal)
Parent ID: NULL (top-level block)
Content Length: 43,762 bytes (~44 KB)
```
### Key Statistics
- **Total Lines of Code**: ~1,150 lines (mixed HTML/JavaScript)
- **JavaScript Lines**: ~600 lines (inline, not external file)
- **HTML Structure**: Bootstrap grid-based 4-step wizard
- **Form Steps**:
1. Service selection with service categories
2. Date/time picker with calendar UI
3. Customer information with vehicle selection
4. Confirmation review
- **Form Fields**: 25+ input fields across all steps
- **Conditional Renders**: 12+ places using `{{ }}` handlebars syntax
- **Event Handlers**: 10+ JavaScript event listeners
- **API Endpoints**: Calls to `/verify_phone_number`, `/appointment` (form submit)
---
## Part 2: Detailed Structure Breakdown
### Data Dependencies
**Account-Level Properties Accessed**:
```javascript
// Via {{account.property}} syntax
account.id // Account ID
account.name // Display name
account.address_str // Address
account.phone_number // Phone
account.online_scheduling_promo_message // Promotional message
account.require_addr_for_appt // Address field requirement
account.require_costco_membership_number? // Costco membership requirement
account.show_towing // Towing field requirement
account.require_license_plate // License plate requirement
account.use_mobile_verification // Phone verification toggle
account.store_hours_options // Hour dropdowns (HTML options)
account.store_hours_html // Rendered hours display
account.render_new_appt_times_string // Calendar filtering flag
account.appt_min_hours // Minimum hours before appointment
```
**Page Object**:
```javascript
page.intro_or_default_text // Customizable intro copy
```
**Model-Level Properties**:
```javascript
account.visible_service_categories // yield: Service category loop
account.account_services // yield: Services within category
```
### Form Structure
```
Appointment Form (id="appt_form")
├── Step 1: Service Selection
│ ├── {{yield account.visible_service_categories}} → Loop through categories
│ ├── Wait/Drop-off dropdown
│ └── Submit: "Select date/time"
│
├── Step 2: Date & Time
│ ├── jQuery UI Datepicker calendar
│ ├── Phone Verification Section (conditional on use_mobile_verification)
│ ├── Preferred Time dropdown ({{account.store_hours_options}})
│ ├── Towing dropdown (conditional on account.show_towing)
│ └── Submit: "Enter your information"
│
├── Step 3: Customer Information
│ ├── Personal Info (first name, last name, email, phone)
│ ├── Costco Membership (conditional on require_costco_membership_number)
│ ├── {{yield Address Fields if account.require_addr_for_appt}}
│ ├── Vehicle Selection
│ │ ├── Existing Vehicles List (populated by JavaScript)
│ │ └── {{yield New Veh Form}} (for new vehicles)
│ ├── Car Issues textarea
│ └── Submit: "Confirm your Appointment Request"
│
├── Step 4: Confirmation
│ ├── Summary Review
│ │ ├── Customer Name & Vehicle
│ │ ├── Location ({{account.name}}, {{account.address_str}})
│ │ ├── Services
│ │ ├── Date/Time
│ │ └── Notes
│ ├── {{yield TuffySignUps}} (right sidebar)
│ ├── Recaptcha box
│ └── Submit: "Request Appointment"
│
└── Modal: Phone Verification
├── Input: Verification code
├── Actions: Verify Code, Resend Code
└── Status messages
```
### JavaScript Functions (Critical to Migration)
```javascript
window.choose_step(stepNum)
// Navigate between form steps
// Updates UI, scrolls to top, shows/hides sidebars
// Core: $('.form-step').hide(); $(`#form-step-${i}`).show();
window.validate_step(stepNum)
// Complex validation logic including:
// - Required field checking
// - Special vehicle selection logic (existing vs new)
// - Date validation (must be 24+ hours from now)
// - Store hours validation
// - Phone verification check (if enabled)
// - Google Tag Manager event firing
window.set_confirm()
// Updates confirmation display when fields change
// Binds to: input[name="services[]"], input[name="customer[*]"]
window.updateConfirmationStep()
// Updates step 4 confirmation with selected vehicle
window.verifyPhoneNumber(accountId, number)
// AJAX call to /verify_phone_number
// Shows modal with verification code input
// Expiry: 5 minutes
window.updateTimeSlots(dateStr, accountId, wait, selectedServiceIds)
// AJAX: Fetches available time slots
// Called when date selected IF render_new_appt_times_string == true
window.change_location()
// Reopens location selection
window.validate_store_hours()
// Checks if appointment time is within store hours
// Uses window.active_hours array
window.removeSelectError / removeError
// Event handlers for error removal on user input
```
### Child Block Yields
Block 122 references these child blocks via `{{yield ...}}`:
1. **{{yield account.visible_service_categories}}**
- Loop through service categories
- Each category has checkboxes for services
- Calls `{{yield account.account_services}}` for services in category
- Example: `yield Block 25342` (service selector)
2. **{{yield Address Fields if account.require_addr_for_appt}}**
- Conditional address input block
- Only shown if account.require_addr_for_appt is true
3. **{{yield New Veh Form}}**
- New vehicle year/make/model selectors
- Example: block 24794, 24793 (vehicle form blocks)
4. **{{yield TuffySignUps}}**
- Newsletter signup on confirmation step
- Example: block 2418 (empty placeholder)
---
## Part 3: Complexity Metrics
### Why Block 122 is High Complexity
| Factor | Score | Details |
|--------|-------|---------|
| **Size** | 9/10 | 43KB, ~1,150 lines |
| **JavaScript** | 10/10 | 600+ lines of inline JS, event handlers, AJAX |
| **Conditional Logic** | 7/10 | 12+ places with `if` conditions, feature flags |
| **State Management** | 9/10 | Multi-step wizard with complex validation |
| **Data Binding** | 8/10 | Form fields must sync with confirmation display |
| **External Dependencies** | 8/10 | jQuery UI Datepicker, reCAPTCHA, Google Tag Manager |
| **API Calls** | 6/10 | POST /appointment, GET /verify_phone_number, XHR for time slots |
| **Testing Difficulty** | 9/10 | Integration + E2E testing required for full coverage |
**Overall Complexity**: **8.5/10** - Enterprise-grade appointment form with sophisticated UX
---
## Part 4: Migration Strategy
### Approach: Decompose into Rails Components
Instead of replacing Block 122 directly, decompose into:
1. **Views** - Multi-step form partials
2. **Controller** - Step navigation, validation
3. **Service** - Business logic (validation, time slot fetching)
4. **Presenter** - Data preparation for views
5. **JavaScript** - Extract to external JS files
### File Structure
```
app/views/appointments/
├── _appointment_form.html.erb # Main form wrapper
├── _step_1_services.html.erb # Service selection
├── _step_2_datetime.html.erb # Date/time picker
├── _step_3_customer_info.html.erb # Personal info & vehicle
├── _step_4_confirmation.html.erb # Review & submit
└── _phone_verification_modal.html.erb # Phone verification modal
app/controllers/
└── appointments_controller.rb # Step navigation, submission
app/presenters/
└── appointment_form_presenter.rb # Data preparation, conditionals
app/services/
└── validate_appointment_service.rb # Complex validation logic
└── fetch_appointment_slots_service.rb # Calendar/slot fetching
app/assets/javascripts/
└── appointment_form.js # Extract inline JS here
```
### Step 1: Create Presenter
File: `app/presenters/appointment_form_presenter.rb`
```ruby
class AppointmentFormPresenter
def initialize(account, appointment = nil)
@account = account
@appointment = appointment || Appointment.new
end
# Account properties
def account_id; @account.id; end
def account_name; @account.name; end
def account_address; @account.address_str; end
def account_phone; @account.phone_number; end
# Conditionals
def show_online_scheduling_promo?
@account.online_scheduling_promo_message.present?
end
def online_scheduling_promo_message
@account.online_scheduling_promo_message
end
def require_address?
@account.require_addr_for_appt?
end
def require_costco_membership?
@account.require_costco_membership_number?
end
def show_towing?
@account.show_towing?
end
def use_mobile_verification?
@account.use_mobile_verification?
end
def minimum_appointment_hours
@account.appt_min_hours || 24
end
def filter_time_slots?
@account.render_new_appt_times_string == 'true'
end
# Service categories (for yield loop)
def visible_service_categories
@account.visible_service_categories
end
# Store hours
def store_hours_html
@account.store_hours_html
end
def store_hours_options
@account.store_hours_options
end
def store_hours_json
# For JavaScript time validation
@account.store_hours_for_js.to_json
end
# Intro text
def intro_text
Page.intro_or_default_text || "Schedule your appointment with us."
end
end
```
### Step 2: Create Controller
File: `app/controllers/appointments_controller.rb`
```ruby
class AppointmentsController < ApplicationController
before_action :set_account
before_action :set_presenter, only: [:new]
# Show appointment form
def new
@appointment = Appointment.new
@presenter = AppointmentFormPresenter.new(@account)
# For existing vehicles (pre-fill if customer found)
@existing_vehicles = @account.vehicles.where(
phone_number: params[:phone]
) if params[:phone].present?
end
# Handle step navigation (AJAX)
def validate_step
step = params[:step].to_i
data = JSON.parse(params[:form_data] || '{}')
result = ValidateAppointmentService.perform(
account: @account,
step: step,
data: data
)
if result[:valid]
render json: { valid: true }
else
render json: { valid: false, errors: result[:errors] }, status: :unprocessable_entity
end
end
# Fetch available time slots
def available_slots
date = params[:date]
services = params[:service_ids] || []
wait_type = params[:wait]
slots = FetchAppointmentSlotsService.perform(
account: @account,
date: date,
service_ids: services,
wait_type: wait_type
)
render json: { slots: slots }
end
# Verify phone number for existing customer lookup
def verify_phone
phone = params[:phone]
result = VerifyPhoneService.perform(phone: phone)
if result[:valid]
# Send verification code (SMS/email)
render json: {
valid: true,
verification_code: result[:code], # For testing only
expires_at: 5.minutes.from_now
}
else
render json: { valid: false }, status: :unprocessable_entity
end
end
# Create appointment
def create
service = CreateAppointmentService.perform(
account: @account,
customer_params: customer_params,
appointment_params: appointment_params
)
if service.is_a?(Appointment)
# Send confirmation email, etc.
render json: { success: true, appointment_id: service.id }
else
render json: { success: false, error: service }, status: :unprocessable_entity
end
end
private
def set_account
@account = current_account
end
def set_presenter
@presenter = AppointmentFormPresenter.new(@account)
end
def customer_params
params.require(:customer).permit(
:first_name, :last_name, :email, :phone,
:costco_membership_number
)
end
def appointment_params
params.require(:appointment).permit(
:appointment_date, :appointment_time, :wait,
:towing, :car_issues, :car_year, :car_make, :car_model,
:service_ids => []
)
end
end
```
### Step 3: Create Views
#### File: `app/views/appointments/new.html.erb`
```erb
<%= render 'appointment_form', presenter: @presenter, existing_vehicles: @existing_vehicles %>
```
#### File: `app/views/appointments/_appointment_form.html.erb`
```erb
```
#### File: `app/views/appointments/_step_1_services.html.erb`
```erb
```
#### File: `app/views/appointments/_step_2_datetime.html.erb`
```erb
```
#### File: `app/views/appointments/_step_3_customer_info.html.erb`
```erb
```
#### File: `app/views/appointments/_step_4_confirmation.html.erb`
```erb
```
### Step 4: Extract JavaScript to External File
File: `app/assets/javascripts/appointment_form.js`
```javascript
class AppointmentForm {
constructor(options = {}) {
this.accountId = options.accountId;
this.filterTimeSlots = options.filterTimeSlots;
this.usePhoneVerification = options.usePhoneVerification;
this.minimumHours = options.minimumHours || 24;
this.storeHours = options.storeHours || {};
this.currentStep = 1;
this.phoneVerified = false;
this.selectedExistingVehicle = null;
this.init();
}
init() {
this.setupEventListeners();
this.initDatepicker();
this.setupStepNavigation();
}
setupEventListeners() {
// Service checkboxes
document.querySelectorAll('.service-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', () => this.updateServicesSummary());
});
// Form field changes
document.querySelectorAll('input[name^="customer["], select[name^="appointment["]').forEach(field => {
field.addEventListener('change', () => this.updateConfirmationDisplay(field));
});
// Wait/Drop-off
document.getElementById('wait').addEventListener('change', (e) => {
document.getElementById('confirm_wait').textContent = e.target.value;
});
// Phone verification
if (this.usePhoneVerification) {
document.getElementById('verifyPhoneBtn').addEventListener('click', () => {
this.verifyPhone();
});
}
// Existing vehicles
if (document.getElementById('existingVehiclesList').children.length > 0) {
document.querySelectorAll('input[name="vehicle_selection"]').forEach(radio => {
radio.addEventListener('change', (e) => {
this.selectExistingVehicle(e.target.value);
});
});
document.getElementById('addNewVehicleBtn').addEventListener('click', () => {
this.switchToNewVehicle();
});
}
}
initDatepicker() {
// Initialize jQuery UI datepicker
// Implement date selection logic
}
setupStepNavigation() {
// Setup step transitions
}
goToStep(step) {
if (!this.validateStep(this.currentStep)) {
return false;
}
this.currentStep = step;
this.showStep(step);
return false;
}
showStep(step) {
// Hide all steps
document.querySelectorAll('[id^="form-step-"]').forEach(el => {
el.style.display = 'none';
});
// Show selected step
document.getElementById(`form-step-${step}`).style.display = 'block';
// Update progress indicator
this.updateProgressIndicator(step);
// Update sidebars
if (step === 4) {
document.getElementById('summary-sidebar').style.display = 'none';
document.getElementById('sign-up-sidebar').style.display = 'block';
} else {
document.getElementById('sign-up-sidebar').style.display = 'none';
document.getElementById('summary-sidebar').style.display = 'block';
}
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
}
validateStep(step) {
// Complex validation logic
// Returns true if valid, false otherwise
}
updateConfirmationDisplay(field) {
// Update confirmation section as user types
}
updateServicesSummary() {
// Update services display
}
verifyPhone() {
// AJAX call to verify phone number
}
selectExistingVehicle(vehicleId) {
// Handle existing vehicle selection
}
switchToNewVehicle() {
// Switch to new vehicle form
}
change_location() {
// Reopen location selection
}
}
```
---
## Part 5: Risks and Mitigation
### High Risk Areas
| Risk | Severity | Mitigation |
|------|----------|-----------|
| **Complex JavaScript Behavior** | CRITICAL | Extract to external file, comprehensive testing, unit tests for validation logic |
| **Form State Management** | HIGH | Use Stimulus controllers or React component for state |
| **AJAX Calls (time slots, phone verify)** | HIGH | Mock endpoints in tests, use fetch API instead of jQuery |
| **Date Validation** | HIGH | Use Rails date validation + JavaScript, test edge cases |
| **Existing Vehicle Lookup** | MEDIUM | Create service for vehicle lookup, test with real data |
| **Phone Verification** | MEDIUM | Create SMS service abstraction, test verification flow |
| **Google Tag Manager** | LOW | Keep GTM code separate, can be added back easily |
| **Accessibility** | MEDIUM | Add ARIA labels, test with screen readers |
---
## Part 6: Implementation Timeline
### Phase 1: Setup & Views (2 weeks)
- Create presenter class
- Create view partials
- Create controller
- Basic HTML structure
### Phase 2: JavaScript & Validation (2 weeks)
- Extract inline JavaScript to Stimulus controller
- Implement step validation
- Implement date picker integration
- Implement form field synchronization
### Phase 3: Services & API Integration (1.5 weeks)
- Create validation services
- Create time slot fetching service
- Create phone verification service
- Create appointment creation service
### Phase 4: Testing & Polish (1.5 weeks)
- Unit tests for services
- Integration tests for form flow
- E2E tests with Capybara
- Performance testing
- Browser compatibility testing
### Phase 5: Rollout (1 week)
- Feature flag configuration
- Beta testing with test account
- Gradual rollout to production
- Monitor error rates
- Collect user feedback
**Total**: 8 weeks for complete migration
---
## Part 7: Conclusion
Block 122 represents **the highest complexity** in PISTN's Block system. Its migration path shows:
1. **Decomposition**: Break monolithic Block into separate concerns (views, controller, services)
2. **Modernization**: Replace inline JavaScript with standard Rails patterns (Stimulus, fetch API)
3. **Testability**: Extract business logic into services that can be tested independently
4. **Maintainability**: Standard Rails conventions make code easier to understand and modify
**Success Criteria**:
- ✅ Feature parity with original Block 122
- ✅ 100% form submission success rate
- ✅ Phone verification flow works
- ✅ Existing vehicle lookup functional
- ✅ All validations pass
- ✅ Performance within 10% of original
- ✅ Accessibility meets WCAG 2.1 AA
**If Block 122 can be migrated successfully, ALL blocks can be migrated.**
---
**Status**: Architectural analysis complete - ready for implementation
**Next Steps**:
1. Create Stimulus controller for form state management
2. Implement form partials with real data
3. Build validation services
4. Begin E2E testing framework