From 53c1ee77f351a6d61faf7d54bb9b1dd4357a49e1 Mon Sep 17 00:00:00 2001 From: Pavlichek Date: Tue, 24 Feb 2026 21:23:14 -0500 Subject: [PATCH 1/6] docs: add spec and codegen evidence log --- Booking/booking.spec.md | 36 +++++++++++++++++++++++++++++++++ Booking/codegen-log.md | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 Booking/booking.spec.md create mode 100644 Booking/codegen-log.md diff --git a/Booking/booking.spec.md b/Booking/booking.spec.md new file mode 100644 index 00000000..15f1d3c3 --- /dev/null +++ b/Booking/booking.spec.md @@ -0,0 +1,36 @@ +# Booking Spec (Spec-Driven) + +## Feature: Appointment booking + +### Domain rules +1) Appointment has StartUtc and EndUtc (UTC). +2) EndUtc must be after StartUtc. +3) Minimum duration: 15 minutes. +4) Cannot book in the past: StartUtc < UtcNow is invalid. +5) No overlaps allowed: + Overlap exists when: newStart < existingEnd AND newEnd > existingStart. + +### Data model +Appointment: +- id: guid +- startUtc: datetime (UTC) +- endUtc: datetime (UTC) +- customer: string + +### API +POST /appointments +Request JSON: +{ + "startUtc": "2026-02-24T18:00:00Z", + "endUtc": "2026-02-24T18:30:00Z", + "customer": "John" +} + +Responses: +- 200 OK: returns created Appointment +- 400 BadRequest: validation errors (range, duration, past, empty customer) +- 409 Conflict: overlaps an existing appointment + +GET /appointments +Responses: +- 200 OK: returns list of Appointment \ No newline at end of file diff --git a/Booking/codegen-log.md b/Booking/codegen-log.md new file mode 100644 index 00000000..1f19c5e3 --- /dev/null +++ b/Booking/codegen-log.md @@ -0,0 +1,45 @@ +# Codegen Log (Evidence) + +## Tools used +- Visual Studio 2026 +- GitHub Copilot (and/or ChatGPT GPT-5.2 / Codex) + +## Workflow +1) Wrote the spec first: booking.spec.md +2) Used codegen to scaffold: domain model, repo interface, service, API endpoints, tests +3) Reviewed and refined generated code to match the spec exactly + +## Prompts (copy/paste) +### P1 - Domain model + repo interface +"Generate C# code for Booking.Core: +- Appointment record (Id, StartUtc, EndUtc, Customer) +- IAppointmentRepository with AddAsync, GetAllAsync, ExistsOverlapAsync +based on booking.spec.md" + +### P2 - Domain service +"Generate BookingService enforcing spec rules: +- end after start +- min 15 min +- not in past using injectable time provider +- no overlap using repository +Throw ArgumentException for validation and InvalidOperationException for conflict" + +### P3 - In-memory repo +"Generate InMemoryAppointmentRepository implementing IAppointmentRepository with overlap check: +newStart < existingEnd && newEnd > existingStart +Use thread-safety." + +### P4 - Minimal API endpoints +"Generate Minimal API endpoints in Booking.Api Program.cs: +POST /appointments and GET /appointments +Return 400 for validation, 409 for overlap, 200 for success. +Include Swagger." + +### P5 - Tests from spec +"Generate xUnit tests using FluentAssertions: +- valid booking +- past booking rejected +- invalid range rejected +- too short rejected +- overlap rejected +- boundary touching allowed (end == next start)" \ No newline at end of file From 87b49b6b279b1da621dea48c976cc4884196970f Mon Sep 17 00:00:00 2001 From: Pavlichek Date: Wed, 25 Feb 2026 03:21:30 -0500 Subject: [PATCH 2/6] core: add domain model, rules, and booking service --- Booking/Booking.Core/Appointment.cs | 8 +++ Booking/Booking.Core/Booking.Core.csproj | 9 ++++ Booking/Booking.Core/BookingErrors.cs | 10 ++++ Booking/Booking.Core/BookingService.cs | 50 +++++++++++++++++++ .../Booking.Core/IAppointmentRepository.cs | 8 +++ Booking/Booking.Core/TimeProvider.cs | 11 ++++ 6 files changed, 96 insertions(+) create mode 100644 Booking/Booking.Core/Appointment.cs create mode 100644 Booking/Booking.Core/Booking.Core.csproj create mode 100644 Booking/Booking.Core/BookingErrors.cs create mode 100644 Booking/Booking.Core/BookingService.cs create mode 100644 Booking/Booking.Core/IAppointmentRepository.cs create mode 100644 Booking/Booking.Core/TimeProvider.cs diff --git a/Booking/Booking.Core/Appointment.cs b/Booking/Booking.Core/Appointment.cs new file mode 100644 index 00000000..bac3399f --- /dev/null +++ b/Booking/Booking.Core/Appointment.cs @@ -0,0 +1,8 @@ +namespace Booking.Core; + +public sealed record Appointment( + Guid Id, + DateTime StartUtc, + DateTime EndUtc, + string Customer +); \ No newline at end of file diff --git a/Booking/Booking.Core/Booking.Core.csproj b/Booking/Booking.Core/Booking.Core.csproj new file mode 100644 index 00000000..b7601447 --- /dev/null +++ b/Booking/Booking.Core/Booking.Core.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Booking/Booking.Core/BookingErrors.cs b/Booking/Booking.Core/BookingErrors.cs new file mode 100644 index 00000000..2ce34b85 --- /dev/null +++ b/Booking/Booking.Core/BookingErrors.cs @@ -0,0 +1,10 @@ +namespace Booking.Core; + +public static class BookingErrors +{ + public const string CustomerRequired = "Customer is required."; + public const string InvalidRange = "EndUtc must be after StartUtc."; + public const string TooShort = "Minimum duration is 15 minutes."; + public const string InPast = "Cannot book an appointment in the past."; + public const string Conflict = "Appointment time conflicts with an existing booking."; +} \ No newline at end of file diff --git a/Booking/Booking.Core/BookingService.cs b/Booking/Booking.Core/BookingService.cs new file mode 100644 index 00000000..81d8b756 --- /dev/null +++ b/Booking/Booking.Core/BookingService.cs @@ -0,0 +1,50 @@ +namespace Booking.Core; + +public sealed class BookingService +{ + private readonly IAppointmentRepository _repo; + private readonly ITimeProvider _time; + + public BookingService(IAppointmentRepository repo, ITimeProvider time) + { + _repo = repo; + _time = time; + } + + public async Task CreateAsync( + DateTime startUtc, + DateTime endUtc, + string customer, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(customer)) + throw new ArgumentException(BookingErrors.CustomerRequired); + + // (Spec) End must be after Start + if (endUtc <= startUtc) + throw new ArgumentException(BookingErrors.InvalidRange); + + // (Spec) Min duration 15 minutes + if (endUtc - startUtc < TimeSpan.FromMinutes(15)) + throw new ArgumentException(BookingErrors.TooShort); + + // (Spec) Cannot book in the past + if (startUtc < _time.UtcNow) + throw new ArgumentException(BookingErrors.InPast); + + // (Spec) No overlap allowed + var conflict = await _repo.ExistsOverlapAsync(startUtc, endUtc, ct); + if (conflict) + throw new InvalidOperationException(BookingErrors.Conflict); + + var appointment = new Appointment( + Id: Guid.NewGuid(), + StartUtc: startUtc, + EndUtc: endUtc, + Customer: customer.Trim() + ); + + await _repo.AddAsync(appointment, ct); + return appointment; + } +} \ No newline at end of file diff --git a/Booking/Booking.Core/IAppointmentRepository.cs b/Booking/Booking.Core/IAppointmentRepository.cs new file mode 100644 index 00000000..36c93a4a --- /dev/null +++ b/Booking/Booking.Core/IAppointmentRepository.cs @@ -0,0 +1,8 @@ +namespace Booking.Core; + +public interface IAppointmentRepository +{ + Task AddAsync(Appointment appointment, CancellationToken ct = default); + Task> GetAllAsync(CancellationToken ct = default); + Task ExistsOverlapAsync(DateTime startUtc, DateTime endUtc, CancellationToken ct = default); +} \ No newline at end of file diff --git a/Booking/Booking.Core/TimeProvider.cs b/Booking/Booking.Core/TimeProvider.cs new file mode 100644 index 00000000..5f61436e --- /dev/null +++ b/Booking/Booking.Core/TimeProvider.cs @@ -0,0 +1,11 @@ +namespace Booking.Core; + +public interface ITimeProvider +{ + DateTime UtcNow { get; } +} + +public sealed class SystemTimeProvider : ITimeProvider +{ + public DateTime UtcNow => DateTime.UtcNow; +} \ No newline at end of file From 245bd5bb0fed3a1463cbffa5b4589fd26b49401d Mon Sep 17 00:00:00 2001 From: Pavlichek Date: Wed, 25 Feb 2026 03:27:31 -0500 Subject: [PATCH 3/6] infra: add in-memory repository and wire up DI --- Booking/Booking.Api/Booking.Api.csproj | 18 +++++++ Booking/Booking.Api/Program.cs | 48 +++++++++++++++++++ .../Booking.Infrastructure.csproj | 13 +++++ .../InMemoryAppointmentRepository.cs | 37 ++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 Booking/Booking.Api/Booking.Api.csproj create mode 100644 Booking/Booking.Api/Program.cs create mode 100644 Booking/Booking.Infrastructure/Booking.Infrastructure.csproj create mode 100644 Booking/Booking.Infrastructure/InMemoryAppointmentRepository.cs diff --git a/Booking/Booking.Api/Booking.Api.csproj b/Booking/Booking.Api/Booking.Api.csproj new file mode 100644 index 00000000..422d48bb --- /dev/null +++ b/Booking/Booking.Api/Booking.Api.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/Booking/Booking.Api/Program.cs b/Booking/Booking.Api/Program.cs new file mode 100644 index 00000000..d32ab598 --- /dev/null +++ b/Booking/Booking.Api/Program.cs @@ -0,0 +1,48 @@ +using Booking.Core; +using Booking.Infrastructure; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast"); + +app.Run(); + +internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/Booking/Booking.Infrastructure/Booking.Infrastructure.csproj b/Booking/Booking.Infrastructure/Booking.Infrastructure.csproj new file mode 100644 index 00000000..eaba4021 --- /dev/null +++ b/Booking/Booking.Infrastructure/Booking.Infrastructure.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/Booking/Booking.Infrastructure/InMemoryAppointmentRepository.cs b/Booking/Booking.Infrastructure/InMemoryAppointmentRepository.cs new file mode 100644 index 00000000..c292374c --- /dev/null +++ b/Booking/Booking.Infrastructure/InMemoryAppointmentRepository.cs @@ -0,0 +1,37 @@ +using Booking.Core; + +namespace Booking.Infrastructure; + +public sealed class InMemoryAppointmentRepository : IAppointmentRepository +{ + private readonly List _items = new(); + private readonly object _lock = new(); + + public Task AddAsync(Appointment appointment, CancellationToken ct = default) + { + lock (_lock) + { + _items.Add(appointment); + } + return Task.CompletedTask; + } + + public Task> GetAllAsync(CancellationToken ct = default) + { + lock (_lock) + { + return Task.FromResult((IReadOnlyList)_items.ToList()); + } + } + + public Task ExistsOverlapAsync(DateTime startUtc, DateTime endUtc, CancellationToken ct = default) + { + lock (_lock) + { + // Spec overlap rule: + // overlap when newStart < existingEnd AND newEnd > existingStart + var exists = _items.Any(a => startUtc < a.EndUtc && endUtc > a.StartUtc); + return Task.FromResult(exists); + } + } +} \ No newline at end of file From 99a88f237e5ab55890cc0e92f9013d4742e7231c Mon Sep 17 00:00:00 2001 From: Pavlichek Date: Wed, 25 Feb 2026 03:49:16 -0500 Subject: [PATCH 4/6] api: add appointments endpoints with validation and conflict handling --- Booking/Booking.Api/Booking.Api.csproj | 1 + .../Contracts/CreateAppointmentRequest.cs | 7 ++ Booking/Booking.Api/Program.cs | 69 ++++++++++++------- 3 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 Booking/Booking.Api/Contracts/CreateAppointmentRequest.cs diff --git a/Booking/Booking.Api/Booking.Api.csproj b/Booking/Booking.Api/Booking.Api.csproj index 422d48bb..3cf96a68 100644 --- a/Booking/Booking.Api/Booking.Api.csproj +++ b/Booking/Booking.Api/Booking.Api.csproj @@ -8,6 +8,7 @@ + diff --git a/Booking/Booking.Api/Contracts/CreateAppointmentRequest.cs b/Booking/Booking.Api/Contracts/CreateAppointmentRequest.cs new file mode 100644 index 00000000..62121fe6 --- /dev/null +++ b/Booking/Booking.Api/Contracts/CreateAppointmentRequest.cs @@ -0,0 +1,7 @@ +namespace Booking.Api.Contracts; + +public sealed record CreateAppointmentRequest( + DateTime StartUtc, + DateTime EndUtc, + string Customer +); \ No newline at end of file diff --git a/Booking/Booking.Api/Program.cs b/Booking/Booking.Api/Program.cs index d32ab598..fab1c69e 100644 --- a/Booking/Booking.Api/Program.cs +++ b/Booking/Booking.Api/Program.cs @@ -1,48 +1,69 @@ +using Booking.Api.Contracts; using Booking.Core; using Booking.Infrastructure; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +// OpenAPI + Swagger UI builder.Services.AddOpenApi(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +// DI builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); var app = builder.Build(); -// Configure the HTTP request pipeline. +// Swagger UI in Development if (app.Environment.IsDevelopment()) { - app.MapOpenApi(); + app.UseSwagger(); + app.UseSwaggerUI(); } app.UseHttpsRedirection(); -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; +// ------------------------- +// Endpoints (per spec) +// ------------------------- -app.MapGet("/weatherforecast", () => +app.MapPost("/appointments", async ( + CreateAppointmentRequest req, + BookingService service, + CancellationToken ct) => { - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast"); + try + { + var created = await service.CreateAsync( + startUtc: req.StartUtc, + endUtc: req.EndUtc, + customer: req.Customer, + ct: ct); -app.Run(); + return Results.Ok(created); + } + catch (ArgumentException ex) + { + return Results.BadRequest(new { error = ex.Message }); + } + catch (InvalidOperationException ex) when (ex.Message == BookingErrors.Conflict) + { + return Results.Conflict(new { error = ex.Message }); + } +}) +.WithName("CreateAppointment") +.WithOpenApi(); -internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +app.MapGet("/appointments", async ( + IAppointmentRepository repo, + CancellationToken ct) => { - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} + var items = await repo.GetAllAsync(ct); + return Results.Ok(items); +}) +.WithName("GetAppointments") +.WithOpenApi(); + +app.Run(); \ No newline at end of file From c005da6f92d188f2f00f604840902b70313a15fc Mon Sep 17 00:00:00 2001 From: Pavlichek Date: Wed, 25 Feb 2026 03:51:59 -0500 Subject: [PATCH 5/6] tests: add booking service unit tests from spec --- Booking/Booking.Tests/Booking.Tests.csproj | 27 ++++ Booking/Booking.Tests/BookingServiceTests.cs | 138 +++++++++++++++++++ Booking/Booking.Tests/FakeTimeProvider.cs | 8 ++ 3 files changed, 173 insertions(+) create mode 100644 Booking/Booking.Tests/Booking.Tests.csproj create mode 100644 Booking/Booking.Tests/BookingServiceTests.cs create mode 100644 Booking/Booking.Tests/FakeTimeProvider.cs diff --git a/Booking/Booking.Tests/Booking.Tests.csproj b/Booking/Booking.Tests/Booking.Tests.csproj new file mode 100644 index 00000000..86519048 --- /dev/null +++ b/Booking/Booking.Tests/Booking.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Booking/Booking.Tests/BookingServiceTests.cs b/Booking/Booking.Tests/BookingServiceTests.cs new file mode 100644 index 00000000..5deeff87 --- /dev/null +++ b/Booking/Booking.Tests/BookingServiceTests.cs @@ -0,0 +1,138 @@ +using Booking.Core; +using Booking.Infrastructure; +using FluentAssertions; + +namespace Booking.Tests; + +public class BookingServiceTests +{ + private static (BookingService service, InMemoryAppointmentRepository repo, FakeTimeProvider time) CreateSut(DateTime nowUtc) + { + var repo = new InMemoryAppointmentRepository(); + var time = new FakeTimeProvider { UtcNow = nowUtc }; + var service = new BookingService(repo, time); + return (service, repo, time); + } + + [Fact] + public async Task CreateAsync_Should_Create_Appointment_When_Valid() + { + var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc); + var (service, repo, _) = CreateSut(now); + + var start = now.AddHours(2); + var end = start.AddMinutes(30); + + var appt = await service.CreateAsync(start, end, "John"); + + appt.Id.Should().NotBeEmpty(); + appt.Customer.Should().Be("John"); + appt.StartUtc.Should().Be(start); + appt.EndUtc.Should().Be(end); + + var all = await repo.GetAllAsync(); + all.Should().HaveCount(1); + } + + [Fact] + public async Task CreateAsync_Should_Reject_Empty_Customer() + { + var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc); + var (service, _, _) = CreateSut(now); + + var start = now.AddHours(1); + var end = start.AddMinutes(30); + + Func act = () => service.CreateAsync(start, end, " "); + + await act.Should().ThrowAsync() + .WithMessage(BookingErrors.CustomerRequired); + } + + [Fact] + public async Task CreateAsync_Should_Reject_Invalid_Range_When_End_Before_Or_Equal_Start() + { + var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc); + var (service, _, _) = CreateSut(now); + + var start = now.AddHours(1); + var end = start; // equal + + Func act = () => service.CreateAsync(start, end, "A"); + + await act.Should().ThrowAsync() + .WithMessage(BookingErrors.InvalidRange); + } + + [Fact] + public async Task CreateAsync_Should_Reject_Too_Short_Duration() + { + var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc); + var (service, _, _) = CreateSut(now); + + var start = now.AddHours(1); + var end = start.AddMinutes(10); + + Func act = () => service.CreateAsync(start, end, "A"); + + await act.Should().ThrowAsync() + .WithMessage(BookingErrors.TooShort); + } + + [Fact] + public async Task CreateAsync_Should_Reject_Start_In_Past() + { + var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc); + var (service, _, _) = CreateSut(now); + + var start = now.AddMinutes(-1); + var end = now.AddMinutes(30); + + Func act = () => service.CreateAsync(start, end, "A"); + + await act.Should().ThrowAsync() + .WithMessage(BookingErrors.InPast); + } + + [Fact] + public async Task CreateAsync_Should_Reject_Overlap() + { + var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc); + var (service, _, _) = CreateSut(now); + + var start1 = now.AddHours(1); + var end1 = start1.AddMinutes(30); + + await service.CreateAsync(start1, end1, "A"); + + // overlaps: starts inside existing + Func act = () => service.CreateAsync( + start1.AddMinutes(10), + end1.AddMinutes(10), + "B"); + + await act.Should().ThrowAsync() + .WithMessage(BookingErrors.Conflict); + } + + [Fact] + public async Task CreateAsync_Should_Allow_Boundary_Touching_End_Equals_Next_Start() + { + var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc); + var (service, repo, _) = CreateSut(now); + + var start1 = now.AddHours(1); + var end1 = start1.AddMinutes(30); + + await service.CreateAsync(start1, end1, "A"); + + // boundary touch: start == existing end => NOT overlap per spec + var start2 = end1; + var end2 = start2.AddMinutes(30); + + var appt2 = await service.CreateAsync(start2, end2, "B"); + + appt2.Customer.Should().Be("B"); + (await repo.GetAllAsync()).Should().HaveCount(2); + } +} \ No newline at end of file diff --git a/Booking/Booking.Tests/FakeTimeProvider.cs b/Booking/Booking.Tests/FakeTimeProvider.cs new file mode 100644 index 00000000..df2fdc09 --- /dev/null +++ b/Booking/Booking.Tests/FakeTimeProvider.cs @@ -0,0 +1,8 @@ +using Booking.Core; + +namespace Booking.Tests; + +public sealed class FakeTimeProvider : ITimeProvider +{ + public DateTime UtcNow { get; set; } +} \ No newline at end of file From 762808cb2ae556834422d45db52b1e0b0e3e5d45 Mon Sep 17 00:00:00 2001 From: Pavlichek Date: Wed, 25 Feb 2026 03:58:00 -0500 Subject: [PATCH 6/6] docs: add project readme and run instructions --- Booking/README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 Booking/README.md diff --git a/Booking/README.md b/Booking/README.md new file mode 100644 index 00000000..bb37e498 --- /dev/null +++ b/Booking/README.md @@ -0,0 +1,74 @@ +# Booking API (Spec-Driven + Codegen + Tests) + +A small appointment booking API built using a spec-driven workflow and code generation tools. + +## What it does (workflow) + +- Create an appointment (POST /appointments) +- View appointments (GET /appointments) +- Enforces domain rules: + - EndUtc must be after StartUtc + - Minimum duration is 15 minutes + - Cannot book in the past + - No overlapping appointments + +Data is stored in-memory (no database required). + +## Spec and codegen evidence + +- Spec: `Booking/booking.spec.md` +- Codegen log: `Booking/docs/codegen-log.md` + +## Tech + +- .NET 10 Minimal API +- xUnit + FluentAssertions +- Swagger UI + +## Run the API locally + +From the repo root: + +```bash +dotnet run --project Booking/Booking.Api +``` + +Open Swagger UI: + +``` +https://localhost:xxxx/swagger +``` + +(The exact port is printed in the console) + +## Example requests + +### Create appointment + +POST /appointments + +```json +{ + "startUtc": "2026-03-01T18:00:00Z", + "endUtc": "2026-03-01T18:30:00Z", + "customer": "John" +} +``` + +### List appointments + +GET /appointments + +## Run tests + +From the repo root: + +```bash +dotnet test +``` + +## Notes + +- No secrets/credentials are used or required. +- Business rules are implemented in Booking.Core. +- Data storage is in-memory for simplicity. \ No newline at end of file