# Booking vs CalendarEvent Architecture

**Status:** Active Architecture Document
**Last Updated:** 2025-11-14
**Purpose:** Define the separation of concerns between Booking (business domain) and CalendarEvent (scheduling domain)

---

## Core Principle: Separation of Concerns

The calendar system uses **two distinct entities** to model appointments:

1. **Booking** = Customer + Services (business container - WHAT, WHO, HOW MUCH)
2. **CalendarEvent** = Scheduling container (WHEN, WHICH RESOURCE)

This separation enables:
- ✅ Flexible scheduling (one booking → multiple time slots or resources)
- ✅ Queue management (bookings without scheduled time)
- ✅ Time adjustments without business logic changes
- ✅ Clear analytics (service duration vs actual time spent)
- ✅ Split-resource bookings (one booking → multiple resources)

---

## Entity Definitions

### Booking (Domain Model)

**Purpose:** Represents a customer service booking ONLY

**Contains:**
- `customerId` - Who receives the service (REQUIRED)
- `status` - Lifecycle state: created, arrived, paid, noshow, cancelled
- `services[]` - Services to be performed (REQUIRED, each has resourceId)
- `totalPrice` - Business value (can differ from service prices)
- `tags`, `notes`, `createdAt` - Metadata

**Does NOT contain:**
- ❌ `start` / `end` - Timing is on CalendarEvent
- ❌ `type` - Always customer, so redundant
- ❌ `resourceId` - Resources assigned per service (IBookingService.resourceId)

**Key Rules:**
- Booking is ALWAYS customer-related (`customerId` required)
- Booking ALWAYS has services (`services[]` required)
- Resources are assigned at SERVICE level (not booking level)
- Booking can exist WITHOUT CalendarEvent (pending/queued state)
- Booking CANNOT be deleted if CalendarEvents reference it
- **Vacation/break/meeting are NOT bookings** - only CalendarEvents

---

### CalendarEvent (Scheduling Model)

**Purpose:** Represents a time slot in the calendar

**Contains:**
- `start` - When does it happen (MASTER)
- `end` - When does it end (MASTER)
- `allDay` - Is it an all-day event
- `type` - CalendarEventType: 'customer', 'vacation', 'break', 'meeting', 'blocked'
- `bookingId` - Reference to business data (nullable - only 'customer' type has booking)
- `resourceId` - Which resource owns this time slot
- `customerId` - Denormalized for query performance
- `title`, `description` - Display properties

**Key Rules:**
- CalendarEvent is the MASTER for timing
- CalendarEvent can exist WITHOUT Booking (vacation, breaks, reminders)
- CalendarEvent CANNOT be deleted if it references a Booking
- Only type='customer' events have associated bookings

---

## Relationship: 1 Booking → N Events

**Key Innovation:** One booking can span MULTIPLE calendar events (split sessions OR split resources).

### Example 1: Split Session (Same Resource, Multiple Days)

**Scenario:** Bundfarve takes 180 minutes but is split across 2 days.

```json
// Booking (business contract)
{
  "id": "BOOK001",
  "customerId": "CUST456",
  "status": "created",
  "services": [
    {
      "serviceName": "Bundfarve komplet",
      "baseDuration": 180,
      "resourceId": "EMP001"  // Karina performs all
    }
  ],
  "totalPrice": 1500
}

// CalendarEvent 1 (first session)
{
  "id": "EVT001",
  "type": "customer",
  "title": "Anna - Bundfarve (Del 1)",
  "start": "2025-11-14T13:00:00",
  "end": "2025-11-14T15:00:00",     // 120 minutes
  "bookingId": "BOOK001",
  "resourceId": "EMP001",
  "customerId": "CUST456"
}

// CalendarEvent 2 (second session - next day)
{
  "id": "EVT002",
  "type": "customer",
  "title": "Anna - Bundfarve (Del 2)",
  "start": "2025-11-15T10:00:00",
  "end": "2025-11-15T11:00:00",     // 60 minutes
  "bookingId": "BOOK001",            // SAME booking!
  "resourceId": "EMP001",
  "customerId": "CUST456"
}
```

**Analytics:**
- Service duration: 180 min (what customer paid for)
- Actual time slots: 120 + 60 = 180 min (resource allocation)

---

### Example 2: Split Resources (Master + Student)

**Scenario:** Hårvask (30 min) by student, then Bundfarve (90 min) by master.

