# Scheduling App

## 🎯 Application Overview

This document demonstrates how to implement a comprehensive appointment scheduling system using the Flexbase framework, featuring domain models, workflow states, resource availability management, appointment booking, and enterprise-grade role-based access control. This system is designed to be **universally applicable** across different business scenarios including healthcare, education, consulting, fitness, professional services, and inventory-backed offerings.

***

## 📋 Business Requirements

### **Core Entities:**

* **Offering** - Catalog items offered (Services, Products, Rentals, Subscriptions)
* **Resource** - Fulfillment resource (People, Assets/Equipment, Facilities, Inventory Pools)
* **AvailabilitySchedule** - Recurring/single schedules created by resources
* **TimeSlot** - Individual time slots available for booking
* **Appointment** - Actual booking by customers
* **Customer** - Users who book appointments (Patients, Students, Customers, Attendees)
* **StaffMember** - Admin/staff who can manage schedules on behalf of resources
* **Notification** - Appointment reminders and notifications

### **Workflow Requirements:**

* **Resource Availability Management** - Daily, Weekly, Single, Recurring schedules
* **Time Slot Generation** - Automatic or manual slot creation based on schedule
* **Appointment Booking** - User-initiated or staff-initiated bookings
* **Appointment Management** - Confirmation, rescheduling, cancellation
* **Staff Override** - Admin/staff can create schedules on behalf of resources
* **Automatic Notifications** - Reminders before appointments
* **Availability Blocking** - Block time for breaks, meetings, leave

### **Role-Based Access Control:**

* **Resource** - Create/manage own availability, view own appointments
* **Customer/User** - View available slots, book appointments, manage own bookings
* **Receptionist** - Manage appointments, send notifications
* **Admin/Staff** - Create schedules on behalf of resources, manage all appointments
* **Billing/Finance** - View payment and billing information
* **FacilityManager** - Manage resources and overall facility schedules
* **Admin** - Full system access

***

## 🎯 Business Scenario Adaptations

This scheduling system is designed to be universally applicable. Here are example adaptations:

| Business Scenario  | Resource                     | Customer | Offering                            | Availability          |
| ------------------ | ---------------------------- | -------- | ----------------------------------- | --------------------- |
| **Healthcare**     | Doctor, Therapist, Dentist   | Patient  | Consultation, Checkup, Surgery      | Daily clinic hours    |
| **Education**      | Teacher, Instructor, Tutor   | Student  | Class, Tutoring Session, Workshop   | Weekly class schedule |
| **Consulting**     | Consultant, Coach, Advisor   | Customer | Consultation, Strategy Session      | By appointment        |
| **Fitness**        | Trainer, Instructor          | Member   | Personal Training, Group Class      | Class schedule        |
| **Legal Services** | Lawyer, Attorney             | Customer | Legal Consultation, Document Review | Office hours          |
| **IT Support**     | Technician, Support Engineer | User     | Technical Support, System Setup     | Support hours         |
| **Salon Services** | Stylist, Beautician          | Customer | Haircut, Massage, Spa Service       | Service schedule      |

***

## 🧱 Core Scheduling Modules (In Depth)

### 1) Offerings Module

#### OfferingsController Features

* CreateOffering (POST) → `Offering.CreateOffering()`
* UpdateOffering (PUT) → `Offering.UpdateOffering()`
* PublishOffering (POST) → `Offering.Publish()`
* DeactivateOffering (POST) → `Offering.Deactivate()`
* ArchiveOffering (POST) → `Offering.Archive()`
* GetOffering(s) (GET)

#### Domain Object: Offering

* Workflow: `OfferingWorkflowState`
* States: Draft → Published → Deactivated → Archived
* RBAC:
  * Admin/Staff: Create, Update, Publish, Deactivate, Archive
  * Resource: View
  * Customer: View published

```csharp
[Table("Offerings")]
[Index(nameof(OfferingCode), Name = "IX_Offerings_OfferingCode", IsUnique = true)]
public partial class Offering : DomainModelBridge
{
    public OfferingWorkflowState OfferingState { get; protected set; } = new OfferingDraft();
    [Required] public string OfferingCode { get; protected set; }
    [Required] public string Name { get; protected set; }
    public string Description { get; protected set; }
    public TimeSpan DefaultDuration { get; protected set; } = TimeSpan.FromMinutes(30);
    public decimal? BasePrice { get; protected set; }
    public bool RequiresAssets { get; protected set; }

    [BusinessMethod]
    public virtual Offering CreateOffering(CreateOfferingCommand cmd)
    {
        var role = cmd.Dto.GetAppContext()?.UserRole ?? "Guest";
        if (!OfferingState.CanTransition(role, "Create")) throw new UnauthorizedAccessException();
        this.Convert(cmd.Dto); this.SetAdded(cmd.Dto.GetGeneratedId());
        return this;
    }

    [BusinessMethod]
    public virtual Offering Publish(PublishOfferingCommand cmd)
    {
        var role = cmd.Dto.GetAppContext()?.UserRole ?? "Guest";
        if (!OfferingState.CanTransition(role, "Publish")) throw new UnauthorizedAccessException();
        OfferingState = OfferingState.Publish(); this.SetModified(); return this;
    }
}

public class OfferingWorkflowState : FlexState
{
    public virtual OfferingWorkflowState Publish() => this;
    public virtual OfferingWorkflowState Deactivate() => this;
    public virtual OfferingWorkflowState Archive() => this;
    public virtual bool CanTransition(string role, string action) => GetAllowedRoles(action).Contains(role);
    protected virtual string[] GetAllowedRoles(string action) => Array.Empty<string>();
}

public class OfferingDraft : OfferingWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "Create" => new[] { "Admin", "Staff" },
        "Publish" => new[] { "Admin", "Staff" },
        _ => Array.Empty<string>()
    };
    public override OfferingWorkflowState Publish() => new OfferingPublished();
}

public class OfferingPublished : OfferingWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "Deactivate" => new[] { "Admin", "Staff" },
        "Archive" => new[] { "Admin" },
        _ => Array.Empty<string>()
    };
    public override OfferingWorkflowState Deactivate() => new OfferingDeactivated();
    public override OfferingWorkflowState Archive() => new OfferingArchived();
}

public class OfferingDeactivated : OfferingWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "Publish" => new[] { "Admin", "Staff" },
        "Archive" => new[] { "Admin" },
        _ => Array.Empty<string>()
    };
    public override OfferingWorkflowState Publish() => new OfferingPublished();
    public override OfferingWorkflowState Archive() => new OfferingArchived();
}

public class OfferingArchived : OfferingWorkflowState { }
```

***

### 2) Resources Module

#### ResourcesController Features

