Files
pos-system/services/booking-service-net/SERVICE_DOCS.md
Ho Ngoc Hai f3779c4ebe docs: add SERVICE_DOCS.md for all 24 microservices from per-service code audit
Each SERVICE_DOCS.md documents: Overview, API Endpoints, Commands, Queries,
Domain Model, Database Schema, Integration Events, Dependencies, Configuration.
Generated by 23 parallel audit agents reading actual source code.

Key corrections from audit:
- inventory-service: 12 commands/6 queries (was listed as scaffold)
- promotion-service: 12 commands/10 queries (was listed as 0)
- mission-service: 4 commands/7 queries (was listed as 0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:54:53 +07:00

535 lines
23 KiB
Markdown

# 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.