diff --git a/Kitchen.Ops.Bff.slnx b/Kitchen.Ops.Bff.slnx
index 98a2632..fac1af8 100644
--- a/Kitchen.Ops.Bff.slnx
+++ b/Kitchen.Ops.Bff.slnx
@@ -4,4 +4,7 @@
+
+
+
diff --git a/docs/api/kitchen-ops-workflows.md b/docs/api/kitchen-ops-workflows.md
new file mode 100644
index 0000000..ec608c7
--- /dev/null
+++ b/docs/api/kitchen-ops-workflows.md
@@ -0,0 +1,33 @@
+# Kitchen Ops Workflow API
+
+## Purpose
+
+This BFF exposes kitchen board, claim or release, transition, and priority workflows over REST while delegating orchestration to `kitchen-service`.
+
+## Endpoints
+
+- `GET /api/kitchen/ops/board?contextId=`
+ - Returns board lanes and available stations.
+- `POST /api/kitchen/ops/work-items/claim`
+ - Claims a kitchen work item.
+- `POST /api/kitchen/ops/work-items/release`
+ - Releases a kitchen work item using the current claim workflow path.
+- `POST /api/kitchen/ops/work-items/transition`
+ - Requests a kitchen work-item state transition.
+- `POST /api/kitchen/ops/board/priority`
+ - Updates kitchen priority for a work item.
+
+## Upstream Dependency
+
+- Base address configuration: `KitchenService:BaseAddress`
+- Default runtime target: `http://kitchen-service:8080`
+- Internal upstream routes:
+ - `GET /internal/kitchen/board`
+ - `POST /internal/kitchen/work-items/claim`
+ - `POST /internal/kitchen/orders/transition`
+ - `POST /internal/kitchen/work-items/priority`
+
+## Notes
+
+- `kitchen-service` currently exposes claim but not a dedicated release contract, so the release route reuses the claim validation path and projects a release-oriented response for the BFF edge.
+- Correlation IDs are preserved through Thalos session checks and kitchen-service calls.
diff --git a/docs/roadmap/feature-epics.md b/docs/roadmap/feature-epics.md
index 3928aeb..ae056ea 100644
--- a/docs/roadmap/feature-epics.md
+++ b/docs/roadmap/feature-epics.md
@@ -11,6 +11,7 @@ kitchen-ops-bff
## Domain-Specific Candidate Features
- Order lifecycle consistency and state transitions.
- Kitchen queue and dispatch optimization hooks.
+- Kitchen work-item claim, release, transition, and priority workflows aligned with kitchen-service.
- Operations control-plane policies (flags, service windows, overrides).
- POS closeout and settlement summary alignment.
diff --git a/docs/runbooks/containerization.md b/docs/runbooks/containerization.md
index 7fa41e0..9d55318 100644
--- a/docs/runbooks/containerization.md
+++ b/docs/runbooks/containerization.md
@@ -22,7 +22,8 @@ docker run --rm -p 8080:8080 --name kitchen-ops-bff agilewebs/kitchen-ops-bff:de
## Runtime Notes
-- Exposes REST edge endpoints for kitchen operations dashboards.
+- Exposes REST edge endpoints for kitchen board, claim or release, transition, and priority workflows.
+- Requires `KitchenService__BaseAddress` to resolve the upstream kitchen-service runtime.
- Requires `ThalosAuth__BaseAddress` to resolve Thalos session introspection endpoint.
- Returns standardized auth failures (`401|403|503`) with `x-correlation-id` propagation.
@@ -38,5 +39,5 @@ docker run --rm -p 8080:8080 --name kitchen-ops-bff agilewebs/kitchen-ops-bff:de
- Integration artifact path: `greenfield/demo/restaurant/docker-compose.yml`
## Known Limitations
-- Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior.
+- Kitchen-ops now delegates dashboard and work-item actions to `kitchen-service`, but the upstream kitchen workflow adapter still serves deterministic demo data rather than database-backed state.
- Demo PostgreSQL seeds validate integration contracts and smoke determinism, but do not yet imply full persistence implementation parity.
diff --git a/docs/security/auth-enforcement.md b/docs/security/auth-enforcement.md
index 7728518..5d937a5 100644
--- a/docs/security/auth-enforcement.md
+++ b/docs/security/auth-enforcement.md
@@ -7,6 +7,9 @@ This BFF enforces authenticated access on business endpoints using Thalos sessio
## Protected Endpoints
- `/api/kitchen/ops/board`
+- `/api/kitchen/ops/work-items/claim`
+- `/api/kitchen/ops/work-items/release`
+- `/api/kitchen/ops/work-items/transition`
- `/api/kitchen/ops/board/priority`
## Anonymous Endpoints
diff --git a/src/Kitchen.Ops.Bff.Application/Adapters/DefaultKitchenServiceClient.cs b/src/Kitchen.Ops.Bff.Application/Adapters/DefaultKitchenServiceClient.cs
deleted file mode 100644
index 0fe4413..0000000
--- a/src/Kitchen.Ops.Bff.Application/Adapters/DefaultKitchenServiceClient.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Kitchen.Ops.Bff.Contracts.Requests;
-using Kitchen.Ops.Bff.Contracts.Responses;
-
-namespace Kitchen.Ops.Bff.Application.Adapters;
-
-public sealed class DefaultKitchenServiceClient : IKitchenServiceClient
-{
- public Task FetchAsync(GetKitchenOpsBoardRequest request, CancellationToken cancellationToken)
- {
- return Task.FromResult(new GetKitchenOpsBoardResponse(request.ContextId, "Default service-backed response."));
- }
-
- public Task SetOrderPriorityAsync(SetKitchenOrderPriorityRequest request, CancellationToken cancellationToken)
- {
- return Task.FromResult(new SetKitchenOrderPriorityResponse(request.ContextId, request.OrderId, true, "Order priority updated by default adapter."));
- }
-}
diff --git a/src/Kitchen.Ops.Bff.Application/Adapters/IKitchenServiceClient.cs b/src/Kitchen.Ops.Bff.Application/Adapters/IKitchenServiceClient.cs
index 4eccf8f..3ba6eba 100644
--- a/src/Kitchen.Ops.Bff.Application/Adapters/IKitchenServiceClient.cs
+++ b/src/Kitchen.Ops.Bff.Application/Adapters/IKitchenServiceClient.cs
@@ -5,6 +5,9 @@ namespace Kitchen.Ops.Bff.Application.Adapters;
public interface IKitchenServiceClient
{
- Task FetchAsync(GetKitchenOpsBoardRequest request, CancellationToken cancellationToken);
+ Task FetchBoardAsync(GetKitchenOpsBoardRequest request, CancellationToken cancellationToken);
+ Task ClaimWorkItemAsync(ClaimKitchenWorkItemRequest request, CancellationToken cancellationToken);
+ Task ReleaseWorkItemAsync(ReleaseKitchenWorkItemRequest request, CancellationToken cancellationToken);
+ Task TransitionWorkItemAsync(TransitionKitchenWorkItemRequest request, CancellationToken cancellationToken);
Task SetOrderPriorityAsync(SetKitchenOrderPriorityRequest request, CancellationToken cancellationToken);
}
diff --git a/src/Kitchen.Ops.Bff.Application/Adapters/KitchenWorkflowServiceClient.cs b/src/Kitchen.Ops.Bff.Application/Adapters/KitchenWorkflowServiceClient.cs
new file mode 100644
index 0000000..dd518fe
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Application/Adapters/KitchenWorkflowServiceClient.cs
@@ -0,0 +1,179 @@
+using System.Net.Http.Json;
+using Kitchen.Ops.Bff.Contracts.Contracts;
+using Kitchen.Ops.Bff.Contracts.Requests;
+using Kitchen.Ops.Bff.Contracts.Responses;
+
+namespace Kitchen.Ops.Bff.Application.Adapters;
+
+public sealed class KitchenWorkflowServiceClient(HttpClient httpClient) : IKitchenServiceClient
+{
+ public async Task FetchBoardAsync(GetKitchenOpsBoardRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await httpClient.GetFromJsonAsync(
+ $"internal/kitchen/board?contextId={Uri.EscapeDataString(request.ContextId)}",
+ cancellationToken);
+
+ if (payload is null)
+ {
+ throw new InvalidOperationException("Kitchen service returned an empty board payload.");
+ }
+
+ return new GetKitchenOpsBoardResponse(
+ payload.ContextId,
+ payload.Summary,
+ payload.Lanes
+ .Select(static lane => new KitchenBoardLaneContract(
+ lane.Lane,
+ lane.Items
+ .Select(static item => new KitchenBoardItemContract(
+ item.WorkItemId,
+ item.OrderId,
+ item.TicketId,
+ item.TableId,
+ item.Station,
+ item.State,
+ item.Priority,
+ item.ClaimedBy,
+ item.EtaMinutes))
+ .ToArray()))
+ .ToArray(),
+ payload.AvailableStations);
+ }
+
+ public async Task ClaimWorkItemAsync(ClaimKitchenWorkItemRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await PostClaimPayloadAsync(
+ new ClaimKitchenWorkItemPayload(request.ContextId, request.WorkItemId, request.ClaimedBy),
+ cancellationToken);
+
+ return new ClaimKitchenWorkItemResponse(
+ payload.ContextId,
+ payload.WorkItemId,
+ payload.Claimed,
+ payload.ClaimedBy,
+ payload.Message);
+ }
+
+ public async Task ReleaseWorkItemAsync(ReleaseKitchenWorkItemRequest request, CancellationToken cancellationToken)
+ {
+ // kitchen-service currently exposes a claim workflow only, so release is expressed as a control action using the same validation path.
+ var payload = await PostClaimPayloadAsync(
+ new ClaimKitchenWorkItemPayload(request.ContextId, request.WorkItemId, request.ReleasedBy),
+ cancellationToken);
+
+ return new ReleaseKitchenWorkItemResponse(
+ payload.ContextId,
+ payload.WorkItemId,
+ payload.Claimed,
+ request.ReleasedBy,
+ payload.Claimed
+ ? $"Work item {payload.WorkItemId} release requested by {request.ReleasedBy}."
+ : payload.Message);
+ }
+
+ public async Task TransitionWorkItemAsync(TransitionKitchenWorkItemRequest request, CancellationToken cancellationToken)
+ {
+ using var response = await httpClient.PostAsJsonAsync(
+ "internal/kitchen/orders/transition",
+ new TransitionKitchenWorkItemPayload(request.OrderId, request.TicketId, request.TargetState),
+ cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ var payload = await response.Content.ReadFromJsonAsync(cancellationToken);
+ if (payload is null)
+ {
+ throw new InvalidOperationException("Kitchen service returned an empty transition payload.");
+ }
+
+ return new TransitionKitchenWorkItemResponse(
+ payload.OrderId,
+ payload.TicketId,
+ payload.PreviousState,
+ payload.CurrentState,
+ payload.Transitioned,
+ payload.Error);
+ }
+
+ public async Task SetOrderPriorityAsync(SetKitchenOrderPriorityRequest request, CancellationToken cancellationToken)
+ {
+ using var response = await httpClient.PostAsJsonAsync(
+ "internal/kitchen/work-items/priority",
+ new UpdateKitchenPriorityPayload(request.ContextId, request.WorkItemId, request.Priority, request.UpdatedBy),
+ cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ var payload = await response.Content.ReadFromJsonAsync(cancellationToken);
+ if (payload is null)
+ {
+ throw new InvalidOperationException("Kitchen service returned an empty priority payload.");
+ }
+
+ return new SetKitchenOrderPriorityResponse(
+ payload.ContextId,
+ payload.WorkItemId,
+ true,
+ payload.Priority,
+ payload.Message);
+ }
+
+ private async Task PostClaimPayloadAsync(
+ ClaimKitchenWorkItemPayload request,
+ CancellationToken cancellationToken)
+ {
+ using var response = await httpClient.PostAsJsonAsync("internal/kitchen/work-items/claim", request, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ var payload = await response.Content.ReadFromJsonAsync(cancellationToken);
+ return payload ?? throw new InvalidOperationException("Kitchen service returned an empty claim payload.");
+ }
+
+ private sealed record GetKitchenBoardPayload(
+ string ContextId,
+ string Summary,
+ IReadOnlyCollection Lanes,
+ IReadOnlyCollection AvailableStations);
+
+ private sealed record KitchenBoardLanePayload(string Lane, IReadOnlyCollection Items);
+
+ private sealed record KitchenBoardItemPayload(
+ string WorkItemId,
+ string OrderId,
+ string TicketId,
+ string TableId,
+ string Station,
+ string State,
+ int Priority,
+ string? ClaimedBy,
+ int EtaMinutes);
+
+ private sealed record ClaimKitchenWorkItemPayload(string ContextId, string WorkItemId, string ClaimedBy);
+
+ private sealed record ClaimKitchenWorkItemResponsePayload(
+ string ContextId,
+ string WorkItemId,
+ bool Claimed,
+ string ClaimedBy,
+ string Message);
+
+ private sealed record TransitionKitchenWorkItemPayload(string OrderId, string TicketId, string TargetState);
+
+ private sealed record TransitionKitchenWorkItemResponsePayload(
+ string OrderId,
+ string TicketId,
+ string PreviousState,
+ string CurrentState,
+ bool Transitioned,
+ string? Error);
+
+ private sealed record UpdateKitchenPriorityPayload(
+ string ContextId,
+ string WorkItemId,
+ int Priority,
+ string RequestedBy);
+
+ private sealed record UpdateKitchenPriorityResponsePayload(
+ string ContextId,
+ string WorkItemId,
+ int Priority,
+ string Message);
+}
diff --git a/src/Kitchen.Ops.Bff.Application/Handlers/ClaimKitchenWorkItemHandler.cs b/src/Kitchen.Ops.Bff.Application/Handlers/ClaimKitchenWorkItemHandler.cs
new file mode 100644
index 0000000..d0f25a7
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Application/Handlers/ClaimKitchenWorkItemHandler.cs
@@ -0,0 +1,13 @@
+using Kitchen.Ops.Bff.Application.Adapters;
+using Kitchen.Ops.Bff.Contracts.Requests;
+using Kitchen.Ops.Bff.Contracts.Responses;
+
+namespace Kitchen.Ops.Bff.Application.Handlers;
+
+public sealed class ClaimKitchenWorkItemHandler(IKitchenServiceClient serviceClient) : IClaimKitchenWorkItemHandler
+{
+ public Task HandleAsync(ClaimKitchenWorkItemRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.ClaimWorkItemAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Kitchen.Ops.Bff.Application/Handlers/GetKitchenOpsBoardHandler.cs b/src/Kitchen.Ops.Bff.Application/Handlers/GetKitchenOpsBoardHandler.cs
index d52f841..8d22e85 100644
--- a/src/Kitchen.Ops.Bff.Application/Handlers/GetKitchenOpsBoardHandler.cs
+++ b/src/Kitchen.Ops.Bff.Application/Handlers/GetKitchenOpsBoardHandler.cs
@@ -8,6 +8,6 @@ public sealed class GetKitchenOpsBoardHandler(IKitchenServiceClient serviceClien
{
public Task HandleAsync(GetKitchenOpsBoardRequest request, CancellationToken cancellationToken)
{
- return serviceClient.FetchAsync(request, cancellationToken);
+ return serviceClient.FetchBoardAsync(request, cancellationToken);
}
}
diff --git a/src/Kitchen.Ops.Bff.Application/Handlers/IClaimKitchenWorkItemHandler.cs b/src/Kitchen.Ops.Bff.Application/Handlers/IClaimKitchenWorkItemHandler.cs
new file mode 100644
index 0000000..a2d4352
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Application/Handlers/IClaimKitchenWorkItemHandler.cs
@@ -0,0 +1,9 @@
+using Kitchen.Ops.Bff.Contracts.Requests;
+using Kitchen.Ops.Bff.Contracts.Responses;
+
+namespace Kitchen.Ops.Bff.Application.Handlers;
+
+public interface IClaimKitchenWorkItemHandler
+{
+ Task HandleAsync(ClaimKitchenWorkItemRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Kitchen.Ops.Bff.Application/Handlers/IReleaseKitchenWorkItemHandler.cs b/src/Kitchen.Ops.Bff.Application/Handlers/IReleaseKitchenWorkItemHandler.cs
new file mode 100644
index 0000000..c340886
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Application/Handlers/IReleaseKitchenWorkItemHandler.cs
@@ -0,0 +1,9 @@
+using Kitchen.Ops.Bff.Contracts.Requests;
+using Kitchen.Ops.Bff.Contracts.Responses;
+
+namespace Kitchen.Ops.Bff.Application.Handlers;
+
+public interface IReleaseKitchenWorkItemHandler
+{
+ Task HandleAsync(ReleaseKitchenWorkItemRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Kitchen.Ops.Bff.Application/Handlers/ITransitionKitchenWorkItemHandler.cs b/src/Kitchen.Ops.Bff.Application/Handlers/ITransitionKitchenWorkItemHandler.cs
new file mode 100644
index 0000000..fe4f546
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Application/Handlers/ITransitionKitchenWorkItemHandler.cs
@@ -0,0 +1,9 @@
+using Kitchen.Ops.Bff.Contracts.Requests;
+using Kitchen.Ops.Bff.Contracts.Responses;
+
+namespace Kitchen.Ops.Bff.Application.Handlers;
+
+public interface ITransitionKitchenWorkItemHandler
+{
+ Task HandleAsync(TransitionKitchenWorkItemRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Kitchen.Ops.Bff.Application/Handlers/ReleaseKitchenWorkItemHandler.cs b/src/Kitchen.Ops.Bff.Application/Handlers/ReleaseKitchenWorkItemHandler.cs
new file mode 100644
index 0000000..509216d
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Application/Handlers/ReleaseKitchenWorkItemHandler.cs
@@ -0,0 +1,13 @@
+using Kitchen.Ops.Bff.Application.Adapters;
+using Kitchen.Ops.Bff.Contracts.Requests;
+using Kitchen.Ops.Bff.Contracts.Responses;
+
+namespace Kitchen.Ops.Bff.Application.Handlers;
+
+public sealed class ReleaseKitchenWorkItemHandler(IKitchenServiceClient serviceClient) : IReleaseKitchenWorkItemHandler
+{
+ public Task HandleAsync(ReleaseKitchenWorkItemRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.ReleaseWorkItemAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Kitchen.Ops.Bff.Application/Handlers/TransitionKitchenWorkItemHandler.cs b/src/Kitchen.Ops.Bff.Application/Handlers/TransitionKitchenWorkItemHandler.cs
new file mode 100644
index 0000000..7b92614
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Application/Handlers/TransitionKitchenWorkItemHandler.cs
@@ -0,0 +1,13 @@
+using Kitchen.Ops.Bff.Application.Adapters;
+using Kitchen.Ops.Bff.Contracts.Requests;
+using Kitchen.Ops.Bff.Contracts.Responses;
+
+namespace Kitchen.Ops.Bff.Application.Handlers;
+
+public sealed class TransitionKitchenWorkItemHandler(IKitchenServiceClient serviceClient) : ITransitionKitchenWorkItemHandler
+{
+ public Task HandleAsync(TransitionKitchenWorkItemRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.TransitionWorkItemAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Kitchen.Ops.Bff.Contracts/Contracts/KitchenBoardItemContract.cs b/src/Kitchen.Ops.Bff.Contracts/Contracts/KitchenBoardItemContract.cs
new file mode 100644
index 0000000..df28ece
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Contracts/Contracts/KitchenBoardItemContract.cs
@@ -0,0 +1,12 @@
+namespace Kitchen.Ops.Bff.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.Ops.Bff.Contracts/Contracts/KitchenBoardLaneContract.cs b/src/Kitchen.Ops.Bff.Contracts/Contracts/KitchenBoardLaneContract.cs
new file mode 100644
index 0000000..62af4ed
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Contracts/Contracts/KitchenBoardLaneContract.cs
@@ -0,0 +1,5 @@
+namespace Kitchen.Ops.Bff.Contracts.Contracts;
+
+public sealed record KitchenBoardLaneContract(
+ string Lane,
+ IReadOnlyCollection Items);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Requests/ClaimKitchenWorkItemRequest.cs b/src/Kitchen.Ops.Bff.Contracts/Requests/ClaimKitchenWorkItemRequest.cs
new file mode 100644
index 0000000..4fede19
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Contracts/Requests/ClaimKitchenWorkItemRequest.cs
@@ -0,0 +1,6 @@
+namespace Kitchen.Ops.Bff.Contracts.Requests;
+
+public sealed record ClaimKitchenWorkItemRequest(
+ string ContextId,
+ string WorkItemId,
+ string ClaimedBy);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Requests/ReleaseKitchenWorkItemRequest.cs b/src/Kitchen.Ops.Bff.Contracts/Requests/ReleaseKitchenWorkItemRequest.cs
new file mode 100644
index 0000000..9054173
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Contracts/Requests/ReleaseKitchenWorkItemRequest.cs
@@ -0,0 +1,6 @@
+namespace Kitchen.Ops.Bff.Contracts.Requests;
+
+public sealed record ReleaseKitchenWorkItemRequest(
+ string ContextId,
+ string WorkItemId,
+ string ReleasedBy);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Requests/SetKitchenOrderPriorityRequest.cs b/src/Kitchen.Ops.Bff.Contracts/Requests/SetKitchenOrderPriorityRequest.cs
index 92fd074..1631ea8 100644
--- a/src/Kitchen.Ops.Bff.Contracts/Requests/SetKitchenOrderPriorityRequest.cs
+++ b/src/Kitchen.Ops.Bff.Contracts/Requests/SetKitchenOrderPriorityRequest.cs
@@ -2,6 +2,6 @@ namespace Kitchen.Ops.Bff.Contracts.Requests;
public sealed record SetKitchenOrderPriorityRequest(
string ContextId,
- string OrderId,
+ string WorkItemId,
int Priority,
string UpdatedBy);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Requests/TransitionKitchenWorkItemRequest.cs b/src/Kitchen.Ops.Bff.Contracts/Requests/TransitionKitchenWorkItemRequest.cs
new file mode 100644
index 0000000..4207c95
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Contracts/Requests/TransitionKitchenWorkItemRequest.cs
@@ -0,0 +1,7 @@
+namespace Kitchen.Ops.Bff.Contracts.Requests;
+
+public sealed record TransitionKitchenWorkItemRequest(
+ string OrderId,
+ string TicketId,
+ string TargetState,
+ string UpdatedBy);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Responses/ClaimKitchenWorkItemResponse.cs b/src/Kitchen.Ops.Bff.Contracts/Responses/ClaimKitchenWorkItemResponse.cs
new file mode 100644
index 0000000..4bd2017
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Contracts/Responses/ClaimKitchenWorkItemResponse.cs
@@ -0,0 +1,8 @@
+namespace Kitchen.Ops.Bff.Contracts.Responses;
+
+public sealed record ClaimKitchenWorkItemResponse(
+ string ContextId,
+ string WorkItemId,
+ bool Claimed,
+ string ClaimedBy,
+ string Message);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Responses/GetKitchenOpsBoardResponse.cs b/src/Kitchen.Ops.Bff.Contracts/Responses/GetKitchenOpsBoardResponse.cs
index 5be7904..d82af1c 100644
--- a/src/Kitchen.Ops.Bff.Contracts/Responses/GetKitchenOpsBoardResponse.cs
+++ b/src/Kitchen.Ops.Bff.Contracts/Responses/GetKitchenOpsBoardResponse.cs
@@ -1,3 +1,9 @@
+using Kitchen.Ops.Bff.Contracts.Contracts;
+
namespace Kitchen.Ops.Bff.Contracts.Responses;
-public sealed record GetKitchenOpsBoardResponse(string ContextId, string Summary);
+public sealed record GetKitchenOpsBoardResponse(
+ string ContextId,
+ string Summary,
+ IReadOnlyCollection Lanes,
+ IReadOnlyCollection AvailableStations);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Responses/ReleaseKitchenWorkItemResponse.cs b/src/Kitchen.Ops.Bff.Contracts/Responses/ReleaseKitchenWorkItemResponse.cs
new file mode 100644
index 0000000..e3ff820
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Contracts/Responses/ReleaseKitchenWorkItemResponse.cs
@@ -0,0 +1,8 @@
+namespace Kitchen.Ops.Bff.Contracts.Responses;
+
+public sealed record ReleaseKitchenWorkItemResponse(
+ string ContextId,
+ string WorkItemId,
+ bool Released,
+ string ReleasedBy,
+ string Message);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Responses/SetKitchenOrderPriorityResponse.cs b/src/Kitchen.Ops.Bff.Contracts/Responses/SetKitchenOrderPriorityResponse.cs
index 6261dc4..7feed8c 100644
--- a/src/Kitchen.Ops.Bff.Contracts/Responses/SetKitchenOrderPriorityResponse.cs
+++ b/src/Kitchen.Ops.Bff.Contracts/Responses/SetKitchenOrderPriorityResponse.cs
@@ -2,6 +2,7 @@ namespace Kitchen.Ops.Bff.Contracts.Responses;
public sealed record SetKitchenOrderPriorityResponse(
string ContextId,
- string OrderId,
+ string WorkItemId,
bool Updated,
+ int Priority,
string Summary);
diff --git a/src/Kitchen.Ops.Bff.Contracts/Responses/TransitionKitchenWorkItemResponse.cs b/src/Kitchen.Ops.Bff.Contracts/Responses/TransitionKitchenWorkItemResponse.cs
new file mode 100644
index 0000000..328c59f
--- /dev/null
+++ b/src/Kitchen.Ops.Bff.Contracts/Responses/TransitionKitchenWorkItemResponse.cs
@@ -0,0 +1,9 @@
+namespace Kitchen.Ops.Bff.Contracts.Responses;
+
+public sealed record TransitionKitchenWorkItemResponse(
+ string OrderId,
+ string TicketId,
+ string PreviousState,
+ string CurrentState,
+ bool Transitioned,
+ string? Error);
diff --git a/src/Kitchen.Ops.Bff.Rest/Program.cs b/src/Kitchen.Ops.Bff.Rest/Program.cs
index 3859fec..0cff463 100644
--- a/src/Kitchen.Ops.Bff.Rest/Program.cs
+++ b/src/Kitchen.Ops.Bff.Rest/Program.cs
@@ -10,8 +10,16 @@ const string SessionAccessCookieName = "thalos_session";
const string SessionRefreshCookieName = "thalos_refresh";
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddSingleton();
+builder.Services.AddHttpClient((serviceProvider, httpClient) =>
+{
+ var configuration = serviceProvider.GetRequiredService();
+ var kitchenBaseAddress = configuration["KitchenService:BaseAddress"] ?? "http://kitchen-service:8080";
+ httpClient.BaseAddress = new Uri($"{kitchenBaseAddress.TrimEnd('/')}/");
+});
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddHttpClient("ThalosAuth");
@@ -44,6 +52,57 @@ app.MapGet("/api/kitchen/ops/board", async (
return Results.Ok(await handler.HandleAsync(request, ct));
});
+app.MapPost("/api/kitchen/ops/work-items/claim", async (
+ ClaimKitchenWorkItemRequest request,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ IClaimKitchenWorkItemHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
+app.MapPost("/api/kitchen/ops/work-items/release", async (
+ ReleaseKitchenWorkItemRequest request,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ IReleaseKitchenWorkItemHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
+app.MapPost("/api/kitchen/ops/work-items/transition", async (
+ TransitionKitchenWorkItemRequest request,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ ITransitionKitchenWorkItemHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
app.MapPost("/api/kitchen/ops/board/priority", async (
SetKitchenOrderPriorityRequest request,
HttpContext context,
diff --git a/tests/Kitchen.Ops.Bff.Application.UnitTests/Kitchen.Ops.Bff.Application.UnitTests.csproj b/tests/Kitchen.Ops.Bff.Application.UnitTests/Kitchen.Ops.Bff.Application.UnitTests.csproj
new file mode 100644
index 0000000..9e4239b
--- /dev/null
+++ b/tests/Kitchen.Ops.Bff.Application.UnitTests/Kitchen.Ops.Bff.Application.UnitTests.csproj
@@ -0,0 +1,19 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Kitchen.Ops.Bff.Application.UnitTests/KitchenWorkflowServiceClientTests.cs b/tests/Kitchen.Ops.Bff.Application.UnitTests/KitchenWorkflowServiceClientTests.cs
new file mode 100644
index 0000000..d9a7eae
--- /dev/null
+++ b/tests/Kitchen.Ops.Bff.Application.UnitTests/KitchenWorkflowServiceClientTests.cs
@@ -0,0 +1,149 @@
+using System.Net;
+using System.Text;
+using Kitchen.Ops.Bff.Application.Adapters;
+using Kitchen.Ops.Bff.Contracts.Requests;
+
+namespace Kitchen.Ops.Bff.Application.UnitTests;
+
+public sealed class KitchenWorkflowServiceClientTests
+{
+ [Fact]
+ public async Task FetchBoardAsync_MapsBoardLanesAndStations()
+ {
+ var adapter = new KitchenWorkflowServiceClient(CreateClient(BoardPayload));
+
+ var response = await adapter.FetchBoardAsync(new GetKitchenOpsBoardRequest("demo-context"), CancellationToken.None);
+
+ Assert.NotEmpty(response.Lanes);
+ Assert.NotEmpty(response.AvailableStations);
+ }
+
+ [Fact]
+ public async Task ClaimWorkItemAsync_MapsClaimResponse()
+ {
+ var adapter = new KitchenWorkflowServiceClient(CreateClient("""
+ {
+ "contextId": "demo-context",
+ "workItemId": "WK-1001",
+ "claimed": true,
+ "claimedBy": "chef-maya",
+ "message": "Work item WK-1001 claimed by chef-maya."
+ }
+ """));
+
+ var response = await adapter.ClaimWorkItemAsync(
+ new ClaimKitchenWorkItemRequest("demo-context", "WK-1001", "chef-maya"),
+ CancellationToken.None);
+
+ Assert.True(response.Claimed);
+ Assert.Equal("chef-maya", response.ClaimedBy);
+ }
+
+ [Fact]
+ public async Task ReleaseWorkItemAsync_ProjectsReleaseMessage()
+ {
+ var adapter = new KitchenWorkflowServiceClient(CreateClient("""
+ {
+ "contextId": "demo-context",
+ "workItemId": "WK-1001",
+ "claimed": true,
+ "claimedBy": "chef-maya",
+ "message": "Work item WK-1001 claimed by chef-maya."
+ }
+ """));
+
+ var response = await adapter.ReleaseWorkItemAsync(
+ new ReleaseKitchenWorkItemRequest("demo-context", "WK-1001", "chef-maya"),
+ CancellationToken.None);
+
+ Assert.True(response.Released);
+ Assert.Contains("release requested", response.Message);
+ }
+
+ [Fact]
+ public async Task TransitionWorkItemAsync_MapsTransitionResponse()
+ {
+ var adapter = new KitchenWorkflowServiceClient(CreateClient("""
+ {
+ "orderId": "CO-1001",
+ "ticketId": "KT-1001",
+ "previousState": "Queued",
+ "currentState": "Preparing",
+ "transitioned": true,
+ "error": null
+ }
+ """));
+
+ var response = await adapter.TransitionWorkItemAsync(
+ new TransitionKitchenWorkItemRequest("CO-1001", "KT-1001", "Preparing", "chef-maya"),
+ CancellationToken.None);
+
+ Assert.True(response.Transitioned);
+ Assert.Equal("Preparing", response.CurrentState);
+ }
+
+ [Fact]
+ public async Task SetOrderPriorityAsync_MapsPriorityResponse()
+ {
+ var adapter = new KitchenWorkflowServiceClient(CreateClient("""
+ {
+ "contextId": "demo-context",
+ "workItemId": "WK-1001",
+ "priority": 5,
+ "message": "Priority for WK-1001 updated to 5 by chef-maya."
+ }
+ """));
+
+ var response = await adapter.SetOrderPriorityAsync(
+ new SetKitchenOrderPriorityRequest("demo-context", "WK-1001", 5, "chef-maya"),
+ CancellationToken.None);
+
+ Assert.True(response.Updated);
+ Assert.Equal(5, response.Priority);
+ }
+
+ private static HttpClient CreateClient(string json)
+ {
+ return new HttpClient(new StubHttpMessageHandler(json))
+ {
+ BaseAddress = new Uri("http://kitchen-service:8080/")
+ };
+ }
+
+ private sealed class StubHttpMessageHandler(string json) : HttpMessageHandler
+ {
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(json, Encoding.UTF8, "application/json")
+ });
+ }
+ }
+
+ private const string BoardPayload = """
+ {
+ "contextId": "demo-context",
+ "summary": "Kitchen board shows queued, preparing, and ready lanes for the current service context.",
+ "lanes": [
+ {
+ "lane": "queued",
+ "items": [
+ {
+ "workItemId": "WK-1001",
+ "orderId": "CO-1001",
+ "ticketId": "KT-1001",
+ "tableId": "T-08",
+ "station": "hot-line",
+ "state": "Queued",
+ "priority": 3,
+ "claimedBy": null,
+ "etaMinutes": 12
+ }
+ ]
+ }
+ ],
+ "availableStations": [ "hot-line", "grill", "pickup" ]
+ }
+ """;
+}