* RegisterResource (POST) → `Resource.RegisterResource()`
* UpdateProfile (PUT)
* Activate (POST) → `Resource.Activate()`
* Deactivate (POST) → `Resource.Deactivate()`
* PutOnLeave (POST) → `Resource.PutOnLeave()`
* ReturnFromLeave (POST) → `Resource.ReturnFromLeave()`

#### Domain Object: Resource

* Workflow: `ResourceWorkflowState`
* States: Registered → Active → OnLeave → Inactive
* RBAC:
  * Resource: Update own profile
  * Staff/Admin: Activate/Deactivate/OnLeave

```csharp
[Table("Resources")]
[Index(nameof(Email), Name="IX_Resources_Email", IsUnique = true)]
public partial class Resource : DomainModelBridge
{
    public ResourceWorkflowState ResourceState { get; protected set; } = new ResourceRegistered();
    [Required] public string FirstName { get; protected set; }
    [Required] public string LastName { get; protected set; }
    [Required] public string Email { get; protected set; }
    public string Phone { get; protected set; }
    public string Specialization { get; protected set; }

    [BusinessMethod]
    public virtual Resource RegisterResource(RegisterResourceCommand cmd)
    {
        var role = cmd.Dto.GetAppContext()?.UserRole ?? "Guest";
        if (!ResourceState.CanTransition(role, "Register")) throw new UnauthorizedAccessException();
        this.Convert(cmd.Dto); this.SetAdded(cmd.Dto.GetGeneratedId());
        return this;
    }

    [BusinessMethod]
    public virtual Resource Activate(ActivateResourceCommand cmd)
    {
        var role = cmd.Dto.GetAppContext()?.UserRole ?? "Guest";
        if (!ResourceState.CanTransition(role, "Activate")) throw new UnauthorizedAccessException();
        ResourceState = ResourceState.Activate(); this.SetModified(); return this;
    }
}

public class ResourceWorkflowState : FlexState
{
    public virtual ResourceWorkflowState Activate() => this;
    public virtual ResourceWorkflowState Deactivate() => this;
    public virtual ResourceWorkflowState PutOnLeave() => this;
    public virtual ResourceWorkflowState ReturnFromLeave() => this;
    public virtual bool CanTransition(string role, string action) => GetAllowedRoles(action).Contains(role);
    protected virtual string[] GetAllowedRoles(string action) => Array.Empty<string>();
}

public class ResourceRegistered : ResourceWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "Register" => new[] { "Staff", "Admin" },
        "Activate" => new[] { "Staff", "Admin" },
        _ => Array.Empty<string>()
    };
    public override ResourceWorkflowState Activate() => new ResourceActive();
}

public class ResourceActive : ResourceWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "Deactivate" => new[] { "Staff", "Admin" },
        "PutOnLeave" => new[] { "Staff", "Admin" },
        _ => Array.Empty<string>()
    };
    public override ResourceWorkflowState Deactivate() => new ResourceInactive();
    public override ResourceWorkflowState PutOnLeave() => new ResourceOnLeave();
}

public class ResourceOnLeave : ResourceWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "ReturnFromLeave" => new[] { "Staff", "Admin" },
        _ => Array.Empty<string>()
    };
    public override ResourceWorkflowState ReturnFromLeave() => new ResourceActive();
}

public class ResourceInactive : ResourceWorkflowState { }
```

***

### 3) AvailabilitySchedules Module

#### AvailabilitySchedulesController Features

* CreateSchedule (POST) → `AvailabilitySchedule.CreateSchedule()`
* UpdateSchedule (PUT)
* PublishSchedule (POST) → `AvailabilitySchedule.Publish()`
* PauseSchedule (POST) → `AvailabilitySchedule.Pause()`
* ArchiveSchedule (POST) → `AvailabilitySchedule.Archive()`
* GenerateTimeSlots (POST) → `AvailabilitySchedule.GenerateTimeSlots()`

#### Domain Object: AvailabilitySchedule

* Types: Single, Daily, Weekly, BiWeekly, Monthly, Custom Recurring
* Fields: Date range, daily window, days of week, slot duration, buffer
* Workflow: `AvailabilityScheduleWorkflowState` (Draft → Published → Paused → Archived)
* RBAC: Resource (own), Staff/Admin (all)

```csharp
[Table("AvailabilitySchedules")]
[Index(nameof(ResourceId), Name="IX_Schedules_ResourceId")]
public partial class AvailabilitySchedule : DomainModelBridge
{
    public AvailabilityScheduleWorkflowState ScheduleState { get; protected set; } = new ScheduleDraft();
    public string ResourceId { get; protected set; }
    public DateTimeOffset StartDate { get; protected set; }
    public DateTimeOffset? EndDate { get; protected set; }
    public TimeSpan StartTime { get; protected set; }
    public TimeSpan EndTime { get; protected set; }
    public int SlotDurationMinutes { get; protected set; } = 30;
    public int BufferMinutes { get; protected set; } = 0;
    public bool Monday { get; protected set; } public bool Tuesday { get; protected set; } public bool Wednesday { get; protected set; }
    public bool Thursday { get; protected set; } public bool Friday { get; protected set; } public bool Saturday { get; protected set; } public bool Sunday { get; protected set; }
    public string TimeZoneId { get; protected set; }

    [BusinessMethod]
    public virtual AvailabilitySchedule CreateSchedule(CreateScheduleCommand cmd)
    {
        var role = cmd.Dto.GetAppContext()?.UserRole ?? "Guest";
        if (!ScheduleState.CanTransition(role, "Create")) throw new UnauthorizedAccessException();
        this.Convert(cmd.Dto); this.SetAdded(cmd.Dto.GetGeneratedId()); return this;
    }

    [BusinessMethod]
    public virtual List<TimeSlot> GenerateTimeSlots(GenerateTimeSlotsCommand cmd)
    {
        var role = cmd.Dto.GetAppContext()?.UserRole ?? "Guest";
        if (!ScheduleState.CanTransition(role, "Generate")) throw new UnauthorizedAccessException();
        var slots = new List<TimeSlot>();
        var endDate = EndDate ?? StartDate.AddYears(1);
        for (var day = StartDate.Date; day <= endDate.Date; day = day.AddDays(1))
        {
            if (!IsActiveDay(day.DayOfWeek)) continue;
            var cursor = StartTime;
            while (cursor + TimeSpan.FromMinutes(SlotDurationMinutes) <= EndTime)
            {
                slots.Add(TimeSlot.Create(ResourceId, day, cursor, cursor + TimeSpan.FromMinutes(SlotDurationMinutes)));
                cursor += TimeSpan.FromMinutes(SlotDurationMinutes + BufferMinutes);
            }
        }
        return slots;
    }

    private bool IsActiveDay(DayOfWeek d) => d switch
    {
        DayOfWeek.Monday => Monday,
        DayOfWeek.Tuesday => Tuesday,
        DayOfWeek.Wednesday => Wednesday,
        DayOfWeek.Thursday => Thursday,
        DayOfWeek.Friday => Friday,
        DayOfWeek.Saturday => Saturday,
        DayOfWeek.Sunday => Sunday,
        _ => false
    };
}

public class AvailabilityScheduleWorkflowState : FlexState
{
    public virtual AvailabilityScheduleWorkflowState Publish() => this;
    public virtual AvailabilityScheduleWorkflowState Pause() => this;
    public virtual AvailabilityScheduleWorkflowState Archive() => this;
    public virtual bool CanTransition(string role, string action) => GetAllowedRoles(action).Contains(role);
    protected virtual string[] GetAllowedRoles(string action) => Array.Empty<string>();
}

public class ScheduleDraft : AvailabilityScheduleWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "Create" => new[] { "ServiceProvider", "Staff", "Admin" },
        "Publish" => new[] { "ServiceProvider", "Staff", "Admin" },
        "Generate" => new[] { "ServiceProvider", "Staff", "Admin" },
        _ => Array.Empty<string>()
    };
    public override AvailabilityScheduleWorkflowState Publish() => new SchedulePublished();
}

public class SchedulePublished : AvailabilityScheduleWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "Pause" => new[] { "ServiceProvider", "Staff", "Admin" },
        "Archive" => new[] { "Admin" },
        "Generate" => new[] { "ServiceProvider", "Staff", "Admin" },
        _ => Array.Empty<string>()
    };
    public override AvailabilityScheduleWorkflowState Pause() => new SchedulePaused();
    public override AvailabilityScheduleWorkflowState Archive() => new ScheduleArchived();
}

public class SchedulePaused : AvailabilityScheduleWorkflowState
{
    protected override string[] GetAllowedRoles(string action) => action switch
    {
        "Publish" => new[] { "ServiceProvider", "Staff", "Admin" },
        _ => Array.Empty<string>()
    };
    public override AvailabilityScheduleWorkflowState Publish() => new SchedulePublished();
}

public class ScheduleArchived : AvailabilityScheduleWorkflowState { }
```