```json
// Booking (business contract)
{
  "id": "BOOK002",
  "customerId": "CUST456",
  "status": "created",
  "services": [
    {
      "serviceName": "Hårvask + bryn",
      "baseDuration": 30,
      "resourceId": "STUDENT001"  // Student performs this
    },
    {
      "serviceName": "Bundfarve",
      "baseDuration": 90,
      "resourceId": "EMP001"      // Master performs this
    }
  ],
  "totalPrice": 1160
}

// CalendarEvent 1 (student's time slot)
{
  "id": "EVT003",
  "type": "customer",
  "title": "Anna - Hårvask",
  "start": "2025-11-14T13:00:00",
  "end": "2025-11-14T13:30:00",
  "bookingId": "BOOK002",
  "resourceId": "STUDENT001",     // Student's calendar
  "customerId": "CUST456"
}

// CalendarEvent 2 (master's time slot)
{
  "id": "EVT004",
  "type": "customer",
  "title": "Anna - Bundfarve",
  "start": "2025-11-14T13:30:00",
  "end": "2025-11-14T15:00:00",
  "bookingId": "BOOK002",          // SAME booking!
  "resourceId": "EMP001",          // Master's calendar
  "customerId": "CUST456"
}
```

**Key Points:**
- One booking spans TWO resources
- Each resource gets their own CalendarEvent
- Events can be at different times (student starts, then master)
- Total time: student 30min + master 90min = 120min

---

### Example 3: Split Resources (Two Masters, Equal Split)

**Scenario:** Bundfarve komplet (3 hours) split equally between Karina and Nanna.

```json
// Booking (customer chose to split between two masters)
{
  "id": "BOOK003",
  "customerId": "CUST789",
  "status": "created",
  "services": [
    {
      "serviceName": "Bundfarve del 1",
      "baseDuration": 90,
      "resourceId": "EMP001"      // Karina
    },
    {
      "serviceName": "Bundfarve del 2",
      "baseDuration": 90,
      "resourceId": "EMP002"      // Nanna
    }
  ],
  "totalPrice": 2000
}

// CalendarEvent 1 (Karina's part)
{
  "id": "EVT005",
  "type": "customer",
  "start": "2025-11-14T10:00:00",
  "end": "2025-11-14T11:30:00",
  "bookingId": "BOOK003",
  "resourceId": "EMP001"          // Karina's calendar
}

// CalendarEvent 2 (Nanna's part)
{
  "id": "EVT006",
  "type": "customer",
  "start": "2025-11-14T11:30:00",
  "end": "2025-11-14T13:00:00",
  "bookingId": "BOOK003",
  "resourceId": "EMP002"          // Nanna's calendar
}
```

**Key Points:**
- No "primary" resource - both are equal
- Customer agreed to split during booking
- Each master gets 90 minutes
- Can be scheduled at different times if needed

---

### Example 4: Queued Booking (No Events Yet)

**Scenario:** Customer wants an appointment but no time slot is available yet.

```json
// Booking exists (in queue)
{
  "id": "BOOK004",
  "customerId": "CUST999",
  "status": "pending",
  "services": [
    {
      "serviceName": "Klipning",
      "baseDuration": 30,
      "resourceId": "EMP001"  // Pre-assigned to Karina
    }
  ]
}

// NO CalendarEvent yet!
// Query: SELECT * FROM CalendarEvent WHERE bookingId = 'BOOK004'
// Result: Empty

// When time is found, create event:
{
  "id": "EVT007",
  "type": "customer",
  "start": "2025-11-20T14:00:00",
  "end": "2025-11-20T14:30:00",
  "bookingId": "BOOK004",
  "resourceId": "EMP001"
}

// Update booking status:
UPDATE Booking SET status = 'scheduled' WHERE id = 'BOOK004'
```

---

### Example 5: Vacation (NO Booking)

**Scenario:** Karina is on vacation - NOT a booking.

```json
// NO Booking exists!

// CalendarEvent only
{
  "id": "EVT008",
  "type": "vacation",              // NOT 'customer'
  "title": "Karina - Juleferie",
  "start": "2025-12-23T00:00:00",
  "end": "2025-12-30T23:59:59",
  "allDay": true,
  "bookingId": null,               // NO booking reference
  "resourceId": "EMP001",          // Karina
  "customerId": null
}
```

**Key Points:**
- Vacation/break/meeting are CalendarEvents ONLY
- They do NOT have associated bookings
- `type` determines whether booking exists:
  - `type = 'customer'` → has booking
  - `type = 'vacation'/'break'/'meeting'` → no booking

---

