Fundamentally refactors booking system to support: - Split-resource bookings - Service-level resource assignment - Clear separation between booking and calendar events Introduces new interfaces and type definitions to model complex booking scenarios, enabling more flexible and accurate resource management Improves type safety by removing ambiguous type annotations
522 lines
16 KiB
Markdown
522 lines
16 KiB
Markdown
# 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.
|