***

### 4) TimeSlots Module

#### TimeSlotsController Features

* GenerateFromSchedule (POST) → `AvailabilitySchedule.GenerateTimeSlots()`
* BlockSlot (POST) → `TimeSlot.Block()`
* ReleaseSlot (POST) → `TimeSlot.Release()`
* GetAvailableSlots (GET)

#### Domain Object: TimeSlot

* Workflow: `TimeSlotWorkflowState` (Available → Blocked → Reserved → Completed/Released)
* RBAC: Resource/Staff can Block/Release, Customer can reserve

```csharp
[Table("TimeSlots")]
[Index(nameof(ResourceId), Name="IX_TimeSlots_ResourceId")]
public partial class TimeSlot : DomainModelBridge
{
    public TimeSlotWorkflowState TimeSlotState { get; protected set; } = new SlotAvailable();
    public string ResourceId { get; protected set; }
    public string? ScheduleId { get; protected set; } // FK -> AvailabilitySchedules.Id
    public AvailabilitySchedule Schedule { get; protected set; }
    public DateTime Date { get; protected set; }
    public TimeSpan StartTime { get; protected set; }
    public TimeSpan EndTime { get; protected set; }
    public string? CalendarIndexId { get; protected set; } // FK -> CalendarIndex.Id (string)
   
```

***

### 5) Bookings (Appointments) Module

#### BookingsController Features

* CreateBooking (POST) → `Appointment.CreateBooking()`
* ConfirmBooking (POST) → `Appointment.Confirm()`
* RescheduleBooking (POST) → `Appointment.Reschedule()`
* CancelBooking (POST) → `Appointment.Cancel()`
* CompleteBooking (POST) → `Appointment.Complete()`

#### Domain Object: Appointment

* Workflow: `AppointmentWorkflowState` (Requested → Confirmed → Rescheduled → Cancelled/Completed)
* Links: `ResourceId`, `OfferingId`, `TimeSlotId`, `CustomerUserAccountId` (or AccountableEntityId per your model)
* Validations: resource/time conflicts via `CalendarIndex` overlaps; capacity vs `ConcurrentLimit` for the slot

```csharp
[Table("Appointments")]
[Index(nameof(ResourceId), nameof(TimeSlotId), Name = "IX_Appointments_Resource_TimeSlot")] 
public partial class Appointment : DomainModelBridge
{
    public AppointmentWorkflowState AppointmentState { get; protected set; } = new AppointmentRequested();

    public string ResourceId { get; protected set; }
    public string OfferingId { get; protected set; }
    public string TimeSlotId { get; protected set; }         // FK → TimeSlots.Id
    public TimeSlot TimeSlot { get; protected set; }

    public string CommissionerUserAccountId { get; protected set; }  // usually customer
    public string? BookedByUserAccountId { get; protected set; }     // staff/customer/provider who booked

    public DateTimeOffset BookedAtUtc { get; protected set; }
    public string? Notes { get; protected set; }

    [BusinessMethod]
    public virtual Appointment CreateBooking(CreateBookingCommand cmd)
    {
        // 1) Validate capacity/exclusivity using CalendarIndex
        var startUtc = cmd.StartUtc; var endUtc = cmd.EndUtc;
        var overlaps = _db.CalendarIndex.Where(c => c.ResourceType == "Resource" && c.ResourceId == cmd.ResourceId && c.StartTicks < endUtc.UtcTicks && startUtc.UtcTicks < c.EndTicks).ToList();
        var exclusiveConflict = overlaps.Any(o => o.IsExclusive);
        var availabilityCapacity = overlaps.Where(o => o.Kind == "Availability" && o.TimeSlotId == cmd.TimeSlotId).Select(o => o.ConcurrentLimit).DefaultIfEmpty(0).Max();
        var existingAppointments = overlaps.Count(o => o.Kind == "Appointment" && o.TimeSlotId == cmd.TimeSlotId);
        if (exclusiveConflict || existingAppointments >= availabilityCapacity) throw new InvalidOperationException("No capacity");

        // 2) Create booking and operational index row
        this.Convert(cmd.Dto); this.SetAdded(cmd.Dto.GetGeneratedId());
        _db.CalendarIndex.Add(new CalendarIndex
        {
            Id = BuildCalendarIndexId("Resource", cmd.ResourceId, startUtc, endUtc, "Appointment", this.Id),
            ResourceType = "Resource",
            ResourceId = cmd.ResourceId,
            TimeZoneId = cmd.TimeZoneId,
            StartTicks = startUtc.UtcTicks,
            EndTicks = endUtc.UtcTicks,
            Date = startUtc.UtcDateTime.Date,
            Kind = "Appointment",
            IsExclusive = true,
            ConcurrentLimit = 1,
            OriginType = nameof(Appointment),
            OriginId = this.Id,
            AvailabilityScheduleId = cmd.ScheduleId,
            TimeSlotId = cmd.TimeSlotId
        });
        return this;
    }
}

public class AppointmentWorkflowState : FlexState
{
    public virtual AppointmentWorkflowState Confirm() => this;
    public virtual AppointmentWorkflowState Reschedule() => this;
    public virtual AppointmentWorkflowState Cancel() => this;
    public virtual AppointmentWorkflowState Complete() => this;
}

public class AppointmentRequested : AppointmentWorkflowState
{
    public override AppointmentWorkflowState Confirm() => new AppointmentConfirmed();
    public override AppointmentWorkflowState Cancel() => new AppointmentCancelled();
}

public class AppointmentConfirmed : AppointmentWorkflowState
{
    public override AppointmentWorkflowState Reschedule() => new AppointmentRescheduled();
    public override AppointmentWorkflowState Complete() => new AppointmentCompleted();
    public override AppointmentWorkflowState Cancel() => new AppointmentCancelled();
}

public class AppointmentRescheduled : AppointmentWorkflowState
{
    public override AppointmentWorkflowState Confirm() => new AppointmentConfirmed();
    public override AppointmentWorkflowState Cancel() => new AppointmentCancelled();
}

public class AppointmentCancelled : AppointmentWorkflowState { }
public class AppointmentCompleted : AppointmentWorkflowState { }
```