### Example 6: Simple Reminder (NO Booking, NO Resource)

**Scenario:** Personal task not tied to any business transaction.

```json
// CalendarEvent (standalone)
{
  "id": "EVT009",
  "type": "meeting",               // Or custom type
  "title": "Husk at ringe til leverandør",
  "start": "2025-11-15T10:00:00",
  "end": "2025-11-15T10:15:00",
  "bookingId": null,               // NO booking
  "resourceId": null,              // NO resource
  "customerId": null
}
```

---

## Duration Calculation

### Service Bookings

**Duration = Sum of service durations**

```typescript
// Booking
{
  "services": [
    { "baseDuration": 90, "resourceId": "EMP001" },
    { "baseDuration": 30, "resourceId": "STUDENT001" }
  ]
}

// Initial event creation:
const totalServiceDuration = services.reduce((sum, s) => sum + s.baseDuration, 0);
// totalServiceDuration = 120 minutes

// Create events (one per resource or split session)
events = services.map(service => ({
  start: calculateStartTime(service),
  end: start + service.baseDuration,
  resourceId: service.resourceId
}));
```

**User Adjustment:**
User can drag/resize event.end in calendar (e.g., from 15:00 to 15:30)
- Services still show 120min (what was booked)
- Event shows 150min (actual time spent)
- This is CORRECT behavior (tracks overrun)

---

## Database Schema

### Backend (SQL - Normalized)

```sql
-- Booking table (NO timing, NO resourceId)
CREATE TABLE Booking (
    Id VARCHAR(50) PRIMARY KEY,
    CustomerId VARCHAR(50) NOT NULL,        -- FK to Customer (REQUIRED)
    Status VARCHAR(20) NOT NULL,
    TotalPrice DECIMAL(10,2) NULL,
    CreatedAt DATETIME NOT NULL,
    Tags NVARCHAR(500) NULL,
    Notes NVARCHAR(MAX) NULL,

    FOREIGN KEY (CustomerId) REFERENCES Customer(Id)
);

-- CalendarEvent table (HAS timing)
CREATE TABLE CalendarEvent (
    Id VARCHAR(50) PRIMARY KEY,
    Title NVARCHAR(200) NOT NULL,
    Description NVARCHAR(MAX) NULL,
    Start DATETIME NOT NULL,
    End DATETIME NOT NULL,
    AllDay BIT NOT NULL,
    Type VARCHAR(50) NOT NULL,              -- 'customer', 'vacation', 'break', etc.

    -- References (BookingId is nullable - only 'customer' type has booking)
    BookingId VARCHAR(50) NULL,

    -- Denormalized (for query performance)
    ResourceId VARCHAR(50) NULL,            -- Which resource owns this slot
    CustomerId VARCHAR(50) NULL,            -- Copied from Booking

    FOREIGN KEY (BookingId) REFERENCES Booking(Id)
);

-- BookingService junction table (WITH ResourceId)
CREATE TABLE BookingService (
    BookingId VARCHAR(50) NOT NULL,
    ServiceId VARCHAR(50) NOT NULL,
    ResourceId VARCHAR(50) NOT NULL,        -- NEW: Resource performing THIS service
    CustomPrice DECIMAL(10,2) NULL,
    SortOrder INT NULL,

    PRIMARY KEY (BookingId, ServiceId),
    FOREIGN KEY (BookingId) REFERENCES Booking(Id),
    FOREIGN KEY (ServiceId) REFERENCES Service(Id),
    FOREIGN KEY (ResourceId) REFERENCES Resource(Id)
);
```

**Key Changes from Old Schema:**
- ❌ Booking table NO LONGER has ResourceId
- ❌ Booking table NO LONGER has Type
- ✅ BookingService table NOW has ResourceId (NEW!)
- ✅ CalendarEvent.Type distinguishes customer vs vacation/break

**Why ResourceId/CustomerId on CalendarEvent?**
- Performance: Filter events by resource WITHOUT joining Booking/BookingService
- IndexedDB: Frontend needs these for fast queries (no JOIN support)

---

### Frontend (IndexedDB - Denormalized)

