# BookingService - Service Documentation

> Auto-generated from source code audit on 2026-03-13

## Overview

BookingService is a microservice for managing appointment bookings, resource allocation, staff schedules, and spa/beauty therapists. It supports multi-vertical booking workflows (Spa, Karaoke, Restaurant, etc.) with slot-finding algorithms based on staff availability and resource capacity.

- **Framework**: .NET 10.0 (C# 14), ASP.NET Core Web API
- **Architecture**: Clean Architecture + CQRS (MediatR 12.4.1)
- **Database**: PostgreSQL (Neon cloud) via EF Core 10 + Npgsql 10
- **Port**: 5020 (Development)
- **Database Name**: `booking_service`
- **Health Checks**: `/health`, `/health/live`, `/health/ready`
- **Auth**: JWT Bearer via IAM IdentityServer OIDC discovery (default authority: `http://localhost:5001`)
- **API Response Format**: `{ success: bool, data: T, message?: string, errors?: string[] }`

## API Endpoints

### Appointments (`/api/v1/appointments`)

| Method | Path | Auth | Description | Request | Response |
|--------|------|------|-------------|---------|----------|
| GET | `/api/v1/appointments?shopId=&customerId=&startDate=&endDate=&status=&page=&pageSize=` | No | Get appointments by shop or customer with filtering/pagination | Query params | `ApiResponse<PaginatedList<AppointmentDto>>` |
| GET | `/api/v1/appointments/{id}` | No | Get appointment by ID | - | `ApiResponse<AppointmentDto>` |
| POST | `/api/v1/appointments` | No | Create a new appointment | `CreateAppointmentRequest` | `ApiResponse<AppointmentDto>` (201) |
| PATCH | `/api/v1/appointments/{id}/status` | No | Update appointment status (confirm/start/complete/noshow) | `UpdateStatusRequest` | `ApiResponse<AppointmentDto>` |
| PATCH | `/api/v1/appointments/{id}/noshow` | No | Mark appointment as no-show | - | `ApiResponse<AppointmentDto>` |
| DELETE | `/api/v1/appointments/{id}` | No | Cancel appointment | `CancelRequest` | `ApiResponse<bool>` |

### Resources (`/api/v1/resources`)

| Method | Path | Auth | Description | Request | Response |
|--------|------|------|-------------|---------|----------|
| GET | `/api/v1/resources?shopId=&isActive=` | No | Get resources by shop | Query params | `ApiResponse<List<ResourceDto>>` |
| POST | `/api/v1/resources` | No | Create a new resource | `CreateResourceRequest` | `ApiResponse<ResourceDto>` (201) |
| PUT | `/api/v1/resources/{id}` | No | Update a resource | `UpdateResourceRequest` | `ApiResponse<ResourceDto>` |

### Slots (`/api/v1/slots`)

| Method | Path | Auth | Description | Request | Response |
|--------|------|------|-------------|---------|----------|
| POST | `/api/v1/slots/find` | No | Find available time slots | `FindSlotsRequest` | `ApiResponse<List<TimeSlotDto>>` |

### Staff Schedules (`/api/v1/staff/{staffId}/schedule`)

| Method | Path | Auth | Description | Request | Response |
|--------|------|------|-------------|---------|----------|
| GET | `/api/v1/staff/{staffId}/schedule?shopId=` | No | Get staff schedule | Query params | `ApiResponse<List<StaffScheduleDto>>` |
| PUT | `/api/v1/staff/{staffId}/schedule` | No | Update staff schedule (replace all) | `UpdateScheduleRequest` | `ApiResponse<List<StaffScheduleDto>>` |

### Schedules (`/api/v1/schedules`)

| Method | Path | Auth | Description | Request | Response |
|--------|------|------|-------------|---------|----------|
| GET | `/api/v1/schedules?shopId=` | No | Get all staff schedules for a shop | Query params | `ApiResponse<List<StaffScheduleDto>>` |
| POST | `/api/v1/schedules` | No | Create a single schedule entry | `CreateScheduleRequest` | `ApiResponse<StaffScheduleDto>` (201) |
| DELETE | `/api/v1/schedules/{id}` | No | Delete a schedule entry | - | `{ success, message }` |

### Therapists (`/api/v1/therapists`)

| Method | Path | Auth | Description | Request | Response |
|--------|------|------|-------------|---------|----------|
| GET | `/api/v1/therapists?shopId=&isActive=` | No | Get therapists by shop | Query params | `ApiResponse<List<TherapistDto>>` |
| POST | `/api/v1/therapists` | No | Create a new therapist | `CreateTherapistRequest` | `ApiResponse<TherapistDto>` (201) |
| PUT | `/api/v1/therapists/{id}` | No | Update a therapist | `UpdateTherapistRequest` | `ApiResponse<TherapistDto>` |
| DELETE | `/api/v1/therapists/{id}` | No | Deactivate therapist (soft delete) | - | `ApiResponse<bool>` |

### Admin Appointments (`/api/v1/admin/appointments`) - **Requires Auth: Admin, ShopOwner**

| Method | Path | Auth | Description | Request | Response |
|--------|------|------|-------------|---------|----------|
| GET | `/api/v1/admin/appointments?shopId=&startDate=&endDate=&status=&page=&pageSize=` | Admin/ShopOwner | Get all appointments with advanced filtering | Query params | `ApiResponse<PaginatedList<AppointmentDto>>` |
| GET | `/api/v1/admin/appointments/statistics?shopId=&startDate=&endDate=` | Admin/ShopOwner | Get appointment statistics | Query params | `ApiResponse<AppointmentStatisticsDto>` |

### Admin Resources (`/api/v1/admin/resources`) - **Requires Auth: Admin, ShopOwner**

| Method | Path | Auth | Description | Request | Response |
|--------|------|------|-------------|---------|----------|
| GET | `/api/v1/admin/resources/{shopId}` | Admin/ShopOwner | Get all resources including inactive | Path param | `ApiResponse<List<ResourceDto>>` |

---

## Commands

### CreateAppointmentCommand
- **Input**: `ShopId (Guid)`, `ServiceId (Guid)`, `StartTime (DateTime)`, `EndTime (DateTime)`, `CustomerId? (Guid)`, `StaffId? (Guid)`, `ResourceId? (Guid)`, `Notes? (string)`
- **Logic**: Creates Appointment domain entity, raises `AppointmentCreatedDomainEvent`, saves via repository
- **Validator**: `CreateAppointmentCommandValidator`
  - ShopId: NotEmpty
  - ServiceId: NotEmpty
  - StartTime: NotEmpty, must be in future (with 5min tolerance)
  - EndTime: NotEmpty, must be after StartTime
  - Notes: MaxLength(1000)

### UpdateAppointmentStatusCommand
- **Input**: `AppointmentId (Guid)`, `Action (string)` - one of: "confirm", "start", "complete", "noshow"
- **Logic**: Loads appointment, executes domain action via switch (Confirm/MarkAsInProgress/Complete/MarkNoShow), saves
- **Validator**: `UpdateAppointmentStatusCommandValidator`
  - AppointmentId: NotEmpty
  - Action: NotEmpty, must be one of valid actions

### CancelAppointmentCommand
- **Input**: `AppointmentId (Guid)`, `Reason (string)`
- **Logic**: Loads appointment, calls `Cancel(reason)`, raises `AppointmentCancelledDomainEvent`
- **Validator**: `CancelAppointmentCommandValidator`
  - AppointmentId: NotEmpty
  - Reason: NotEmpty, MaxLength(500)

### MarkNoShowCommand
- **Input**: `AppointmentId (Guid)`
- **Logic**: Loads appointment, calls `MarkNoShow()`, saves
- **Validator**: `MarkNoShowCommandValidator`
  - AppointmentId: NotEmpty

### CreateResourceCommand
- **Input**: `ShopId (Guid)`, `Name (string)`, `ResourceType (string)`, `Capacity (int, default 1)`
- **Logic**: Creates Resource entity, saves via repository
- **Validator**: `CreateResourceCommandValidator`
  - ShopId: NotEmpty
  - Name: NotEmpty, MaxLength(200)
  - ResourceType: NotEmpty, MaxLength(50)
  - Capacity: GreaterThan(0)

### UpdateResourceCommand
- **Input**: `ResourceId (Guid)`, `Name (string)`, `Capacity (int)`, `IsActive (bool)`
- **Logic**: Loads resource, toggles Activate/Deactivate, saves
- **Note**: Name and Capacity are accepted but NOT actually applied to the entity (only IsActive is used in handler). This is a potential bug.

### UpdateStaffScheduleCommand
- **Input**: `StaffId (Guid)`, `ShopId (Guid)`, `Schedule (List<ScheduleDay>)`
- **ScheduleDay**: `DayOfWeek (int, 0=Sunday..6=Saturday)`, `StartTime (TimeOnly)`, `EndTime (TimeOnly)`
- **Logic**: Deletes all existing schedules for staff+shop, creates new ones from input
- **Validator**: None

### CreateTherapistCommand
- **Input**: `ShopId (Guid)`, `Name (string)`, `Specialties (string[])`, `WorkingHours (WorkingHoursDto)`
- **Logic**: Serializes WorkingHours to JSON, creates Therapist entity, raises `TherapistCreatedDomainEvent`
- **Validator**: `CreateTherapistCommandValidator`
  - ShopId: NotEmpty
  - Name: NotEmpty, MaxLength(200)
  - WorkingHours: NotNull
  - WorkingHours.Days: NotEmpty
  - Each Day: DayOfWeek 0-6, StartTime/EndTime required when IsWorking

### UpdateTherapistCommand
- **Input**: `TherapistId (Guid)`, `Name (string)`, `Specialties (string[])`, `WorkingHours (WorkingHoursDto)`
- **Logic**: Loads therapist, serializes WorkingHours to JSON, calls `Update(name, specialties, workingHoursJson)`, raises `TherapistUpdatedDomainEvent`
- **Validator**: `UpdateTherapistCommandValidator` (same rules as Create)

### DeactivateTherapistCommand
- **Input**: `TherapistId (Guid)`
- **Logic**: Loads therapist, calls `Deactivate()` (soft delete), saves
- **Validator**: `DeactivateTherapistCommandValidator`
  - TherapistId: NotEmpty

---

## Queries

### GetAppointmentQuery
- **Input**: `AppointmentId (Guid)`
- **Logic**: Loads from repository by ID, returns null if not found

### GetAppointmentsByShopQuery
- **Input**: `ShopId (Guid)`, `StartDate? (DateTime)`, `EndDate? (DateTime)`, `Status? (string)`, `Page (int, default 1)`, `PageSize (int, default 20)`
- **Logic**: Filters by ShopId, optional date range and status, ordered by StartTime DESC, paginated

### GetAppointmentsByCustomerQuery
- **Input**: `CustomerId (Guid)`, `Page (int, default 1)`, `PageSize (int, default 20)`
- **Logic**: Filters by CustomerId, ordered by StartTime DESC, paginated

### FindAvailableSlotsQuery
- **Input**: `ShopId (Guid)`, `ServiceId (Guid)`, `Date (DateTime)`, `ServiceDurationMinutes (int)`, `StaffId? (Guid)`, `ResourceId? (Guid)`
- **Logic**: Algorithm: `Available Slots = Staff Schedule intersection Resource Availability - Booked Appointments`
  1. Gets staff schedules for the day (filtered by specific staff if provided)
  2. Gets existing non-cancelled appointments for the date
  3. For each staff schedule, generates 15-minute interval slots
  4. Excludes slots that conflict with existing appointments (overlap check)
  5. Returns sorted by StartTime
- **Validator**: `FindAvailableSlotsQueryValidator`
  - ShopId: NotEmpty
  - ServiceId: NotEmpty
  - Date: NotEmpty
  - ServiceDurationMinutes: GreaterThan(0), LessThanOrEqualTo(480)

### GetResourcesByShopQuery
- **Input**: `ShopId (Guid)`, `IsActive? (bool)`
- **Logic**: Filters resources by ShopId, optional active filter

### GetStaffScheduleQuery
- **Input**: `StaffId (Guid)`, `ShopId (Guid)`
- **Logic**: Gets schedules for specific staff in a shop

### GetSchedulesByShopQuery
- **Input**: `ShopId (Guid)`
- **Logic**: Gets all staff schedules for a shop, ordered by StaffId then DayOfWeek

### GetTherapistsQuery
- **Input**: `ShopId (Guid)`, `IsActive? (bool)`
- **Logic**: Gets therapists for a shop, optional active filter, ordered by Name

---

## Domain Model

### Appointment (Aggregate Root)

**Fields**:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| Id | Guid | Yes | Auto-generated UUID |
| ShopId | Guid | Yes | Shop this appointment belongs to |
| CustomerId | Guid? | No | Customer who booked |
| StaffId | Guid? | No | Assigned staff/therapist |
| ResourceId | Guid? | No | Assigned resource (room/bed) |
| ServiceId | Guid | Yes | Service being booked |
| StartTime | DateTime | Yes | Appointment start |
| EndTime | DateTime | Yes | Must be after StartTime |
| Status | string | Yes | Pending/Confirmed/InProgress/Completed/Cancelled/NoShow |
| Notes | string? | No | Max 1000 chars |
| CreatedAt | DateTime | Yes | UTC timestamp |

**Behavior Methods**:
- `Confirm()` - Pending -> Confirmed, raises `AppointmentConfirmedDomainEvent`
- `MarkAsInProgress()` - Confirmed -> InProgress
- `Complete()` - InProgress -> Completed, raises `AppointmentCompletedDomainEvent`
- `Cancel(reason)` - Any (except Completed/Cancelled) -> Cancelled, raises `AppointmentCancelledDomainEvent`
- `MarkNoShow()` - Pending/Confirmed -> NoShow

**State Machine**:
```
Pending --> Confirmed --> InProgress --> Completed
  |            |
  |            v
  +--------> NoShow
  |
  v
Cancelled (from any state except Completed)
```

**Domain Events**:
- `AppointmentCreatedDomainEvent(Appointment)` - on creation
- `AppointmentConfirmedDomainEvent(Appointment)` - on confirm
- `AppointmentCompletedDomainEvent(Appointment)` - on complete
- `AppointmentCancelledDomainEvent(Appointment, Reason)` - on cancel

### AppointmentStatus (Enumeration)

Type-safe enum: Pending(1), Confirmed(2), InProgress(3), Completed(4), Cancelled(5), NoShow(6)

**Note**: The Appointment entity uses string-based status internally, not the Enumeration. The AppointmentStatus Enumeration is only used as a seed data reference table.

### Resource (Aggregate Root)

**Fields**:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| Id | Guid | Yes | Auto-generated UUID |
| ShopId | Guid | Yes | Shop this resource belongs to |
| Name | string | Yes | Max 200 chars |
| ResourceType | string | Yes | Room/Bed/Equipment, max 50 chars |
| Capacity | int | Yes | Default 1 |
| IsActive | bool | Yes | Default true |
| CreatedAt | DateTime | Yes | UTC timestamp |

**Behavior Methods**:
- `Activate()` - Sets IsActive = true
- `Deactivate()` - Sets IsActive = false

### StaffSchedule (Entity, NOT Aggregate Root)

**Fields**:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| Id | Guid | Yes | Auto-generated UUID |
| StaffId | Guid | Yes | Staff member ID |
| ShopId | Guid | Yes | Shop ID |
| DayOfWeek | int | Yes | 0=Sunday, 6=Saturday |
| StartTime | TimeOnly | Yes | Work start time |
| EndTime | TimeOnly | Yes | Work end time |

### Therapist (Aggregate Root)

**Fields**:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| Id | Guid | Yes | Auto-generated UUID |
| ShopId | Guid | Yes | Shop this therapist belongs to |
| Name | string | Yes | Max 200 chars |
| Specialties | string[] | Yes | PostgreSQL text[] (e.g., massage, facial, nails) |
| WorkingHours | string | Yes | JSONB weekly schedule |
| IsActive | bool | Yes | Default true |
| CreatedAt | DateTime | Yes | UTC timestamp |
| UpdatedAt | DateTime? | No | Last update timestamp |

**Behavior Methods**:
- `Activate()` - Sets IsActive = true, raises error if already active
- `Deactivate()` - Sets IsActive = false, raises error if already inactive
- `UpdateSpecialties(specialties)` - Updates specialties, raises `TherapistUpdatedDomainEvent`
- `UpdateWorkingHours(workingHours)` - Updates working hours JSON, raises `TherapistUpdatedDomainEvent`
- `UpdateName(name)` - Updates name, raises `TherapistUpdatedDomainEvent`
- `Update(name, specialties, workingHours)` - Bulk update, raises `TherapistUpdatedDomainEvent`

**Domain Events**:
- `TherapistCreatedDomainEvent(Therapist)` - on creation
- `TherapistUpdatedDomainEvent(Therapist)` - on any update

---

## Database Schema

**Database**: `booking_service` (PostgreSQL)

### Table: `appointments`
| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| id | uuid | NOT NULL | PK, ValueGeneratedNever |
| shop_id | uuid | NOT NULL | Shop FK |
| customer_id | uuid | NULL | Customer FK |
| staff_id | uuid | NULL | Staff FK |
| resource_id | uuid | NULL | Resource FK |
| service_id | uuid | NOT NULL | Service FK |
| start_time | timestamp with time zone | NOT NULL | |
| end_time | timestamp with time zone | NOT NULL | |
| status | varchar(50) | NOT NULL | |
| notes | varchar(1000) | NULL | Added in PhaseTwo migration |
| created_at | timestamp with time zone | NOT NULL | |

**Indexes**:
- `ix_appointments_shop_id` on (shop_id)
- `ix_appointments_customer_id` on (customer_id)
- `ix_appointments_staff_id` on (staff_id)
- `ix_appointments_start_time` on (start_time)

### Table: `appointment_statuses` (Reference/Seed)
| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| id | integer | NOT NULL | PK, ValueGeneratedNever |
| name | varchar(50) | NOT NULL | Status name |

**Seed Data**: Pending(1), Confirmed(2), InProgress(3), Completed(4), Cancelled(5), NoShow(6)

### Table: `resources`
| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| id | uuid | NOT NULL | PK, ValueGeneratedNever |
| shop_id | uuid | NOT NULL | Shop FK |
| name | varchar(200) | NOT NULL | |
| resource_type | varchar(50) | NOT NULL | Room/Bed/Equipment |
| capacity | integer | NOT NULL | |
| is_active | boolean | NOT NULL | |
| created_at | timestamp with time zone | NOT NULL | |

**Indexes**:
- `ix_resources_shop_id` on (shop_id)

### Table: `staff_schedules`
| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| id | uuid | NOT NULL | PK, ValueGeneratedNever |
| staff_id | uuid | NOT NULL | Staff member FK |
| shop_id | uuid | NOT NULL | Shop FK |
| day_of_week | integer | NOT NULL | 0=Sunday..6=Saturday |
| start_time | time without time zone | NOT NULL | |
| end_time | time without time zone | NOT NULL | |

**Indexes**:
- `ix_staff_schedules_staff_day` on (staff_id, day_of_week)
- `ix_staff_schedules_shop_id` on (shop_id)

### Table: `therapists`
| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| id | uuid | NOT NULL | PK, ValueGeneratedNever |
| shop_id | uuid | NOT NULL | Shop FK |
| name | varchar(200) | NOT NULL | |
| specialties | text[] | NOT NULL | PostgreSQL array |
| working_hours | jsonb | NOT NULL | Weekly schedule JSON |
| is_active | boolean | NOT NULL | |
| created_at | timestamp with time zone | NOT NULL | |
| updated_at | timestamp with time zone | NULL | |

**Indexes**:
- `ix_therapists_shop_id` on (shop_id)
- `ix_therapists_shop_active` on (shop_id, is_active)

### Migrations
1. `20260117181734_InitialCreate` - appointments, appointment_statuses (seeded), resources, staff_schedules + all indexes
2. `20260306175525_PhaseTwo` - Added `notes` column to appointments, added `therapists` table + indexes

---

## Dependencies

### NuGet Packages (API Layer)
| Package | Version |
|---------|---------|
| MediatR | 12.4.1 |
| FluentValidation | 11.11.0 |
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
| Microsoft.EntityFrameworkCore.Design | 10.0.0 |
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.1 |
| Swashbuckle.AspNetCore | 7.2.0 |
| AspNetCore.HealthChecks.NpgSql | 8.0.2 |
| AspNetCore.HealthChecks.Redis | 8.0.1 |
| Hellang.Middleware.ProblemDetails | 6.5.1 |
| Serilog.AspNetCore | 8.0.3 |
| Serilog.Sinks.Console | 6.0.0 |
| Serilog.Sinks.Seq | 8.0.0 |

### NuGet Packages (Domain Layer)
| Package | Version |
|---------|---------|
| MediatR.Contracts | 2.0.1 |

### NuGet Packages (Infrastructure Layer)
| Package | Version |
|---------|---------|
| Microsoft.EntityFrameworkCore | 10.0.0 |
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.0 |
| Microsoft.EntityFrameworkCore.Tools | 10.0.0 |
| MediatR | 12.4.1 |
| Dapper | 2.1.35 |
| Microsoft.Extensions.Http.Polly | 9.0.0 |
| Polly | 8.5.0 |
| StackExchange.Redis | 2.8.16 |

### NuGet Packages (Functional Tests)
| Package | Version |
|---------|---------|
| Microsoft.NET.Test.Sdk | 17.12.0 |
| xunit | 2.9.2 |
| Microsoft.AspNetCore.Mvc.Testing | 10.0.0 |
| Microsoft.EntityFrameworkCore.InMemory | 10.0.0 |
| FluentAssertions | 6.12.2 |
| Testcontainers.PostgreSql | 4.1.0 |
| coverlet.collector | 6.0.2 |

### Service Dependencies
- **IAM Service** (port 5001): JWT token validation via OIDC discovery
- **PostgreSQL**: `booking_service` database (Neon cloud for staging/prod, local for dev)
- **Redis**: Configured but not actively used in current code (connection string in appsettings)

---

## Configuration

### appsettings.json
```json
{
  "ConnectionStrings": {
    "DefaultConnection": "<Neon PostgreSQL connection string>"
  },
  "Redis": {
    "ConnectionString": "localhost:6379"
  },
  "Jwt": {
    "Secret": "...",
    "Issuer": "goodgo-platform",
    "Audience": "goodgo-services",
    "AccessTokenExpiryMinutes": 15,
    "RefreshTokenExpiryDays": 7
  },
  "Serilog": { ... }
}
```

### Environment Variables
- `ASPNETCORE_ENVIRONMENT` - Development/Staging/Production
- `DATABASE_URL` - Fallback connection string
- `Jwt:Authority` - IAM IdentityServer URL (default: `http://localhost:5001`)

### MediatR Pipeline
```
Request --> LoggingBehavior --> ValidatorBehavior --> TransactionBehavior --> Handler
```
- **LoggingBehavior**: Logs request name + elapsed time (Stopwatch)
- **ValidatorBehavior**: Runs FluentValidation, throws `ValidationException` on failure
- **TransactionBehavior**: Wraps Commands in DB transaction (skips Queries by name suffix check), uses `ExecutionStrategy` for retry

### DI Registrations (Infrastructure)
- `BookingContext` (DbContext + IUnitOfWork)
- `IAppointmentRepository` -> `AppointmentRepository`
- `IResourceRepository` -> `ResourceRepository`
- `IStaffScheduleRepository` -> `StaffScheduleRepository`
- `ITherapistRepository` -> `TherapistRepository`
- `IRequestManager` -> `RequestManager`

### Startup Behavior
- Auto-applies EF Core migrations on startup (catches and logs errors without crashing)
- Swagger UI available at `/swagger` in Development

---

## Tests

### Functional Tests (`tests/BookingService.FunctionalTests/`)
- **CustomWebApplicationFactory**: Swaps PostgreSQL DbContext for InMemoryDatabase
- **ResourcesControllerTests**: 2 tests
  - `GetResources_ShouldReturnOk` - GET resources with random shopId returns 200
  - `HealthCheck_ShouldReturnHealthy` - GET /health/live returns 200

### Test Gaps
- No unit tests exist (no `BookingService.UnitTests` project)
- No tests for appointment CRUD operations
- No tests for slot-finding algorithm
- No tests for therapist operations
- No tests for domain entity behavior methods
- No validator tests

---

## Known Issues / Observations

1. **UpdateResourceCommand handler bug**: The handler receives `Name` and `Capacity` fields but only uses `IsActive` to toggle Activate/Deactivate. Name and Capacity values are ignored.

2. **IAppointmentRepository in wrong layer**: `IAppointmentRepository` is defined in `BookingService.Infrastructure.Repositories` instead of `BookingService.Domain.AggregatesModel.AppointmentAggregate`. Same for `IResourceRepository` and `IStaffScheduleRepository`. Only `ITherapistRepository` correctly resides in the Domain layer. This violates Clean Architecture (Domain should define interfaces, Infrastructure implements them).

3. **String-based status instead of Enumeration**: The `Appointment` entity uses raw strings for status ("Pending", "Confirmed", etc.) rather than the `AppointmentStatus` Enumeration class. The Enumeration exists only as a seed data reference table.

4. **No [Authorize] on public endpoints**: Most controllers (Appointments, Resources, Schedules, Slots, StaffSchedules, Therapists) lack `[Authorize]` attributes. Only Admin controllers require authorization.

5. **SchedulesController bypasses CQRS**: The `CreateSchedule` and `DeleteSchedule` actions directly use `IStaffScheduleRepository` instead of dispatching commands through MediatR.

6. **No DomainEvents on Resource**: Resource entity has no domain events (unlike Appointment and Therapist).

7. **Redis configured but unused**: Redis connection string is in appsettings but no caching logic exists.

8. **Idempotency infrastructure exists but is unused**: `ClientRequest`, `IRequestManager`, `RequestManager` are registered but never called from any command handler.

9. **Dapper registered but unused**: Dapper is in the Infrastructure csproj but no Dapper queries exist.

10. **Missing `.AsNoTracking()` on read queries**: Query handlers directly use `BookingContext` but don't call `.AsNoTracking()` for read-only operations, which could impact performance.