Booking flow (end-to-end):

* Discover: client queries available `TimeSlot`s for a `Resource`/`Offering`.
* Validate: server checks `CalendarIndex` for overlaps and capacity.
* Reserve: create `Appointment` and a matching `CalendarIndex` Appointment row.
* Confirm: optional state move; reminders/notifications scheduled.
* Reschedule: create new slot mapping and update corresponding `CalendarIndex` rows (old row deleted, new row inserted).
* Cancel/Complete: update appointment state and delete/mark `CalendarIndex` Appointment row accordingly; restore capacity.

## 📅 Schedule Calendar (Timezone-Aware, Resource-Centric)

Purpose: Present a resource’s working calendar (availability + appointments) in the viewer’s timezone, with fast navigation (day/week/month) and conflict-safe validations.

Data sources:

* `TimeSlot` (Available/Blocked/Reserved/Completed)
* `Appointment` (Requested/Confirmed/Rescheduled/Cancelled/Completed)
* Projected into `CalendarEntry` and `CalendarIndex` with `StartUtc`, `EndUtc`, `TimeZoneId`

API pattern (accepts `viewTz`):

```csharp
[HttpGet("/schedule/{resourceId}/week")] 
public IEnumerable<CalendarViewDto> GetResourceWeek(string resourceId, DateTime weekStartLocal, string viewTz = "UTC")
{
	var tz = DateTimeZoneProviders.Tzdb[viewTz];
	var startLocal = new LocalDateTime(weekStartLocal.Year, weekStartLocal.Month, weekStartLocal.Day, 0, 0);
	var endLocal = startLocal.PlusDays(7);
	var startUtc = tz.AtLeniently(startLocal).ToDateTimeOffset();
	var endUtc = tz.AtLeniently(endLocal).ToDateTimeOffset();

	var items = _db.CalendarIndex
		.Where(c => c.ResourceType == "Resource" && c.ResourceId == resourceId && c.StartTicks < endUtc.UtcTicks && startUtc.UtcTicks < c.EndTicks)
		.OrderBy(c => c.StartTicks)
		.ToList();

	return items.Select(c => ConvertForView(c, viewTz));
}
```

Rendering rules:

* Availability entries: `EntryType = "Availability"` (from `TimeSlot`)
* Appointments: `EntryType = "Appointment"`, include `AppointmentId`
* Blocked: show as non-bookable
* Respect DST by always converting `StartUtc/EndUtc` to `viewTz` at response time

Validation endpoint:

```csharp
[HttpGet("/schedule/{resourceId}/validate")] 
public CalendarValidationResult ValidateResourceRange(string resourceId, DateTime dateLocal, TimeSpan startLocal, TimeSpan endLocal, string viewTz = "UTC")
{
	var tz = DateTimeZoneProviders.Tzdb[viewTz];
	var startLocalDt = new LocalDateTime(dateLocal.Year, dateLocal.Month, dateLocal.Day, startLocal.Hours, startLocal.Minutes);
	var endLocalDt = new LocalDateTime(dateLocal.Year, dateLocal.Month, dateLocal.Day, endLocal.Hours, endLocal.Minutes);
	var startUtc = tz.AtLeniently(startLocalDt).ToDateTimeOffset();
	var endUtc = tz.AtLeniently(endLocalDt).ToDateTimeOffset();

	var overlaps = _db.CalendarIndex.Where(c => c.ResourceType == "Resource" && c.ResourceId == resourceId && c.StartTicks < endUtc.UtcTicks && startUtc.UtcTicks < c.EndTicks).ToList();
	var exclusiveConflict = overlaps.Any(o => o.IsExclusive);
	var concurrentCount = overlaps.Count;
	var limit = overlaps.FirstOrDefault()?.ConcurrentLimit ?? int.MaxValue;
	return new CalendarValidationResult { HasConflict = exclusiveConflict || concurrentCount >= limit, Overlaps = overlaps };
}
```

***

### 6) Appointment Requests Module

Purpose: Allow customers (or anonymous users) to request an appointment without picking an exact slot. Requests can be targeted to a specific Resource or left open. Resources (or staff) review, select a suitable slot, and convert the request into a Booking.

#### RequestsController Features

* CreateRequest (POST) → `AppointmentRequest.Create()` (supports authenticated customer or anonymous)
* AssignResource (POST) → `AppointmentRequest.AssignResource()` (staff/resource)
* ProposeSlot (POST) → `AppointmentRequest.ProposeSlot()` (staff/resource proposes candidate TimeSlot)
* AcceptProposal (POST) → `AppointmentRequest.AcceptProposal()` (customer confirms)
* ConvertToBooking (POST) → `AppointmentRequest.ConvertToBooking()` (creates `Appointment` and CalendarIndex row)
* CancelRequest (POST) → `AppointmentRequest.Cancel()`

#### Domain Object: AppointmentRequest

* Workflow: `AppointmentRequestWorkflowState` (Submitted → Assigned → Proposed → Converted | Cancelled)
* Fields: optional `ResourceId`, `OfferingId`, desired date/time window, notes, requester identity or anonymous contact info
* Visibility: anonymous requests are visible to all resources of the Offering; targeted requests only to the selected Resource