```typescript
// IndexedDB stores
const db = await openDB('CalendarDB', 1, {
  upgrade(db) {
    // Events store (MASTER for queries)
    const eventStore = db.createObjectStore('events', { keyPath: 'id' });
    eventStore.createIndex('by_start', 'start');
    eventStore.createIndex('by_type', 'type');
    eventStore.createIndex('by_resourceId', 'resourceId');   // Denormalized!
    eventStore.createIndex('by_customerId', 'customerId');   // Denormalized!
    eventStore.createIndex('by_bookingId', 'bookingId');

    // Bookings store (REFERENCE data, loaded on-demand)
    const bookingStore = db.createObjectStore('bookings', { keyPath: 'id' });
    bookingStore.createIndex('by_customerId', 'customerId');
    bookingStore.createIndex('by_status', 'status');
  }
});
```

**Query Pattern:**

```typescript
// Get events for resource (NO JOIN needed)
async getEventsForResource(resourceId: string, start: Date, end: Date) {
  const events = await db.getAllFromIndex('events', 'by_resourceId', resourceId);
  return events.filter(e => e.start >= start && e.start <= end);
}

// Get customer bookings (via events)
async getCustomerBookingsInPeriod(customerId: string, start: Date, end: Date) {
  // 1. Get customer events
  const events = await db.events
    .where('customerId').equals(customerId)
    .filter(e => e.start >= start && e.start <= end && e.type === 'customer')
    .toArray();

  // 2. Get unique booking IDs
  const bookingIds = [...new Set(events.map(e => e.bookingId).filter(Boolean))];

  // 3. Load bookings
  const bookings = await Promise.all(
    bookingIds.map(id => db.get('bookings', id))
  );

  return bookings;
}

// Get booking with all resources involved
async getBookingResources(bookingId: string) {
  const booking = await db.get('bookings', bookingId);

  // Extract unique resources from services
  const resourceIds = [...new Set(booking.services.map(s => s.resourceId))];

  return resourceIds;
}
```

---

## API Data Flow

### Backend Normalization → Frontend Denormalization

**Backend API Response (Denormalized via JOIN):**

```http
GET /api/events?start=2025-11-14&end=2025-11-21
```

```json
[
  {
    "id": "EVT001",
    "type": "customer",
    "title": "Anna - Bundfarve",
    "start": "2025-11-14T13:00:00",
    "end": "2025-11-14T15:00:00",
    "allDay": false,
    "bookingId": "BOOK001",
    "resourceId": "EMP001",      // ← Backend determines from event context
    "customerId": "CUST456"      // ← Backend JOINed from Booking
  },
  {
    "id": "EVT008",
    "type": "vacation",
    "title": "Karina - Juleferie",
    "start": "2025-12-23T00:00:00",
    "end": "2025-12-30T23:59:59",
    "allDay": true,
    "bookingId": null,           // ← No booking for vacation
    "resourceId": "EMP001",
    "customerId": null
  }
]
```

**Backend SQL (with JOIN):**

```sql
SELECT
  e.Id,
  e.Type,
  e.Title,
  e.Start,
  e.End,
  e.AllDay,
  e.BookingId,
  e.ResourceId,              -- Already on CalendarEvent (denormalized)
  b.CustomerId               -- Joined from Booking (if exists)
FROM CalendarEvent e
LEFT JOIN Booking b ON e.BookingId = b.Id
WHERE e.Start >= @start AND e.Start <= @end
```

---

## Business Rules

### Rule 1: Booking Requires Customer (Always)

```typescript
// ✅ VALID - Booking always has customer + services
{
  "customerId": "CUST456",
  "services": [
    { "resourceId": "EMP001", "serviceName": "Klipning" }
  ]
}

// ❌ INVALID - Booking without customer
{
  "customerId": null,          // ERROR: Booking requires customer
  "services": [...]
}

// ❌ INVALID - Booking without services
{
  "customerId": "CUST456",
  "services": []               // ERROR: Booking requires services
}
```

---

### Rule 2: Event Cannot Be Deleted If It Has Booking

```typescript
async deleteEvent(eventId: string) {
  const event = await db.get('events', eventId);

  if (event.bookingId) {
    throw new Error("Cannot delete event with booking. Cancel booking first.");
  }

  await db.delete('events', eventId);  // OK for vacation/reminders
}
```

---

### Rule 3: Booking Can Be Cancelled (Cascades to Events)

```typescript
async cancelBooking(bookingId: string) {
  // 1. Update booking status
  await db.bookings.update(bookingId, { status: 'cancelled' });

  // 2. Find ALL related events (may be multiple resources/sessions)
  const events = await db.events.where('bookingId').equals(bookingId).toArray();

  // 3. Delete OR mark cancelled
  for (const event of events) {
    await db.events.delete(event.id);  // Remove from calendar
  }
}
```

---

### Rule 4: Moving Event Updates Only Event (Not Booking)

