From 119f23ca66b9b754b9a31f4ed4bc1990a3c1cad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Tue, 31 Mar 2026 16:18:04 -0600 Subject: [PATCH] feat(kitchen-service): expand kitchen workflow contracts --- Kitchen.Service.slnx | 3 + docs/api/internal-kitchen-workflows.md | 27 ++++++ docs/roadmap/feature-epics.md | 1 + docs/runbooks/containerization.md | 2 + .../Ports/DefaultKitchenWorkflowPort.cs | 88 +++++++++++++++++++ .../Ports/IKitchenWorkflowPort.cs | 12 +++ .../UseCases/ClaimKitchenWorkItemUseCase.cs | 13 +++ .../UseCases/GetKitchenBoardUseCase.cs | 13 +++ .../UseCases/IClaimKitchenWorkItemUseCase.cs | 9 ++ .../UseCases/IGetKitchenBoardUseCase.cs | 9 ++ .../UseCases/IUpdateKitchenPriorityUseCase.cs | 9 ++ .../UseCases/UpdateKitchenPriorityUseCase.cs | 13 +++ .../Contracts/KitchenBoardItemContract.cs | 12 +++ .../Contracts/KitchenBoardLaneContract.cs | 5 ++ .../Requests/ClaimKitchenWorkItemRequest.cs | 6 ++ .../Requests/GetKitchenBoardRequest.cs | 3 + .../Requests/UpdateKitchenPriorityRequest.cs | 7 ++ .../Responses/ClaimKitchenWorkItemResponse.cs | 8 ++ .../Responses/GetKitchenBoardResponse.cs | 9 ++ .../UpdateKitchenPriorityResponse.cs | 7 ++ src/Kitchen.Service.Grpc/Program.cs | 28 ++++++ ...tchen.Service.Application.UnitTests.csproj | 19 ++++ .../KitchenWorkflowUseCasesTests.cs | 60 +++++++++++++ 23 files changed, 363 insertions(+) create mode 100644 docs/api/internal-kitchen-workflows.md create mode 100644 src/Kitchen.Service.Application/Ports/DefaultKitchenWorkflowPort.cs create mode 100644 src/Kitchen.Service.Application/Ports/IKitchenWorkflowPort.cs create mode 100644 src/Kitchen.Service.Application/UseCases/ClaimKitchenWorkItemUseCase.cs create mode 100644 src/Kitchen.Service.Application/UseCases/GetKitchenBoardUseCase.cs create mode 100644 src/Kitchen.Service.Application/UseCases/IClaimKitchenWorkItemUseCase.cs create mode 100644 src/Kitchen.Service.Application/UseCases/IGetKitchenBoardUseCase.cs create mode 100644 src/Kitchen.Service.Application/UseCases/IUpdateKitchenPriorityUseCase.cs create mode 100644 src/Kitchen.Service.Application/UseCases/UpdateKitchenPriorityUseCase.cs create mode 100644 src/Kitchen.Service.Contracts/Contracts/KitchenBoardItemContract.cs create mode 100644 src/Kitchen.Service.Contracts/Contracts/KitchenBoardLaneContract.cs create mode 100644 src/Kitchen.Service.Contracts/Requests/ClaimKitchenWorkItemRequest.cs create mode 100644 src/Kitchen.Service.Contracts/Requests/GetKitchenBoardRequest.cs create mode 100644 src/Kitchen.Service.Contracts/Requests/UpdateKitchenPriorityRequest.cs create mode 100644 src/Kitchen.Service.Contracts/Responses/ClaimKitchenWorkItemResponse.cs create mode 100644 src/Kitchen.Service.Contracts/Responses/GetKitchenBoardResponse.cs create mode 100644 src/Kitchen.Service.Contracts/Responses/UpdateKitchenPriorityResponse.cs create mode 100644 tests/Kitchen.Service.Application.UnitTests/Kitchen.Service.Application.UnitTests.csproj create mode 100644 tests/Kitchen.Service.Application.UnitTests/KitchenWorkflowUseCasesTests.cs diff --git a/Kitchen.Service.slnx b/Kitchen.Service.slnx index aa32480..c4934ae 100644 --- a/Kitchen.Service.slnx +++ b/Kitchen.Service.slnx @@ -4,4 +4,7 @@ + + + diff --git a/docs/api/internal-kitchen-workflows.md b/docs/api/internal-kitchen-workflows.md new file mode 100644 index 0000000..8c2613a --- /dev/null +++ b/docs/api/internal-kitchen-workflows.md @@ -0,0 +1,27 @@ +# Internal Kitchen Workflow Contracts + +## Purpose + +`kitchen-service` now exposes a board-oriented internal workflow surface that the kitchen-ops BFF can consume directly. + +## Endpoint Surface + +- `GET /internal/kitchen/queue?queueName=&limit=` +- `POST /internal/kitchen/orders/transition` +- `GET /internal/kitchen/board?contextId=` +- `POST /internal/kitchen/work-items/claim` +- `POST /internal/kitchen/work-items/priority` + +## Contract Depth Added In Stage 41 + +The new kitchen workflow contracts add enough shape for downstream BFF and SPA work: + +- board lanes with per-item station, claim, ETA, and priority details +- explicit claim/release-ready work-item ownership responses +- dedicated priority update responses separate from generic state transitions +- existing transition contract kept in place for order-state changes + +## Current Runtime Shape + +- The default implementation remains deterministic and in-memory. +- This repo still focuses on orchestration and contract shape, not kitchen persistence realism. diff --git a/docs/roadmap/feature-epics.md b/docs/roadmap/feature-epics.md index 2c68d06..a0fc000 100644 --- a/docs/roadmap/feature-epics.md +++ b/docs/roadmap/feature-epics.md @@ -13,6 +13,7 @@ kitchen-service - Kitchen queue and dispatch optimization hooks. - Operations control-plane policies (flags, service windows, overrides). - POS closeout and settlement summary alignment. +- Kitchen board lanes, claim ownership, and priority updates aligned to operator workflows. ## Documentation Contract Any code change in this repository must include docs updates in the same branch. diff --git a/docs/runbooks/containerization.md b/docs/runbooks/containerization.md index 8f84638..266e7ee 100644 --- a/docs/runbooks/containerization.md +++ b/docs/runbooks/containerization.md @@ -23,6 +23,7 @@ docker run --rm -p 8080:8080 --name kitchen-service agilewebs/kitchen-service:de ## Runtime Notes - Exposes internal queue and order state transition endpoints. +- Also exposes board, work-item claim, and priority update endpoints for the kitchen-ops BFF flow. ## Health Endpoint Consistency @@ -38,3 +39,4 @@ docker run --rm -p 8080:8080 --name kitchen-service agilewebs/kitchen-service:de - Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior. - Demo PostgreSQL seeds validate integration contracts and smoke determinism, but do not yet imply full persistence implementation parity. +- Stage 41 adds kitchen workflow contract depth first; downstream BFFs still need to adopt these endpoints before richer board behavior reaches the web app. diff --git a/src/Kitchen.Service.Application/Ports/DefaultKitchenWorkflowPort.cs b/src/Kitchen.Service.Application/Ports/DefaultKitchenWorkflowPort.cs new file mode 100644 index 0000000..f0675b1 --- /dev/null +++ b/src/Kitchen.Service.Application/Ports/DefaultKitchenWorkflowPort.cs @@ -0,0 +1,88 @@ +using Kitchen.Service.Contracts.Contracts; +using Kitchen.Service.Contracts.Requests; +using Kitchen.Service.Contracts.Responses; + +namespace Kitchen.Service.Application.Ports; + +public sealed class DefaultKitchenWorkflowPort : IKitchenWorkflowPort +{ + public Task GetKitchenBoardAsync(GetKitchenBoardRequest request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var lanes = new[] + { + new KitchenBoardLaneContract( + "queued", + new[] + { + new KitchenBoardItemContract("WK-1001", "CO-1001", "KT-1001", "T-08", "hot-line", "Queued", 3, null, 12), + new KitchenBoardItemContract("WK-1002", "CO-1003", "KT-1002", "T-12", "expedite", "Queued", 2, null, 8) + }), + new KitchenBoardLaneContract( + "preparing", + new[] + { + new KitchenBoardItemContract("WK-1003", "CO-1002", "KT-1003", "T-15", "grill", "Preparing", 4, "chef-maya", 5) + }), + new KitchenBoardLaneContract( + "ready", + new[] + { + new KitchenBoardItemContract("WK-1004", "CO-0999", "KT-0999", "T-21", "pickup", "ReadyForPickup", 1, "expo-noah", 0) + }) + }; + + return Task.FromResult(new GetKitchenBoardResponse( + request.ContextId, + "Kitchen board shows queued, preparing, and ready lanes for the current service context.", + lanes, + new[] { "hot-line", "grill", "salad", "pickup", "expedite" })); + } + + public Task ClaimKitchenWorkItemAsync(ClaimKitchenWorkItemRequest request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var claimed = !string.IsNullOrWhiteSpace(request.ContextId) && + !string.IsNullOrWhiteSpace(request.WorkItemId) && + !string.IsNullOrWhiteSpace(request.ClaimedBy); + + return Task.FromResult(new ClaimKitchenWorkItemResponse( + request.ContextId, + request.WorkItemId, + claimed, + request.ClaimedBy, + claimed + ? $"Work item {request.WorkItemId} claimed by {request.ClaimedBy}." + : "Kitchen work-item claim is incomplete.")); + } + + public Task UpdateKitchenPriorityAsync(UpdateKitchenPriorityRequest request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var priority = Math.Max(1, request.Priority); + return Task.FromResult(new UpdateKitchenPriorityResponse( + request.ContextId, + request.WorkItemId, + priority, + $"Priority for {request.WorkItemId} updated to {priority} by {request.RequestedBy}.")); + } + + public Task TransitionKitchenOrderStateAsync(TransitionKitchenOrderStateRequest request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var previous = "Queued"; + var allowed = request.TargetState is "Preparing" or "ReadyForPickup" or "Canceled" or "Plated"; + + return Task.FromResult(new TransitionKitchenOrderStateResponse( + request.OrderId, + request.TicketId, + previous, + allowed ? request.TargetState : previous, + allowed, + allowed ? null : "Target state is not allowed by kitchen-service policy.")); + } +} diff --git a/src/Kitchen.Service.Application/Ports/IKitchenWorkflowPort.cs b/src/Kitchen.Service.Application/Ports/IKitchenWorkflowPort.cs new file mode 100644 index 0000000..e14cb91 --- /dev/null +++ b/src/Kitchen.Service.Application/Ports/IKitchenWorkflowPort.cs @@ -0,0 +1,12 @@ +using Kitchen.Service.Contracts.Requests; +using Kitchen.Service.Contracts.Responses; + +namespace Kitchen.Service.Application.Ports; + +public interface IKitchenWorkflowPort +{ + Task GetKitchenBoardAsync(GetKitchenBoardRequest request, CancellationToken cancellationToken); + Task ClaimKitchenWorkItemAsync(ClaimKitchenWorkItemRequest request, CancellationToken cancellationToken); + Task UpdateKitchenPriorityAsync(UpdateKitchenPriorityRequest request, CancellationToken cancellationToken); + Task TransitionKitchenOrderStateAsync(TransitionKitchenOrderStateRequest request, CancellationToken cancellationToken); +} diff --git a/src/Kitchen.Service.Application/UseCases/ClaimKitchenWorkItemUseCase.cs b/src/Kitchen.Service.Application/UseCases/ClaimKitchenWorkItemUseCase.cs new file mode 100644 index 0000000..17120e6 --- /dev/null +++ b/src/Kitchen.Service.Application/UseCases/ClaimKitchenWorkItemUseCase.cs @@ -0,0 +1,13 @@ +using Kitchen.Service.Application.Ports; +using Kitchen.Service.Contracts.Requests; +using Kitchen.Service.Contracts.Responses; + +namespace Kitchen.Service.Application.UseCases; + +public sealed class ClaimKitchenWorkItemUseCase(IKitchenWorkflowPort workflowPort) : IClaimKitchenWorkItemUseCase +{ + public Task HandleAsync(ClaimKitchenWorkItemRequest request, CancellationToken cancellationToken) + { + return workflowPort.ClaimKitchenWorkItemAsync(request, cancellationToken); + } +} diff --git a/src/Kitchen.Service.Application/UseCases/GetKitchenBoardUseCase.cs b/src/Kitchen.Service.Application/UseCases/GetKitchenBoardUseCase.cs new file mode 100644 index 0000000..1c1bb0c --- /dev/null +++ b/src/Kitchen.Service.Application/UseCases/GetKitchenBoardUseCase.cs @@ -0,0 +1,13 @@ +using Kitchen.Service.Application.Ports; +using Kitchen.Service.Contracts.Requests; +using Kitchen.Service.Contracts.Responses; + +namespace Kitchen.Service.Application.UseCases; + +public sealed class GetKitchenBoardUseCase(IKitchenWorkflowPort workflowPort) : IGetKitchenBoardUseCase +{ + public Task HandleAsync(GetKitchenBoardRequest request, CancellationToken cancellationToken) + { + return workflowPort.GetKitchenBoardAsync(request, cancellationToken); + } +} diff --git a/src/Kitchen.Service.Application/UseCases/IClaimKitchenWorkItemUseCase.cs b/src/Kitchen.Service.Application/UseCases/IClaimKitchenWorkItemUseCase.cs new file mode 100644 index 0000000..9b80a98 --- /dev/null +++ b/src/Kitchen.Service.Application/UseCases/IClaimKitchenWorkItemUseCase.cs @@ -0,0 +1,9 @@ +using Kitchen.Service.Contracts.Requests; +using Kitchen.Service.Contracts.Responses; + +namespace Kitchen.Service.Application.UseCases; + +public interface IClaimKitchenWorkItemUseCase +{ + Task HandleAsync(ClaimKitchenWorkItemRequest request, CancellationToken cancellationToken); +} diff --git a/src/Kitchen.Service.Application/UseCases/IGetKitchenBoardUseCase.cs b/src/Kitchen.Service.Application/UseCases/IGetKitchenBoardUseCase.cs new file mode 100644 index 0000000..e394f6a --- /dev/null +++ b/src/Kitchen.Service.Application/UseCases/IGetKitchenBoardUseCase.cs @@ -0,0 +1,9 @@ +using Kitchen.Service.Contracts.Requests; +using Kitchen.Service.Contracts.Responses; + +namespace Kitchen.Service.Application.UseCases; + +public interface IGetKitchenBoardUseCase +{ + Task HandleAsync(GetKitchenBoardRequest request, CancellationToken cancellationToken); +} diff --git a/src/Kitchen.Service.Application/UseCases/IUpdateKitchenPriorityUseCase.cs b/src/Kitchen.Service.Application/UseCases/IUpdateKitchenPriorityUseCase.cs new file mode 100644 index 0000000..2a21ba8 --- /dev/null +++ b/src/Kitchen.Service.Application/UseCases/IUpdateKitchenPriorityUseCase.cs @@ -0,0 +1,9 @@ +using Kitchen.Service.Contracts.Requests; +using Kitchen.Service.Contracts.Responses; + +namespace Kitchen.Service.Application.UseCases; + +public interface IUpdateKitchenPriorityUseCase +{ + Task HandleAsync(UpdateKitchenPriorityRequest request, CancellationToken cancellationToken); +} diff --git a/src/Kitchen.Service.Application/UseCases/UpdateKitchenPriorityUseCase.cs b/src/Kitchen.Service.Application/UseCases/UpdateKitchenPriorityUseCase.cs new file mode 100644 index 0000000..1c03658 --- /dev/null +++ b/src/Kitchen.Service.Application/UseCases/UpdateKitchenPriorityUseCase.cs @@ -0,0 +1,13 @@ +using Kitchen.Service.Application.Ports; +using Kitchen.Service.Contracts.Requests; +using Kitchen.Service.Contracts.Responses; + +namespace Kitchen.Service.Application.UseCases; + +public sealed class UpdateKitchenPriorityUseCase(IKitchenWorkflowPort workflowPort) : IUpdateKitchenPriorityUseCase +{ + public Task HandleAsync(UpdateKitchenPriorityRequest request, CancellationToken cancellationToken) + { + return workflowPort.UpdateKitchenPriorityAsync(request, cancellationToken); + } +} diff --git a/src/Kitchen.Service.Contracts/Contracts/KitchenBoardItemContract.cs b/src/Kitchen.Service.Contracts/Contracts/KitchenBoardItemContract.cs new file mode 100644 index 0000000..c7ce3d6 --- /dev/null +++ b/src/Kitchen.Service.Contracts/Contracts/KitchenBoardItemContract.cs @@ -0,0 +1,12 @@ +namespace Kitchen.Service.Contracts.Contracts; + +public sealed record KitchenBoardItemContract( + string WorkItemId, + string OrderId, + string TicketId, + string TableId, + string Station, + string State, + int Priority, + string? ClaimedBy, + int EtaMinutes); diff --git a/src/Kitchen.Service.Contracts/Contracts/KitchenBoardLaneContract.cs b/src/Kitchen.Service.Contracts/Contracts/KitchenBoardLaneContract.cs new file mode 100644 index 0000000..5e5a768 --- /dev/null +++ b/src/Kitchen.Service.Contracts/Contracts/KitchenBoardLaneContract.cs @@ -0,0 +1,5 @@ +namespace Kitchen.Service.Contracts.Contracts; + +public sealed record KitchenBoardLaneContract( + string Lane, + IReadOnlyCollection Items); diff --git a/src/Kitchen.Service.Contracts/Requests/ClaimKitchenWorkItemRequest.cs b/src/Kitchen.Service.Contracts/Requests/ClaimKitchenWorkItemRequest.cs new file mode 100644 index 0000000..7ff19ce --- /dev/null +++ b/src/Kitchen.Service.Contracts/Requests/ClaimKitchenWorkItemRequest.cs @@ -0,0 +1,6 @@ +namespace Kitchen.Service.Contracts.Requests; + +public sealed record ClaimKitchenWorkItemRequest( + string ContextId, + string WorkItemId, + string ClaimedBy); diff --git a/src/Kitchen.Service.Contracts/Requests/GetKitchenBoardRequest.cs b/src/Kitchen.Service.Contracts/Requests/GetKitchenBoardRequest.cs new file mode 100644 index 0000000..e97e216 --- /dev/null +++ b/src/Kitchen.Service.Contracts/Requests/GetKitchenBoardRequest.cs @@ -0,0 +1,3 @@ +namespace Kitchen.Service.Contracts.Requests; + +public sealed record GetKitchenBoardRequest(string ContextId); diff --git a/src/Kitchen.Service.Contracts/Requests/UpdateKitchenPriorityRequest.cs b/src/Kitchen.Service.Contracts/Requests/UpdateKitchenPriorityRequest.cs new file mode 100644 index 0000000..c727a92 --- /dev/null +++ b/src/Kitchen.Service.Contracts/Requests/UpdateKitchenPriorityRequest.cs @@ -0,0 +1,7 @@ +namespace Kitchen.Service.Contracts.Requests; + +public sealed record UpdateKitchenPriorityRequest( + string ContextId, + string WorkItemId, + int Priority, + string RequestedBy); diff --git a/src/Kitchen.Service.Contracts/Responses/ClaimKitchenWorkItemResponse.cs b/src/Kitchen.Service.Contracts/Responses/ClaimKitchenWorkItemResponse.cs new file mode 100644 index 0000000..22fa38f --- /dev/null +++ b/src/Kitchen.Service.Contracts/Responses/ClaimKitchenWorkItemResponse.cs @@ -0,0 +1,8 @@ +namespace Kitchen.Service.Contracts.Responses; + +public sealed record ClaimKitchenWorkItemResponse( + string ContextId, + string WorkItemId, + bool Claimed, + string ClaimedBy, + string Message); diff --git a/src/Kitchen.Service.Contracts/Responses/GetKitchenBoardResponse.cs b/src/Kitchen.Service.Contracts/Responses/GetKitchenBoardResponse.cs new file mode 100644 index 0000000..04101a9 --- /dev/null +++ b/src/Kitchen.Service.Contracts/Responses/GetKitchenBoardResponse.cs @@ -0,0 +1,9 @@ +using Kitchen.Service.Contracts.Contracts; + +namespace Kitchen.Service.Contracts.Responses; + +public sealed record GetKitchenBoardResponse( + string ContextId, + string Summary, + IReadOnlyCollection Lanes, + IReadOnlyCollection AvailableStations); diff --git a/src/Kitchen.Service.Contracts/Responses/UpdateKitchenPriorityResponse.cs b/src/Kitchen.Service.Contracts/Responses/UpdateKitchenPriorityResponse.cs new file mode 100644 index 0000000..e6b6d57 --- /dev/null +++ b/src/Kitchen.Service.Contracts/Responses/UpdateKitchenPriorityResponse.cs @@ -0,0 +1,7 @@ +namespace Kitchen.Service.Contracts.Responses; + +public sealed record UpdateKitchenPriorityResponse( + string ContextId, + string WorkItemId, + int Priority, + string Message); diff --git a/src/Kitchen.Service.Grpc/Program.cs b/src/Kitchen.Service.Grpc/Program.cs index c54d8c0..e334832 100644 --- a/src/Kitchen.Service.Grpc/Program.cs +++ b/src/Kitchen.Service.Grpc/Program.cs @@ -4,8 +4,12 @@ using Kitchen.Service.Contracts.Requests; var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var app = builder.Build(); @@ -23,6 +27,30 @@ app.MapPost("/internal/kitchen/orders/transition", async ( return Results.Ok(await useCase.HandleAsync(request, ct)); }); +app.MapGet("/internal/kitchen/board", async ( + string contextId, + IGetKitchenBoardUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(new GetKitchenBoardRequest(contextId), ct)); +}); + +app.MapPost("/internal/kitchen/work-items/claim", async ( + ClaimKitchenWorkItemRequest request, + IClaimKitchenWorkItemUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(request, ct)); +}); + +app.MapPost("/internal/kitchen/work-items/priority", async ( + UpdateKitchenPriorityRequest request, + IUpdateKitchenPriorityUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(request, ct)); +}); + app.MapGet("/health", () => Results.Ok(new { status = "ok", service = "kitchen-service" })); app.MapGet("/healthz", () => Results.Ok(new { status = "ok", service = "kitchen-service" })); diff --git a/tests/Kitchen.Service.Application.UnitTests/Kitchen.Service.Application.UnitTests.csproj b/tests/Kitchen.Service.Application.UnitTests/Kitchen.Service.Application.UnitTests.csproj new file mode 100644 index 0000000..a11e802 --- /dev/null +++ b/tests/Kitchen.Service.Application.UnitTests/Kitchen.Service.Application.UnitTests.csproj @@ -0,0 +1,19 @@ + + + net10.0 + enable + enable + false + + + + + + + + + + + + + diff --git a/tests/Kitchen.Service.Application.UnitTests/KitchenWorkflowUseCasesTests.cs b/tests/Kitchen.Service.Application.UnitTests/KitchenWorkflowUseCasesTests.cs new file mode 100644 index 0000000..404be16 --- /dev/null +++ b/tests/Kitchen.Service.Application.UnitTests/KitchenWorkflowUseCasesTests.cs @@ -0,0 +1,60 @@ +using Kitchen.Service.Application.Ports; +using Kitchen.Service.Application.UseCases; +using Kitchen.Service.Contracts.Requests; + +namespace Kitchen.Service.Application.UnitTests; + +public class KitchenWorkflowUseCasesTests +{ + private readonly DefaultKitchenWorkflowPort workflowPort = new(); + + [Fact] + public async Task GetKitchenBoardUseCase_ReturnsLanesAndStations() + { + var useCase = new GetKitchenBoardUseCase(workflowPort); + + var response = await useCase.HandleAsync(new GetKitchenBoardRequest("demo-context"), CancellationToken.None); + + Assert.Equal("demo-context", response.ContextId); + Assert.NotEmpty(response.Lanes); + Assert.NotEmpty(response.AvailableStations); + } + + [Fact] + public async Task ClaimKitchenWorkItemUseCase_ReturnsClaimedResponse() + { + var useCase = new ClaimKitchenWorkItemUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new ClaimKitchenWorkItemRequest("demo-context", "WK-1001", "chef-maya"), + CancellationToken.None); + + Assert.True(response.Claimed); + Assert.Equal("chef-maya", response.ClaimedBy); + } + + [Fact] + public async Task UpdateKitchenPriorityUseCase_NormalizesPriority() + { + var useCase = new UpdateKitchenPriorityUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new UpdateKitchenPriorityRequest("demo-context", "WK-1001", 0, "expo-noah"), + CancellationToken.None); + + Assert.Equal(1, response.Priority); + } + + [Fact] + public async Task TransitionKitchenOrderStateUseCase_AllowsConfiguredStates() + { + var useCase = new TransitionKitchenOrderStateUseCase(); + + var response = await useCase.HandleAsync( + new TransitionKitchenOrderStateRequest("CO-1001", "KT-1001", "Preparing", "chef-maya"), + CancellationToken.None); + + Assert.True(response.Applied); + Assert.Equal("Preparing", response.CurrentState); + } +}