```csharp
[Table("AppointmentRequests")]
[Index(nameof(OfferingId), nameof(ResourceId), Name = "IX_Requests_Offering_Resource")]
public partial class AppointmentRequest : DomainModelBridge
{
    public AppointmentRequestWorkflowState RequestState { get; protected set; } = new RequestSubmitted();

    public string OfferingId { get; protected set; }
    public string? ResourceId { get; protected set; }              // null => open request

    // Requester identity (customer or anonymous)
    public string? CustomerUserAccountId { get; protected set; }   // null => anonymous
    public string? AnonymousName { get; protected set; }
    public string? AnonymousEmail { get; protected set; }
    public string? AnonymousPhone { get; protected set; }

    // Preferred window
    public DateTimeOffset? PreferredStartFromUtc { get; protected set; }
    public DateTimeOffset? PreferredStartToUtc { get; protected set; }
    public string TimeZoneId { get; protected set; }
    public string? Notes { get; protected set; }

    // Proposal linkage
    public string? ProposedTimeSlotId { get; protected set; }

    [BusinessMethod]
    public virtual AppointmentRequest Create(CreateAppointmentRequestCommand cmd)
    {
        this.Convert(cmd.Dto); this.SetAdded(cmd.Dto.GetGeneratedId());
        return this;
    }

    [BusinessMethod]
    public virtual AppointmentRequest AssignResource(AssignResourceToRequestCommand cmd)
    {
        var role = cmd.Dto.GetAppContext()?.UserRole ?? "Guest";
        // staff/resource only
        if (role is not ("Staff" or "Admin" or "Resource")) throw new UnauthorizedAccessException();
        ResourceId = cmd.ResourceId; this.SetModified();
        RequestState = RequestState.Assign();
        return this;
    }

    [BusinessMethod]
    public virtual AppointmentRequest ProposeSlot(ProposeSlotForRequestCommand cmd)
    {
        var role = cmd.Dto.GetAppContext()?.UserRole ?? "Guest";
        if (role is not ("Staff" or "Admin" or "Resource")) throw new UnauthorizedAccessException();
        ProposedTimeSlotId = cmd.TimeSlotId; this.SetModified();
        RequestState = RequestState.Propose();
        return this;
    }

    [BusinessMethod]
    public virtual Appointment ConvertToBooking(ConvertRequestToBookingCommand cmd)
    {
        // Validate: ProposedTimeSlotId present and available via CalendarIndex capacity checks
        var slot = _db.TimeSlots.Find(ProposedTimeSlotId ?? cmd.TimeSlotId);
        if (slot == null) throw new InvalidOperationException("No proposed slot");
        // Run the same validation as CreateBooking
        // ... omitted for brevity ...

        var appt = new Appointment().Convert(cmd.Dto);
        appt.SetAdded(cmd.Dto.GetGeneratedId());
        // link to selected slot/resource/offering
        // ... set fields from request + cmd ...
        RequestState = RequestState.Convert(); this.SetModified();
        return appt;
    }
}

public class AppointmentRequestWorkflowState : FlexState
{
    public virtual AppointmentRequestWorkflowState Assign() => this;
    public virtual AppointmentRequestWorkflowState Propose() => this;
    public virtual AppointmentRequestWorkflowState Convert() => this;
    public virtual AppointmentRequestWorkflowState Cancel() => this;
}

public class RequestSubmitted : AppointmentRequestWorkflowState
{
    public override AppointmentRequestWorkflowState Assign() => new RequestAssigned();
    public override AppointmentRequestWorkflowState Cancel() => new RequestCancelled();
}

public class RequestAssigned : AppointmentRequestWorkflowState
{
    public override AppointmentRequestWorkflowState Propose() => new RequestProposed();
    public override AppointmentRequestWorkflowState Cancel() => new RequestCancelled();
}

public class RequestProposed : AppointmentRequestWorkflowState
{
    public override AppointmentRequestWorkflowState Convert() => new RequestConverted();
    public override AppointmentRequestWorkflowState Cancel() => new RequestCancelled();
}

public class RequestConverted : AppointmentRequestWorkflowState { }
public class RequestCancelled : AppointmentRequestWorkflowState { }
```

List endpoints to fetch:

* Open requests (no `ResourceId`) per Offering for staff/resources.
* Targeted requests by `ResourceId`.
* Customer’s own requests (if authenticated).

Conversion notes:

* Use `CalendarIndex` overlap/capacity validation when converting to booking.
* On conversion, create `Appointment` and corresponding `CalendarIndex` Appointment row, then mark request as Converted.

## 📊 Summary Calendar (Aggregated, Timezone-Aware)

Purpose: Executive/operations view with daily/weekly counts and utilization across resources and assets, rendered in a chosen timezone.

New projection table:

```csharp
[Table("CalendarSummaryIndex")]
[Index(nameof(ResourceType), nameof(ResourceId), nameof(Date), Name = "IX_CalendarSummary_Resource_Date")]
public class CalendarSummaryIndex
{
[Key] public string Id { get; set; } // Hash(ResourceType|ResourceId|Date|TimeZoneId)
	public string ResourceType { get; set; } // Resource | Asset | Facility
	public string ResourceId { get; set; }
	public string TimeZoneId { get; set; }
	public DateTime Date { get; set; } // local date in TimeZoneId
	public int AvailabilityMinutes { get; set; }
	public int BookedMinutes { get; set; }
	public int BlockedMinutes { get; set; }
	public int NoShowCount { get; set; }
	public int AppointmentCount { get; set; }
	public int CancelledCount { get; set; }
	public decimal Revenue { get; set; }
}
```

Aggregation job (daily/hourly):

* Read `CalendarIndex` in UTC
* For each resource and day in `summaryTz`, convert intervals to local and accumulate buckets:
  * Availability, Booked (appointments), Blocked, etc.
* Upsert by deterministic `Key`

API examples:

```csharp
[HttpGet("/summary/resources/day")] 
public IEnumerable<CalendarSummaryIndex> GetResourceDailySummary(string[] resourceIds, DateTime dayLocal, string summaryTz)
{
	var tz = DateTimeZoneProviders.Tzdb[summaryTz];
	var localStart = new LocalDateTime(dayLocal.Year, dayLocal.Month, dayLocal.Day, 0, 0);
	var localEnd = localStart.PlusDays(1);
	var startUtc = tz.AtLeniently(localStart).ToDateTimeOffset();
	var endUtc = tz.AtLeniently(localEnd).ToDateTimeOffset();

	// assume table already aggregated in summaryTz
	return _db.CalendarSummaryIndex
		.Where(s => s.ResourceType == "Resource" && resourceIds.Contains(s.ResourceId) && s.TimeZoneId == summaryTz && s.Date == dayLocal.Date)
		.ToList();
}

[HttpGet("/summary/assets/week")] 
public IEnumerable<CalendarSummaryIndex> GetAssetWeeklySummary(string[] assetIds, DateTime weekStartLocal, string summaryTz)
{
	var weekStart = weekStartLocal.Date;
	var days = Enumerable.Range(0, 7).Select(i => weekStart.AddDays(i)).ToArray();
	return _db.CalendarSummaryIndex
		.Where(s => s.ResourceType == "Asset" && assetIds.Contains(s.ResourceId) && s.TimeZoneId == summaryTz && days.Contains(s.Date))
		.OrderBy(s => s.ResourceId).ThenBy(s => s.Date)
		.ToList();
}
```