```typescript
// User drags event from 13:00 to 14:00
async moveEvent(eventId: string, newStart: Date, newEnd: Date) {
  // Update ONLY the event
  await db.events.update(eventId, {
    start: newStart,
    end: newEnd
  });

  // Booking services/price remain unchanged
  // (This is correct - tracks time adjustments vs booked services)
}
```

---

### Rule 5: Service Resource Can Be Changed (Elev Syg)

```typescript
// Elev is sick - reassign to another student
async reassignServiceResource(bookingId: string, serviceId: string, newResourceId: string) {
  // 1. Update booking service
  const booking = await db.get('bookings', bookingId);
  const service = booking.services.find(s => s.serviceId === serviceId);
  service.resourceId = newResourceId;
  await db.put('bookings', booking);

  // 2. Find and update related event
  const events = await db.events.where('bookingId').equals(bookingId).toArray();
  const eventToUpdate = events.find(e => e.resourceId === oldResourceId);

  if (eventToUpdate) {
    eventToUpdate.resourceId = newResourceId;
    await db.put('events', eventToUpdate);
  }
}
```

---

## Analytics & Reporting

### Service Duration vs Actual Duration

```typescript
// Get booking with actual time spent
async getBookingAnalytics(bookingId: string) {
  const booking = await db.get('bookings', bookingId);
  const events = await db.events.where('bookingId').equals(bookingId).toArray();

  // Booked duration (from services)
  const bookedDuration = booking.services
    .reduce((sum, s) => sum + s.baseDuration, 0);

  // Actual duration (from events)
  const actualDuration = events
    .reduce((sum, e) => sum + (e.end - e.start) / 60000, 0);

  return {
    bookedMinutes: bookedDuration,     // 120 min (what customer paid for)
    actualMinutes: actualDuration,     // 150 min (real time spent)
    overrun: actualDuration - bookedDuration  // +30 min
  };
}
```

---

### Resource Utilization

```typescript
// How much time did Karina work in November?
async getResourceUtilization(resourceId: string, month: Date) {
  const start = startOfMonth(month);
  const end = endOfMonth(month);

  // Query events (NOT bookings - because events have timing)
  const events = await db.events
    .where('resourceId').equals(resourceId)
    .filter(e => e.start >= start && e.start <= end)
    .toArray();

  const totalMinutes = events
    .reduce((sum, e) => sum + (e.end - e.start) / 60000, 0);

  return {
    resourceId,
    month: format(month, 'yyyy-MM'),
    totalHours: totalMinutes / 60,
    eventCount: events.length,
    breakdown: {
      customer: events.filter(e => e.type === 'customer').length,
      vacation: events.filter(e => e.type === 'vacation').length,
      break: events.filter(e => e.type === 'break').length
    }
  };
}
```

---

### Find Bookings Involving Resource

```typescript
// Find all bookings where Karina is involved (even if not all services)
async getBookingsForResource(resourceId: string, start: Date, end: Date) {
  // 1. Get events for resource in period
  const events = await db.events
    .where('resourceId').equals(resourceId)
    .filter(e => e.start >= start && e.start <= end && e.type === 'customer')
    .toArray();

  // 2. Get unique booking IDs
  const bookingIds = [...new Set(events.map(e => e.bookingId).filter(Boolean))];

  // 3. Load bookings
  const bookings = await Promise.all(
    bookingIds.map(id => db.get('bookings', id))
  );

  return bookings;
}
```

---

## TypeScript Interfaces (Reference)

```typescript
// src/types/BookingTypes.ts
export interface IBooking {
  id: string;
  customerId: string;        // REQUIRED - always has customer
  status: BookingStatus;
  createdAt: Date;

  services: IBookingService[];  // REQUIRED - always has services
  totalPrice?: number;
  tags?: string[];
  notes?: string;

  // NO resourceId - assigned per service
  // NO type - always customer
  // NO start/end - timing on CalendarEvent
}

export interface IBookingService {
  serviceId: string;
  serviceName: string;
  baseDuration: number;
  basePrice: number;
  customPrice?: number;
  resourceId: string;        // Resource who performs THIS service
}

export type CalendarEventType =
  | 'customer'    // Customer appointment (HAS booking)
  | 'vacation'    // Vacation/time off (NO booking)
  | 'break'       // Lunch/break (NO booking)
  | 'meeting'     // Meeting (NO booking)
  | 'blocked';    // Blocked time (NO booking)

export type BookingStatus =
  | 'created'     // AftaleOprettet
  | 'arrived'     // Ankommet
  | 'paid'        // Betalt
  | 'noshow'      // Udeblevet
  | 'cancelled';  // Aflyst

// src/types/CalendarTypes.ts
export interface ICalendarEvent {
  id: string;
  title: string;
  description?: string;

  start: Date;               // MASTER for timing
  end: Date;                 // MASTER for timing
  allDay: boolean;

  type: CalendarEventType;   // Determines if booking exists

  // References (denormalized)
  bookingId?: string;        // Only if type = 'customer'
  resourceId?: string;       // Which resource owns this slot
  customerId?: string;       // Denormalized from Booking

  syncStatus: SyncStatus;
  recurringId?: string;
  metadata?: Record<string, any>;
}
```

