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

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), 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

{
  "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.