Why separate summary:

* Keeps operational queries fast and cheap (pre-aggregated)
* Stable across DST boundaries by anchoring per-day in the summary timezone
* Clean separation: `CalendarIndex` → operational events; `CalendarSummaryIndex` → aggregated KPI view

This brings the scheduling calendar and summary calendar back to the center of the application, both correctly timezone-aware and optimized for their respective use cases.

***

## 📚 Operational CalendarIndex (Expanded Slots for Fast Validation)

Purpose: Store one row per concrete availability slot and appointment interval to make conflict detection, capacity checks, and rendering fast and simple—even for recurring schedules.

Why expand recurring schedules:

* Simplifies overlap queries to range checks over a flat table.
* Avoids complex on-the-fly recurrence math at request time.
* Enables indexing by Resource, day, and time for fast lookups.

Recommended schema:

```csharp
[Table("CalendarIndex")]
[Index(nameof(ResourceType), nameof(ResourceId), nameof(StartTicks), nameof(EndTicks), Name = "IX_CalendarIndex_Res_Start_End")]
[Index(nameof(Date), nameof(ResourceType), nameof(ResourceId), Name = "IX_CalendarIndex_Date_Res")] // local date in TimeZoneId for partitioning
public class CalendarIndex
{
    [Key] public string Id { get; set; } // deterministic hash: ResourceType|ResourceId|StartUtc|EndUtc|Kind|OriginId

    public string ResourceType { get; set; } // Resource | AccountableEntity | Asset
    public string ResourceId { get; set; }
    public string TimeZoneId { get; set; }

    // UTC interval for conflict-safe math
    public long StartTicks { get; set; }
    public long EndTicks { get; set; }

    // Optional local day for grouping/partitioning in viewer/resource TZ
    public DateTime Date { get; set; }

    public string Kind { get; set; } // Availability | Appointment | Block
    public bool IsExclusive { get; set; } // true => blocks concurrent use
    public int ConcurrentLimit { get; set; } // availability capacity (1 person, N pool)

    // Backrefs to source
    public string OriginType { get; set; } // TimeSlot | Appointment | ScheduleBlock
    public string OriginId { get; set; }

    // Optional normalized links for fast joins
    public string? AvailabilityScheduleId { get; set; }
    public string? TimeSlotId { get; set; }
}
```

Population strategy:

* On schedule Publish/Pause/Archive and on GenerateTimeSlots, expand recurrence into concrete `TimeSlot` rows (as you already do), then project those into `CalendarIndex` with Kind = Availability.
* On Appointment create/update/cancel, write/update `CalendarIndex` rows with Kind = Appointment and `IsExclusive = true` unless the Offering is concurrent.
* For blocks/maintenance, upsert Kind = Block with `IsExclusive = true`.

Linking rules:

* When a `TimeSlot` is created from a schedule, set `TimeSlot.ScheduleId = schedule.Id` and set `CalendarIndex.AvailabilityScheduleId = schedule.Id` and `TimeSlot.CalendarIndexId = CalendarIndex.Id` for that slot.
* When an `Appointment` is created on a slot, create a `CalendarIndex` row (Kind = Appointment) and set `CalendarIndex.TimeSlotId = slot.Id` and, if known, `CalendarIndex.AvailabilityScheduleId = slot.ScheduleId`.

Horizon policy (to control volume):

* Expand and maintain a rolling horizon, e.g., \[today − 7 days, today + 365 days].
* Nightly job extends the future horizon; cleanup job prunes beyond the past window.

Validation using CalendarIndex:

```csharp
// Conflict check for a new booking request
var overlaps = _db.CalendarIndex
    .Where(c => c.ResourceType == "Resource" && c.ResourceId == resourceId
        && c.StartTicks < requestEndUtc.Ticks && requestStartUtc.Ticks < c.EndTicks)
    .ToList();

var exclusiveConflict = overlaps.Any(o => o.IsExclusive);
var availabilityCapacity = overlaps.Where(o => o.Kind == "Availability").Select(o => o.ConcurrentLimit).DefaultIfEmpty(0).Max();
var existingAppointments = overlaps.Count(o => o.Kind == "Appointment");
var hasCapacity = existingAppointments < availabilityCapacity;

var isValid = !exclusiveConflict && hasCapacity;
```

Notes:

* Timezone: always store Start/End in UTC for math; include `TimeZoneId` so local `Date` can be computed deterministically.
* Capacity: `ConcurrentLimit` enables pooled resources (e.g., 5 seats, 10 devices).
* Idempotency: use deterministic `Key` for upserts from schedule/appointment events.

This table complements `CalendarSummaryIndex`: the former enables fast operational validation and rendering; the latter offers pre-aggregated analytics.

## 🧾 Accounts and Accountability Integration (Composition, Roles via Accountability)

To align with your real design and the pattern used in the recruitment document, we model roles through the Accountability table. There is a single Account root; roles are not a separate RoleMembership table but Accountability edges with `AccountabilityType` representing the role or relationship.

Key concepts

* Account (root): identity for a person or organization.
* UserAccount (facet): profile for a human; optional login via ApplicationUser.
* OrgAccount (facet): profile for an organization.
* Accountability: edges between Accounts with `CommissionerAccountId`, `ResponsibleAccountId`, `AccountabilityType`, effectivity dates.
* AccountabilityType: includes both role semantics and relationship semantics, e.g., `Resource`, `Customer`, `StaffMember`, `WorksFor`, `BooksOnBehalf`, etc.

Canonical tables

```csharp
[Table("Accounts")]
public class Account
{
	public string Id { get; set; }
	public string DisplayName { get; set; }
	public string TimeZoneId { get; set; }
}

[Table("UserAccounts")] // optional 1:1 facet
public class UserAccount
{
	[Key] public string AccountId { get; set; } // PK = FK → Account
	public Account Account { get; set; }
	public string? ApplicationUserId { get; set; }
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string Email { get; set; }
}

[Table("OrgAccounts")] // optional 1:1 facet
public class OrgAccount
{
	[Key] public string AccountId { get; set; } // PK = FK → Account
	public Account Account { get; set; }
	public string LegalName { get; set; }
	public string RegistrationNumber { get; set; }
}

[Table("Accountabilities")]
[Index(nameof(CommissionerAccountId), nameof(ResponsibleAccountId), nameof(AccountabilityType))]
public class Accountability
{
	public string Id { get; set; }
	public string CommissionerAccountId { get; set; } // primary
	public string ResponsibleAccountId { get; set; }  // secondary
	public string AccountabilityType { get; set; } // Resource | Customer | StaffMember | WorksFor | BooksOnBehalf ...
	public DateTimeOffset? EffectiveFromUtc { get; set; }
	public DateTimeOffset? EffectiveToUtc { get; set; }
}
```

