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>
23 KiB
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), raisesAppointmentCancelledDomainEvent - 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), raisesTherapistUpdatedDomainEvent - 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- Gets staff schedules for the day (filtered by specific staff if provided)
- Gets existing non-cancelled appointments for the date
- For each staff schedule, generates 15-minute interval slots
- Excludes slots that conflict with existing appointments (overlap check)
- 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, raisesAppointmentConfirmedDomainEventMarkAsInProgress()- Confirmed -> InProgressComplete()- InProgress -> Completed, raisesAppointmentCompletedDomainEventCancel(reason)- Any (except Completed/Cancelled) -> Cancelled, raisesAppointmentCancelledDomainEventMarkNoShow()- Pending/Confirmed -> NoShow
State Machine:
Pending --> Confirmed --> InProgress --> Completed
| |
| v
+--------> NoShow
|
v
Cancelled (from any state except Completed)
Domain Events:
AppointmentCreatedDomainEvent(Appointment)- on creationAppointmentConfirmedDomainEvent(Appointment)- on confirmAppointmentCompletedDomainEvent(Appointment)- on completeAppointmentCancelledDomainEvent(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 = trueDeactivate()- 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 activeDeactivate()- Sets IsActive = false, raises error if already inactiveUpdateSpecialties(specialties)- Updates specialties, raisesTherapistUpdatedDomainEventUpdateWorkingHours(workingHours)- Updates working hours JSON, raisesTherapistUpdatedDomainEventUpdateName(name)- Updates name, raisesTherapistUpdatedDomainEventUpdate(name, specialties, workingHours)- Bulk update, raisesTherapistUpdatedDomainEvent
Domain Events:
TherapistCreatedDomainEvent(Therapist)- on creationTherapistUpdatedDomainEvent(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_idon (shop_id)ix_appointments_customer_idon (customer_id)ix_appointments_staff_idon (staff_id)ix_appointments_start_timeon (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_idon (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_dayon (staff_id, day_of_week)ix_staff_schedules_shop_idon (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_idon (shop_id)ix_therapists_shop_activeon (shop_id, is_active)
Migrations
20260117181734_InitialCreate- appointments, appointment_statuses (seeded), resources, staff_schedules + all indexes20260306175525_PhaseTwo- Addednotescolumn to appointments, addedtherapiststable + 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_servicedatabase (Neon cloud for staging/prod, local for dev) - Redis: Configured but not actively used in current code (connection string in appsettings)
Configuration
appsettings.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/ProductionDATABASE_URL- Fallback connection stringJwt: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
ValidationExceptionon failure - TransactionBehavior: Wraps Commands in DB transaction (skips Queries by name suffix check), uses
ExecutionStrategyfor retry
DI Registrations (Infrastructure)
BookingContext(DbContext + IUnitOfWork)IAppointmentRepository->AppointmentRepositoryIResourceRepository->ResourceRepositoryIStaffScheduleRepository->StaffScheduleRepositoryITherapistRepository->TherapistRepositoryIRequestManager->RequestManager
Startup Behavior
- Auto-applies EF Core migrations on startup (catches and logs errors without crashing)
- Swagger UI available at
/swaggerin Development
Tests
Functional Tests (tests/BookingService.FunctionalTests/)
- CustomWebApplicationFactory: Swaps PostgreSQL DbContext for InMemoryDatabase
- ResourcesControllerTests: 2 tests
GetResources_ShouldReturnOk- GET resources with random shopId returns 200HealthCheck_ShouldReturnHealthy- GET /health/live returns 200
Test Gaps
- No unit tests exist (no
BookingService.UnitTestsproject) - 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
-
UpdateResourceCommand handler bug: The handler receives
NameandCapacityfields but only usesIsActiveto toggle Activate/Deactivate. Name and Capacity values are ignored. -
IAppointmentRepository in wrong layer:
IAppointmentRepositoryis defined inBookingService.Infrastructure.Repositoriesinstead ofBookingService.Domain.AggregatesModel.AppointmentAggregate. Same forIResourceRepositoryandIStaffScheduleRepository. OnlyITherapistRepositorycorrectly resides in the Domain layer. This violates Clean Architecture (Domain should define interfaces, Infrastructure implements them). -
String-based status instead of Enumeration: The
Appointmententity uses raw strings for status ("Pending", "Confirmed", etc.) rather than theAppointmentStatusEnumeration class. The Enumeration exists only as a seed data reference table. -
No [Authorize] on public endpoints: Most controllers (Appointments, Resources, Schedules, Slots, StaffSchedules, Therapists) lack
[Authorize]attributes. Only Admin controllers require authorization. -
SchedulesController bypasses CQRS: The
CreateScheduleandDeleteScheduleactions directly useIStaffScheduleRepositoryinstead of dispatching commands through MediatR. -
No DomainEvents on Resource: Resource entity has no domain events (unlike Appointment and Therapist).
-
Redis configured but unused: Redis connection string is in appsettings but no caching logic exists.
-
Idempotency infrastructure exists but is unused:
ClientRequest,IRequestManager,RequestManagerare registered but never called from any command handler. -
Dapper registered but unused: Dapper is in the Infrastructure csproj but no Dapper queries exist.
-
Missing
.AsNoTracking()on read queries: Query handlers directly useBookingContextbut don't call.AsNoTracking()for read-only operations, which could impact performance.