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);
+ }
+}