How scheduling entities reference Accounts

* Replace role-specific FKs with generic `AccountId` fields; roles are validated via `AccountabilityType` at runtime.

Updated entities (fields only)

```csharp
public partial class TimeSlot
{
	public string ResourceAccountId { get; protected set; } // provider account owning this slot
	public Account ResourceAccount { get; protected set; }
}

public partial class Appointment
{
	public string CommissionerAccountId { get; protected set; }  // usually customer
	public string ResponsibleAccountId { get; protected set; }   // service provider
	public string? BookedByAccountId { get; protected set; }     // staff/customer/provider who booked
	public Account Commissioner { get; protected set; }
	public Account Responsible { get; protected set; }
	public Account BookedBy { get; protected set; }
}

public partial class AvailabilitySchedule
{
	public string ResourceAccountId { get; protected set; } // provider account
	public Account ResourceAccount { get; protected set; }
}
```

Validation examples (composition with Accountability)

```csharp
// Is this a current Resource (fulfiller)?
bool IsResource(string accountId, DateTimeOffset nowUtc) =>
	_db.Accountabilities.Any(a => a.ResponsibleAccountId == accountId && a.AccountabilityType == "Resource" &&
		(!a.EffectiveFromUtc.HasValue || a.EffectiveFromUtc <= nowUtc) &&
		(!a.EffectiveToUtc.HasValue || a.EffectiveToUtc >= nowUtc));

// Is this booking made on behalf of the resource by a staff member with a valid WorksFor/BooksOnBehalf edge?
bool CanBookOnBehalf(string staffAccountId, string resourceAccountId, DateTimeOffset nowUtc) =>
	_db.Accountabilities.Any(a => a.CommissionerAccountId == staffAccountId && a.ResponsibleAccountId == resourceAccountId &&
		(a.AccountabilityType == "WorksFor" || a.AccountabilityType == "BooksOnBehalf") &&
		(!a.EffectiveFromUtc.HasValue || a.EffectiveFromUtc <= nowUtc) &&
		(!a.EffectiveToUtc.HasValue || a.EffectiveToUtc >= nowUtc));
```

Calendar unification

* `CalendarIndex.ResourceType = "Account"` and `ResourceId = Account.Id` supports calendars for people and orgs uniformly.
* Assets can remain separate (`ResourceType = "Asset"`) or be modeled as Accounts if you want single-resource abstraction across the platform.

Migration note

* Where the document previously used `ServiceProviderId`/`ClientId`, adopt `ResponsibleAccountId`/`CommissionerAccountId` and validate role via AccountabilityType (`Resource`/`Customer`).
* Where `TimeSlot` had `ServiceProviderId`, adopt `ResourceAccountId`.

EF mapping deltas (facets)

```csharp
modelBuilder.Entity<UserAccount>()
	.HasOne(u => u.Account)
	.WithOne()
	.HasForeignKey<UserAccount>(u => u.AccountId)
	.OnDelete(DeleteBehavior.Cascade);

modelBuilder.Entity<OrgAccount>()
	.HasOne(o => o.Account)
	.WithOne()
	.HasForeignKey<OrgAccount>(o => o.AccountId)
	.OnDelete(DeleteBehavior.Cascade);
```

This keeps `UserAccount` intact and models roles/relationships via Accountability (composition over inheritance), matching the recruitment design while preserving a single, elegant FK (`AccountId`) throughout scheduling, calendars, and assets.

***

### ▶️ Refinement: UserAccount as Party Root, ApplicationUser as Login (Per Your Design)

To match your clarified model: UserAccount is the universal party for all actors (login or not). ApplicationUser represents the login identity and links 1:1 (optionally) to UserAccount. Roles are expressed via Accountability with AccountabilityType.

Updated canonical tables

```csharp
[Table("UserAccounts")] // universal party
public class UserAccount
{
	public string Id { get; set; }
	public string DisplayName { get; set; }
	public string TimeZoneId { get; set; }
	// optional person/org facets can attach 1:1 on UserAccountId
}

[Table("ApplicationUsers")] // login identity (optional)
public class ApplicationUser
{
	public string Id { get; set; }               // identity user key
	public string AccountableEntityId { get; set; } // must point to a UserAccount row
	public UserAccount UserAccount { get; set; }
}

[Table("OrgAccounts")] // organization facet (optional 1:1)
public class OrgAccount
{
	[Key] public string UserAccountId { get; set; } // PK = FK → UserAccount
	public UserAccount UserAccount { get; set; }
	public string LegalName { get; set; }
	public string RegistrationNumber { get; set; }
}

[Table("Accountabilities")]
[Index(nameof(CommissionerUserAccountId), nameof(ResponsibleUserAccountId), nameof(AccountabilityType))]
public class Accountability
{
	public string Id { get; set; }
	public string CommissionerUserAccountId { get; set; } // primary
	public string ResponsibleUserAccountId { get; set; }  // secondary
	public string AccountabilityType { get; set; }     // Resource | Customer | StaffMember | WorksFor | BooksOnBehalf ...
	public DateTimeOffset? EffectiveFromUtc { get; set; }
	public DateTimeOffset? EffectiveToUtc { get; set; }
}
```

Entity references (use UserAccountId everywhere)

```csharp
public partial class TimeSlot
{
	public string ResourceUserAccountId { get; protected set; } // provider’s party
	public UserAccount ResourceUserAccount { get; protected set; }
}

public partial class Appointment
{
	public string CommissionerUserAccountId { get; protected set; }  // usually customer
	public string ResponsibleUserAccountId { get; protected set; }   // service provider
	public string? BookedByUserAccountId { get; protected set; }     // staff/customer/provider who booked
	public UserAccount Commissioner { get; protected set; }
	public UserAccount Responsible { get; protected set; }
	public UserAccount BookedBy { get; protected set; }
}

public partial class AvailabilitySchedule
{
	public string ResourceUserAccountId { get; protected set; } // provider party
	public UserAccount ResourceUserAccount { get; protected set; }
}
```

Validation (roles via AccountabilityType)