---

## Common Pitfalls

### ❌ Pitfall 1: Trying to Query Bookings by Time

```typescript
// WRONG - Bookings don't have start/end
const bookings = await db.bookings
  .where('start').between(startDate, endDate)  // ERROR: No 'start' field!
  .toArray();

// CORRECT - Query events, then load bookings
const events = await db.events
  .where('start').between(startDate, endDate)
  .filter(e => e.type === 'customer')  // Only customer events have bookings
  .toArray();

const bookingIds = events
  .filter(e => e.bookingId)
  .map(e => e.bookingId);

const bookings = await Promise.all(
  bookingIds.map(id => db.get('bookings', id))
);
```

---

### ❌ Pitfall 2: Assuming 1:1 Relationship

```typescript
// WRONG - Assumes one event per booking
const booking = await db.get('bookings', bookingId);
const event = await db.events
  .where('bookingId').equals(bookingId)
  .first();  // Only gets first event!

// CORRECT - Get ALL events for booking (split sessions/resources)
const events = await db.events
  .where('bookingId').equals(bookingId)
  .toArray();  // May return multiple events
```

---

### ❌ Pitfall 3: Looking for resourceId on Booking

```typescript
// WRONG - Booking doesn't have resourceId
const booking = await db.get('bookings', bookingId);
console.log(booking.resourceId);  // ERROR: undefined!

// CORRECT - Get resources from services
const resourceIds = [...new Set(booking.services.map(s => s.resourceId))];
console.log(resourceIds);  // ["EMP001", "STUDENT001"]
```

---

### ❌ Pitfall 4: Trying to Create Vacation as Booking

```typescript
// WRONG - Vacation is NOT a booking
const vacationBooking = {
  customerId: null,
  type: 'vacation',
  services: []
};
await db.add('bookings', vacationBooking);  // ERROR: Invalid!

// CORRECT - Vacation is CalendarEvent only
const vacationEvent = {
  type: 'vacation',
  start: new Date('2025-12-23'),
  end: new Date('2025-12-30'),
  bookingId: null,          // NO booking
  resourceId: 'EMP001'
};
await db.add('events', vacationEvent);  // OK
```

---

## Summary

| Aspect | Booking | CalendarEvent |
|--------|---------|---------------|
| **Purpose** | Customer + Services | Time slot + Resource |
| **Timing** | No start/end | Has start/end |
| **Resource** | Per service | Per event (denormalized) |
| **Type** | Always customer | customer/vacation/break/meeting |
| **Relationship** | 1 booking → N events | N events → 0..1 booking |
| **Can exist alone?** | Yes (queued) | Yes (vacation/reminders) |
| **Query by time?** | ❌ No | ✅ Yes |
| **Query by resource?** | ❌ No (via services) | ✅ Yes (direct) |
| **Master for...** | Services, price, customer | Timing, scheduling, resource |

**Key Takeaways:**
- Use **CalendarEvent** for all scheduling queries (when, who)
- Use **Booking** for business data (what, how much)
- Only `type='customer'` events have bookings
- Vacation/break/meeting are events without bookings
- Resources are assigned at **service level**, not booking level
- One booking can involve **multiple resources** (split services)

---

## Decision Guide

If you're unsure whether something belongs in Booking or CalendarEvent, ask:

1. **"Is this about WHEN it happens?"** → CalendarEvent
2. **"Is this about WHICH RESOURCE does it?"** → CalendarEvent
3. **"Is this about WHAT/WHO/HOW MUCH?"** → Booking
4. **"Can this change when the event is moved in calendar?"**
   - If YES → CalendarEvent
   - If NO → Booking
5. **"Is this vacation/break/meeting?"** → CalendarEvent ONLY (no booking)

---

**Document End**
