Calendar/coding-sessions/2025-11-14-booking-resource-architecture-redesign.md

523 lines
16 KiB
Markdown
Raw Normal View History

# Booking & Resource Architecture Redesign
**Date:** 2025-11-14
**Duration:** ~2+ hours
**Initial Scope:** Create IResource interface and fix `Date | any` type
**Actual Scope:** Fundamental redesign of booking architecture with service-level resource assignment
---
## Executive Summary
What started as a simple task to create an `IResource` interface evolved into a comprehensive architectural redesign after critical developer questioning revealed fundamental flaws in the initial approach. Through 4 major iterations and multiple course corrections, we arrived at a clean architecture that properly models split-resource bookings (e.g., student + master stylist working on the same booking).
**Key Achievement:** Avoided building 3 different incorrect architectures by thinking through real business scenarios before committing to code.
---
## Original Request
> "Can we create an IResource now? Then you can also fix `data: Date | any`"
**Expected:** 10 minutes - create interface, update type, done.
**Reality:** 2+ hours of architectural discussion, 4 major iterations, 930 lines of documentation.
---
## Iteration History
### Iteration 1: "Booking HAS resourceId (Primary Resource)" ❌
**My Initial Assumption:**
```typescript
export interface IBooking {
id: string;
resourceId: string; // Primary resource
customerId?: string;
type: BookingType;
services?: IBookingService[];
}
```
**Rationale:** Bookings are always resource-bound (someone performs the service), so booking should have a resourceId.
**Developer Challenge:**
> "A booking with 2 services can be split between multiple resources. For example, a large hair coloring - the student handles hair wash and eyebrows, the master stylist does the actual coloring."
**Problem Identified:**
- Booking.resourceId assumes ONE primary resource
- But in reality: hårvask (student) + bundfarve (master) = TWO resources, neither "primary"
- Model doesn't support split-resource bookings
**Result:** ❌ Discarded after 15 minutes of discussion
---
### Iteration 2: "Booking HAS Primary + Services Have Resources" ❌
**My Second Attempt:**
```typescript
export interface IBooking {
id: string;
resourceId: string; // "Primary" responsible person
customerId?: string;
services?: IBookingService[];
}
export interface IBookingService {
serviceId: string;
resourceId: string; // Who performs THIS service
}
```
**Rationale:** Keep primary resource for "who is responsible" while allowing service-level assignment.
**Developer Challenge:**
> "Why should there be a primary? Use case: Customer chooses service, UI shows it can be split between Karina and Nanna, customer says yes. Here there's no primary, both are master stylists."
**Problem Identified:**
- "Primary resource" is an artificial concept
- Equal splits (Karina + Nanna) don't have a "responsible person"
- Creates confusion: "Who is primary if they're equal?"
**Result:** ❌ Discarded after thorough use case review
---
### Iteration 3: "Booking WITHOUT resourceId, Only Services Have Resources" ✅
**My Third Attempt:**
```typescript
export interface IBooking {
id: string;
customerId: string; // REQUIRED
services: IBookingService[]; // REQUIRED
// NO resourceId at all!
}
export interface IBookingService {
serviceId: string;
resourceId: string; // Resource performs THIS service
}
```
**My Concern:**
> "But what about vacation/break? Those are resource-bound but have no services!"
**Developer Response:**
> "Vacation is not a booking, it's just a CalendarEvent with resourceId. Bookings are only for customers. We always have a booking for customer services."
**Breakthrough Realization:**
- **Booking = Customer + Services** (business domain)
- **CalendarEvent = Scheduling + Resource** (time domain)
- Vacation/break/meeting are CalendarEvents WITHOUT bookings
**Architecture Clarified:**
```typescript
// Booking = ONLY customer services
export interface IBooking {
customerId: string; // REQUIRED - always customer
services: IBookingService[]; // REQUIRED - always has services
// NO resourceId
// NO type (always customer)
}
// CalendarEvent = ALL scheduling
export interface ICalendarEvent {
type: CalendarEventType; // 'customer' | 'vacation' | 'break' | ...
bookingId?: string; // Only if type = 'customer'
resourceId?: string; // Which resource owns this slot
}
```
**Result:** ✅ **ACCEPTED** - Clean separation of concerns
---
### Iteration 4: "BookingType Should Be Removed" ✅
**My Miss:**
I created `BookingType` union even though `IBooking` no longer had a `type` field.
**Developer Catch:**
> "But wait... booking type is gone, right? It's always customer-related."
**Fix Applied:**
- Renamed `BookingType``CalendarEventType`
- Clarified: Only `ICalendarEvent` uses this type, not `IBooking`
- Updated all references
**Result:** ✅ Type system now consistent
---
## Key Architectural Decisions
### Decision 1: Booking = Customer ONLY
**Before:** Booking could be customer, vacation, break, meeting
**After:** Booking is ONLY for customer services
```typescript
// ✅ Valid
{
"customerId": "CUST456",
"services": [{ "resourceId": "EMP001" }]
}
// ❌ Invalid - vacation is NOT a booking
{
"type": "vacation",
"resourceId": "EMP001"
}
```
**Vacation is now:**
```typescript
{
"type": "vacation",
"bookingId": null, // NO booking
"resourceId": "EMP001" // Just a time slot
}
```
---
### Decision 2: Resources Assigned Per Service
**Before:** Booking.resourceId (primary)
**After:** IBookingService.resourceId (per service)
**Enables:**
- Split resources: student + master
- Equal splits: Karina + Nanna
- Resource reassignment: "student is sick, assign to another student"
**Example - Split Resource Booking:**
```json
{
"customerId": "CUST456",
"services": [
{
"serviceName": "Hårvask",
"resourceId": "STUDENT001",
"baseDuration": 30
},
{
"serviceName": "Bundfarve",
"resourceId": "EMP001",
"baseDuration": 90
}
]
}
```
Two CalendarEvents created:
- Event 1: Student's calendar, 13:00-13:30
- Event 2: Master's calendar, 13:30-15:00
- Both reference same booking
---
### Decision 3: 1 Booking → N Events
**Relationship:** One booking can span multiple events
**Scenarios:**
1. **Split Session:** 180min service split across 2 days (same resource)
2. **Split Resources:** Student + Master (different resources)
3. **Equal Split:** Karina + Nanna (two masters, equal)
**Key Insight:** Events are scheduled per resource, not per booking.
---
### Decision 4: CalendarEvent.type Determines Booking Existence
```typescript
type CalendarEventType =
| 'customer' // HAS booking
| 'vacation' // NO booking
| 'break' // NO booking
| 'meeting' // NO booking
| 'blocked'; // NO booking
```
**Business Rule:** If `event.type === 'customer'`, then `event.bookingId` must be set.
---
## Database Schema Changes
### Before (Iteration 1 - Wrong)
```sql
CREATE TABLE Booking (
ResourceId VARCHAR(50) NOT NULL, -- ❌ Wrong: assumes one resource
CustomerId VARCHAR(50) NULL, -- ❌ Wrong: should be required
Type VARCHAR(20) NOT NULL -- ❌ Wrong: always customer
);
CREATE TABLE BookingService (
BookingId VARCHAR(50) NOT NULL,
ServiceId VARCHAR(50) NOT NULL
-- ❌ Missing: ResourceId
);
```
### After (Final Design - Correct)
```sql
CREATE TABLE Booking (
Id VARCHAR(50) PRIMARY KEY,
CustomerId VARCHAR(50) NOT NULL, -- ✅ REQUIRED
Status VARCHAR(20) NOT NULL,
TotalPrice DECIMAL(10,2) NULL,
-- ✅ NO ResourceId (moved to service level)
-- ✅ NO Type (always customer)
FOREIGN KEY (CustomerId) REFERENCES Customer(Id)
);
CREATE TABLE BookingService (
BookingId VARCHAR(50) NOT NULL,
ServiceId VARCHAR(50) NOT NULL,
ResourceId VARCHAR(50) NOT NULL, -- ✅ NEW: Resource per service
CustomPrice DECIMAL(10,2) NULL,
SortOrder INT NULL,
FOREIGN KEY (BookingId) REFERENCES Booking(Id),
FOREIGN KEY (ServiceId) REFERENCES Service(Id),
FOREIGN KEY (ResourceId) REFERENCES Resource(Id) -- ✅ NEW FK
);
CREATE TABLE CalendarEvent (
Id VARCHAR(50) PRIMARY KEY,
Start DATETIME NOT NULL,
End DATETIME NOT NULL,
Type VARCHAR(50) NOT NULL, -- ✅ Distinguishes customer vs vacation
BookingId VARCHAR(50) NULL, -- ✅ Nullable (only customer has booking)
ResourceId VARCHAR(50) NULL, -- ✅ Denormalized for performance
CustomerId VARCHAR(50) NULL, -- ✅ Denormalized for performance
FOREIGN KEY (BookingId) REFERENCES Booking(Id)
);
```
---
## Code Changes Summary
### Files Created (3)
1. **src/types/ResourceTypes.ts** - IResource interface
2. **src/types/BookingTypes.ts** - IBooking + IBookingService + CalendarEventType
3. **src/types/CustomerTypes.ts** - ICustomer interface
### Files Modified (2)
4. **src/types/ColumnDataSource.ts** - Changed `Date | any` to `Date | IResource`
5. **src/types/CalendarTypes.ts** - Updated ICalendarEvent to use CalendarEventType
### Documentation Created (1)
6. **docs/booking-event-architecture.md** - 930 lines comprehensive guide
- 6 detailed examples
- Database schemas (SQL + IndexedDB)
- 5 business rules
- 4 common pitfalls
- Analytics patterns
- Query examples
---
## Statistics
| Metric | Count |
|--------|-------|
| **Time Spent** | 2+ hours |
| **Initial Estimate** | 10 minutes |
| **Major Iterations** | 4 |
| **Architectural Pivots** | 3 |
| **Developer Catches** | 5+ critical issues |
| **Interfaces Created** | 5 (IResource, IBooking, IBookingService, ICustomer, CalendarEventType) |
| **Interfaces Modified** | 2 (IColumnInfo, ICalendarEvent) |
| **Documentation Written** | 930 lines |
| **Examples Documented** | 6 scenarios |
| **Business Rules Defined** | 5 |
| **Common Pitfalls Identified** | 4 |
| **Database Tables Changed** | 3 (Booking, BookingService, CalendarEvent) |
---
## Developer Interventions (Critical Thinking Points)
### 1. "What if multiple resources work on same booking?"
**Prevented:** Single-resource assumption that would block split bookings
### 2. "Why should there be a primary resource?"
**Prevented:** Artificial hierarchy that doesn't match business reality
### 3. "Is vacation a booking?"
**Resulted in:** Clean separation - Booking = customers only, Events = everything
### 4. "If the student is sick?"
**Resulted in:** Service-level resource assignment enabling reassignment
### 5. "BookingType is gone now, right?"
**Resulted in:** Consistent naming - CalendarEventType for events, not bookings
---
## What We Almost Built (And Avoided)
### ❌ Wrong Architecture 1: Primary Resource Model
Would have required:
- Complex logic to determine "who is primary"
- Arbitrary decision making (is master or student primary?)
- Doesn't support equal splits (Karina + Nanna)
### ❌ Wrong Architecture 2: Booking.resourceId + Service.resourceId
Would have resulted in:
- Redundant data
- Confusion about which resourceId to use
- "Primary" concept that doesn't exist in business logic
### ❌ Wrong Architecture 3: Vacation as Booking
Would have created:
- Optional fields everywhere (services? customer? for vacation?)
- Complex validation logic
- Confusing domain model
---
## Final Architecture Benefits
### ✅ Clean Separation of Concerns
- **Booking** = Customer + Services (WHAT + HOW MUCH)
- **CalendarEvent** = Time + Resource (WHEN + WHO)
### ✅ Supports All Business Scenarios
1. Simple booking (one customer, one service, one resource)
2. Split session (one booking, multiple days)
3. Split resources (student + master)
4. Equal splits (two masters, no primary)
5. Queued bookings (no time slot yet)
6. Vacation/break (no booking, just time blocking)
### ✅ Type-Safe
- Required fields are enforced: `customerId: string`, `services: IBookingService[]`
- No optional fields that "sometimes" matter
- CalendarEventType clearly distinguishes booking vs non-booking events
### ✅ Flexible Resource Assignment
- Assign different resources per service
- Reassign if resource unavailable (student sick)
- No "primary" to maintain
### ✅ Query-Optimized
- CalendarEvent denormalizes resourceId for fast queries
- No JOIN needed to find "Karina's events"
- IndexedDB-friendly (no JOIN support)
---
## Lessons Learned
### 1. Question Assumptions Early
**My assumption:** "Bookings are resource-bound"
**Reality:** Bookings are customer-bound, resources are service-bound
### 2. Use Real Business Scenarios
Abstract discussions led to wrong models. Concrete examples ("student + master split") revealed the truth.
### 3. Don't Force Hierarchies
"Primary resource" seemed logical but didn't match business reality. Equal splits exist.
### 4. Separation of Concerns Matters
Mixing booking (business) with scheduling (time) created optional fields everywhere.
### 5. Developer Domain Knowledge is Critical
I had TypeScript expertise, but developer had business domain knowledge. The combination caught 5+ critical flaws before they became code.
---
## Documentation Deliverables
### booking-event-architecture.md (930 lines)
**Sections:**
1. Core Principles - Separation of concerns
2. Entity Definitions - Booking vs CalendarEvent
3. Relationship Model - 1:N (one booking → many events)
4. 6 Detailed Examples:
- Split session (same resource, multiple days)
- Split resources (master + student)
- Equal split (two masters)
- Queued booking
- Vacation (no booking)
- Simple reminder
5. Database Schema - SQL (normalized) + IndexedDB (denormalized)
6. API Data Flow - Backend JOIN → Frontend denormalization
7. Business Rules - 5 rules with code examples
8. Analytics & Reporting - Service duration vs actual time
9. TypeScript Interfaces - Complete reference
10. Common Pitfalls - 4 mistakes to avoid
11. Summary Table - Quick reference
12. Decision Guide - "Where does this belong?"
---
## Build Results
**All TypeScript compilation successful**
- 57 files processed
- 0 errors
- 0 warnings
- Build time: ~1.2s
**Type safety improved**
- Removed `Date | any` (was unsafe)
- Added `Date | IResource` (type-safe union)
- Added CalendarEventType (was `string`)
---
## Conclusion
**Initial request:** "Create IResource interface"
**Time estimated:** 10 minutes
**Time actual:** 2+ hours
**Why the difference?**
Because the developer asked the right questions at the right time:
1. "What if multiple resources work together?"
2. "Why assume a primary?"
3. "Is vacation really a booking?"
4. "What if we need to reassign?"
Each question revealed a flaw in my assumptions. We iterated 4 times, discarded 3 approaches, and arrived at an architecture that actually matches the business model.
**This is exactly how good software development should work.**
The 2 hours spent thinking prevented weeks of refactoring later. Critical thinking and domain knowledge prevented premature optimization and over-engineering.
---
## Next Steps (Not Part of This Session)
**Phase 2: Resource Calendar Implementation**
- Create ResourceColumnDataSource (returns IColumnInfo with IResource data)
- Create ResourceHeaderRenderer (renders resource headers instead of dates)
- Implement resource-mode switching (date-mode ↔ resource-mode)
- Add repository methods for resource queries
**Phase 3: Booking Management**
- Implement booking creation flow
- Service-to-event mapping logic
- Split-resource UI (assign services to different resources)
- Resource reassignment (when student is sick)
---
**Session End**
**Key Takeaway:** Slow down to speed up. 2 hours of architecture discussion saved weeks of wrong implementation.