```csharp
bool IsResource(string userAccountId, DateTimeOffset nowUtc) =>
	_db.Accountabilities.Any(a => a.ResponsibleUserAccountId == userAccountId && a.AccountabilityType == "Resource" &&
		(!a.EffectiveFromUtc.HasValue || a.EffectiveFromUtc <= nowUtc) &&
		(!a.EffectiveToUtc.HasValue || a.EffectiveToUtc >= nowUtc));

bool CanBookOnBehalf(string staffUserAccountId, string resourceUserAccountId, DateTimeOffset nowUtc) =>
	_db.Accountabilities.Any(a => a.CommissionerUserAccountId == staffUserAccountId && a.ResponsibleUserAccountId == resourceUserAccountId &&
		(a.AccountabilityType == "WorksFor" || a.AccountabilityType == "BooksOnBehalf") &&
		(!a.EffectiveFromUtc.HasValue || a.EffectiveFromUtc <= nowUtc) &&
		(!a.EffectiveToUtc.HasValue || a.EffectiveToUtc >= nowUtc));
```

Calendar alignment

* Use `CalendarIndex.ResourceType = "UserAccount"` and `ResourceId = UserAccount.Id` for people/org calendars.
* Assets remain `ResourceType = "Asset"` (or can be unified later if desired).

EF mappings (key facets)

```csharp
modelBuilder.Entity<ApplicationUser>()
	.HasOne(x => x.UserAccount).WithOne()
	.HasForeignKey<ApplicationUser>(x => x.AccountableEntityId)
	.OnDelete(DeleteBehavior.Cascade);

modelBuilder.Entity<OrgAccount>()
	.HasOne(o => o.UserAccount).WithOne()
	.HasForeignKey<OrgAccount>(o => o.UserAccountId)
	.OnDelete(DeleteBehavior.Cascade);
```

This keeps UserAccount as the single FK used across scheduling (slots, appointments, schedules, calendars), while ApplicationUser remains the optional login identity. Roles and on‑behalf authority come from Accountability rows (AccountabilityType).

***

### ▶️ Correction: AccountableEntity Root with TPH (UserAccount, OrgAccount)

Per your model, both `UserAccount` and `OrgAccount` derive from a common `AccountableEntity`. We’ll use TPH (table-per-hierarchy) with a discriminator and reference `AccountableEntityId` everywhere. `ApplicationUser` links (optionally) to a `UserAccount` row via the same key.

Canonical model (TPH)

```csharp
[Table("AccountableEntities")]
public abstract class AccountableEntity // Party root (TPH)
{
	public string Id { get; set; }
	public string DisplayName { get; set; }
	public string TimeZoneId { get; set; }
	public string Discriminator { get; private set; } // UserAccount | OrgAccount
}

public class UserAccount : AccountableEntity
{
	public string? ApplicationUserId { get; set; } // null => no login
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string Email { get; set; }
}

public class OrgAccount : AccountableEntity
{
	public string LegalName { get; set; }
	public string RegistrationNumber { get; set; }
}

[Table("ApplicationUsers")] // login identity (optional)
public class ApplicationUser
{
	public string Id { get; set; }               // identity user key
	public string AccountableEntityId { get; set; } // must point to a UserAccount row
	public UserAccount UserAccount { get; set; }
}
```

EF Core mapping

```csharp
modelBuilder.Entity<AccountableEntity>()
	.HasDiscriminator<string>("Discriminator")
	.HasValue<UserAccount>("UserAccount")
	.HasValue<OrgAccount>("OrgAccount");

modelBuilder.Entity<ApplicationUser>()
	.HasOne(x => x.UserAccount)
	.WithOne()
	.HasForeignKey<ApplicationUser>(x => x.AccountableEntityId)
	.OnDelete(DeleteBehavior.Cascade);
```

Accountability using AccountableEntityId

```csharp
[Table("Accountabilities")]
[Index(nameof(CommissionerAccountableEntityId), nameof(ResponsibleAccountableEntityId), nameof(AccountabilityType))]
public class Accountability
{
	public string Id { get; set; }
	public string CommissionerAccountableEntityId { get; set; }
	public string ResponsibleAccountableEntityId { get; set; }
	public string AccountabilityType { get; set; } // ServiceProvider | Customer | StaffMember | WorksFor | BooksOnBehalf ...
	public DateTimeOffset? EffectiveFromUtc { get; set; }
	public DateTimeOffset? EffectiveToUtc { get; set; }
}
```

Entity references (replace UserAccountId with AccountableEntityId)

```csharp
public partial class TimeSlot
{
	public string ResourceAccountableEntityId { get; protected set; }
	public AccountableEntity Resource { get; protected set; } // typically UserAccount
}

public partial class Appointment
{
	public string CommissionerAccountableEntityId { get; protected set; }  // customer (UserAccount or OrgAccount)
	public string ResponsibleAccountableEntityId { get; protected set; }   // provider (UserAccount)
	public string? BookedByAccountableEntityId { get; protected set; }     // staff/customer/provider
	public AccountableEntity Commissioner { get; protected set; }
	public AccountableEntity Responsible { get; protected set; }
	public AccountableEntity BookedBy { get; protected set; }
}

public partial class AvailabilitySchedule
{
	public string ResourceAccountableEntityId { get; protected set; }
	public AccountableEntity Resource { get; protected set; }
}
```

Validation with AccountabilityType

```csharp
bool IsServiceProvider(string aeId, DateTimeOffset nowUtc) =>
	_db.Accountabilities.Any(a => a.ResponsibleAccountableEntityId == aeId && a.AccountabilityType == "ServiceProvider" &&
		(!a.EffectiveFromUtc.HasValue || a.EffectiveFromUtc <= nowUtc) &&
		(!a.EffectiveToUtc.HasValue || a.EffectiveToUtc >= nowUtc));

bool CanBookOnBehalf(string staffAeId, string providerAeId, DateTimeOffset nowUtc) =>
	_db.Accountabilities.Any(a => a.CommissionerAccountableEntityId == staffAeId && a.ResponsibleAccountableEntityId == providerAeId &&
		(a.AccountabilityType == "WorksFor" || a.AccountabilityType == "BooksOnBehalf") &&
		(!a.EffectiveFromUtc.HasValue || a.EffectiveFromUtc <= nowUtc) &&
		(!a.EffectiveToUtc.HasValue || a.EffectiveToUtc >= nowUtc));
```

Calendar alignment

* `CalendarIndex.ResourceType = "AccountableEntity"`, `ResourceId = AccountableEntity.Id`
* Works for both people (UserAccount) and organizations (OrgAccount) uniformly; assets remain separate unless unified later

This update matches your hierarchy precisely: `UserAccount` and `OrgAccount` are concrete subtypes of a single `AccountableEntity` root; all foreign keys and accountabilities use `AccountableEntityId` consistently.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.flexbase.in/sample-references-domain/scheduling-app.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
