diff --git a/Waiter.Floor.Bff.slnx b/Waiter.Floor.Bff.slnx
index f237148..28dea0c 100644
--- a/Waiter.Floor.Bff.slnx
+++ b/Waiter.Floor.Bff.slnx
@@ -4,4 +4,7 @@
+
+
+
diff --git a/docs/api/waiter-floor-workflows.md b/docs/api/waiter-floor-workflows.md
new file mode 100644
index 0000000..5a41857
--- /dev/null
+++ b/docs/api/waiter-floor-workflows.md
@@ -0,0 +1,29 @@
+# Waiter Floor Workflow API
+
+## Purpose
+
+This BFF exposes execution-facing waiter workflows over REST while delegating orchestration to `operations-service`.
+
+## Endpoints
+
+- `GET /api/waiter/floor/assignments?contextId=`
+ - Returns assignment summary, location context, current assignments, and recent activity.
+- `GET /api/waiter/floor/activity?contextId=`
+ - Returns recent waiter activity projected from the same operations workflow snapshot.
+- `POST /api/waiter/floor/orders`
+ - Submits a waiter order snapshot for processing.
+- `PUT /api/waiter/floor/orders/{orderId}`
+ - Updates an existing waiter order snapshot using the same operations workflow contract.
+
+## Upstream Dependency
+
+- Base address configuration: `OperationsService:BaseAddress`
+- Default runtime target: `http://operations-service:8080`
+- Internal upstream routes:
+ - `GET /internal/operations/waiter/assignments`
+ - `POST /internal/operations/orders`
+
+## Notes
+
+- The update route currently reuses the operations order submission contract so waiter-floor can expose update semantics without introducing a new cross-repo dependency.
+- Correlation IDs are preserved through Thalos session checks and operations-service calls.
diff --git a/docs/architecture/ownership-boundary.md b/docs/architecture/ownership-boundary.md
index c092119..2534945 100644
--- a/docs/architecture/ownership-boundary.md
+++ b/docs/architecture/ownership-boundary.md
@@ -1,6 +1,6 @@
# Waiter Floor Ownership Boundary
- Execution-facing BFF for floor staff workflows.
-- Owns edge contracts for assignment views and order-taking orchestration.
+- Owns edge contracts for assignment views, recent floor activity, and order submit or update orchestration.
- Does not own policy/config control-plane concerns.
- Consumes service APIs only; no direct DAL access.
diff --git a/docs/roadmap/feature-epics.md b/docs/roadmap/feature-epics.md
index c23fd74..0ab211f 100644
--- a/docs/roadmap/feature-epics.md
+++ b/docs/roadmap/feature-epics.md
@@ -10,6 +10,8 @@ waiter-floor-bff
## Domain-Specific Candidate Features
- Order lifecycle consistency and state transitions.
+- Waiter assignment visibility with recent floor activity context.
+- Waiter order update workflows that stay aligned with service-level restaurant order orchestration.
- Kitchen queue and dispatch optimization hooks.
- 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 9eb66f4..714232b 100644
--- a/docs/runbooks/containerization.md
+++ b/docs/runbooks/containerization.md
@@ -22,7 +22,8 @@ docker run --rm -p 8080:8080 --name waiter-floor-bff agilewebs/waiter-floor-bff:
## Runtime Notes
-- Exposes REST edge endpoints for waiter assignment and order submission flows.
+- Exposes REST edge endpoints for waiter assignment, recent activity, and order submit or update flows.
+- Requires `OperationsService__BaseAddress` to resolve the upstream operations-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 waiter-floor-bff agilewebs/waiter-floor-bff:
- Integration artifact path: `greenfield/demo/restaurant/docker-compose.yml`
## Known Limitations
-- Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior.
+- Waiter-floor now delegates workflow snapshots to `operations-service`, but the upstream operations 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 3e2463b..13927af 100644
--- a/docs/security/auth-enforcement.md
+++ b/docs/security/auth-enforcement.md
@@ -7,7 +7,9 @@ This BFF enforces authenticated access on business endpoints using Thalos sessio
## Protected Endpoints
- `/api/waiter/floor/assignments`
+- `/api/waiter/floor/activity`
- `/api/waiter/floor/orders`
+- `/api/waiter/floor/orders/{orderId}`
## Anonymous Endpoints
diff --git a/src/Waiter.Floor.Bff.Application/Adapters/DefaultWaiterServiceClient.cs b/src/Waiter.Floor.Bff.Application/Adapters/DefaultWaiterServiceClient.cs
deleted file mode 100644
index 82789b2..0000000
--- a/src/Waiter.Floor.Bff.Application/Adapters/DefaultWaiterServiceClient.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Waiter.Floor.Bff.Contracts.Requests;
-using Waiter.Floor.Bff.Contracts.Responses;
-
-namespace Waiter.Floor.Bff.Application.Adapters;
-
-public sealed class DefaultWaiterServiceClient : IWaiterServiceClient
-{
- public Task FetchAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
- {
- return Task.FromResult(new GetWaiterAssignmentsResponse(request.ContextId, "Default service-backed response."));
- }
-
- public Task SubmitOrderAsync(SubmitFloorOrderRequest request, CancellationToken cancellationToken)
- {
- return Task.FromResult(new SubmitFloorOrderResponse(request.OrderId, true, "Order accepted by waiter-floor default adapter."));
- }
-}
diff --git a/src/Waiter.Floor.Bff.Application/Adapters/IWaiterServiceClient.cs b/src/Waiter.Floor.Bff.Application/Adapters/IWaiterServiceClient.cs
index c768397..6dbdccf 100644
--- a/src/Waiter.Floor.Bff.Application/Adapters/IWaiterServiceClient.cs
+++ b/src/Waiter.Floor.Bff.Application/Adapters/IWaiterServiceClient.cs
@@ -5,6 +5,8 @@ namespace Waiter.Floor.Bff.Application.Adapters;
public interface IWaiterServiceClient
{
- Task FetchAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
+ Task FetchAssignmentsAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
+ Task FetchRecentActivityAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
Task SubmitOrderAsync(SubmitFloorOrderRequest request, CancellationToken cancellationToken);
+ Task UpdateOrderAsync(UpdateFloorOrderRequest request, CancellationToken cancellationToken);
}
diff --git a/src/Waiter.Floor.Bff.Application/Adapters/OperationsWaiterServiceClient.cs b/src/Waiter.Floor.Bff.Application/Adapters/OperationsWaiterServiceClient.cs
new file mode 100644
index 0000000..76d47c0
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Application/Adapters/OperationsWaiterServiceClient.cs
@@ -0,0 +1,120 @@
+using System.Net.Http.Json;
+using Waiter.Floor.Bff.Contracts.Contracts;
+using Waiter.Floor.Bff.Contracts.Requests;
+using Waiter.Floor.Bff.Contracts.Responses;
+
+namespace Waiter.Floor.Bff.Application.Adapters;
+
+public sealed class OperationsWaiterServiceClient(HttpClient httpClient) : IWaiterServiceClient
+{
+ public async Task FetchAssignmentsAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await GetAssignmentsPayloadAsync(request.ContextId, cancellationToken);
+
+ return new GetWaiterAssignmentsResponse(
+ payload.ContextId,
+ payload.LocationId,
+ payload.Summary,
+ payload.Assignments
+ .Select(static assignment => new WaiterAssignmentContract(
+ assignment.WaiterId,
+ assignment.TableId,
+ assignment.Status,
+ assignment.ActiveOrders))
+ .ToArray(),
+ payload.RecentActivity);
+ }
+
+ public async Task FetchRecentActivityAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await GetAssignmentsPayloadAsync(request.ContextId, cancellationToken);
+
+ return new GetWaiterRecentActivityResponse(
+ payload.ContextId,
+ payload.LocationId,
+ payload.Summary,
+ payload.RecentActivity);
+ }
+
+ public async Task SubmitOrderAsync(SubmitFloorOrderRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await SubmitOrderPayloadAsync(
+ new SubmitRestaurantOrderPayload(request.ContextId, request.OrderId, request.TableId, request.ItemCount),
+ cancellationToken);
+
+ return new SubmitFloorOrderResponse(
+ payload.ContextId,
+ payload.OrderId,
+ payload.Accepted,
+ payload.Summary,
+ payload.Status,
+ payload.SubmittedAtUtc);
+ }
+
+ public async Task UpdateOrderAsync(UpdateFloorOrderRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await SubmitOrderPayloadAsync(
+ new SubmitRestaurantOrderPayload(request.ContextId, request.OrderId, request.TableId, request.ItemCount),
+ cancellationToken);
+
+ // operations-service currently accepts a full order snapshot for both submit and update semantics.
+ var summary = payload.Accepted
+ ? $"Updated order {payload.OrderId}. {payload.Summary}"
+ : payload.Summary;
+
+ return new UpdateFloorOrderResponse(
+ payload.ContextId,
+ payload.OrderId,
+ payload.Accepted,
+ summary,
+ payload.Status,
+ payload.SubmittedAtUtc);
+ }
+
+ private async Task GetAssignmentsPayloadAsync(string contextId, CancellationToken cancellationToken)
+ {
+ var payload = await httpClient.GetFromJsonAsync(
+ $"internal/operations/waiter/assignments?contextId={Uri.EscapeDataString(contextId)}",
+ cancellationToken);
+
+ return payload ?? throw new InvalidOperationException("Operations service returned an empty waiter assignments payload.");
+ }
+
+ private async Task SubmitOrderPayloadAsync(
+ SubmitRestaurantOrderPayload request,
+ CancellationToken cancellationToken)
+ {
+ using var response = await httpClient.PostAsJsonAsync("internal/operations/orders", request, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ var payload = await response.Content.ReadFromJsonAsync(cancellationToken);
+ return payload ?? throw new InvalidOperationException("Operations service returned an empty order workflow payload.");
+ }
+
+ private sealed record GetWaiterAssignmentsPayload(
+ string ContextId,
+ string LocationId,
+ string Summary,
+ IReadOnlyCollection Assignments,
+ IReadOnlyCollection RecentActivity);
+
+ private sealed record WaiterAssignmentPayload(
+ string WaiterId,
+ string TableId,
+ string Status,
+ int ActiveOrders);
+
+ private sealed record SubmitRestaurantOrderPayload(
+ string ContextId,
+ string OrderId,
+ string TableId,
+ int ItemCount);
+
+ private sealed record SubmitRestaurantOrderResponsePayload(
+ string ContextId,
+ string OrderId,
+ bool Accepted,
+ string Summary,
+ string Status,
+ DateTime SubmittedAtUtc);
+}
diff --git a/src/Waiter.Floor.Bff.Application/Handlers/GetWaiterAssignmentsHandler.cs b/src/Waiter.Floor.Bff.Application/Handlers/GetWaiterAssignmentsHandler.cs
index 234ba91..31f5f8d 100644
--- a/src/Waiter.Floor.Bff.Application/Handlers/GetWaiterAssignmentsHandler.cs
+++ b/src/Waiter.Floor.Bff.Application/Handlers/GetWaiterAssignmentsHandler.cs
@@ -8,6 +8,6 @@ public sealed class GetWaiterAssignmentsHandler(IWaiterServiceClient serviceClie
{
public Task HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
{
- return serviceClient.FetchAsync(request, cancellationToken);
+ return serviceClient.FetchAssignmentsAsync(request, cancellationToken);
}
}
diff --git a/src/Waiter.Floor.Bff.Application/Handlers/GetWaiterRecentActivityHandler.cs b/src/Waiter.Floor.Bff.Application/Handlers/GetWaiterRecentActivityHandler.cs
new file mode 100644
index 0000000..db1a852
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Application/Handlers/GetWaiterRecentActivityHandler.cs
@@ -0,0 +1,13 @@
+using Waiter.Floor.Bff.Application.Adapters;
+using Waiter.Floor.Bff.Contracts.Requests;
+using Waiter.Floor.Bff.Contracts.Responses;
+
+namespace Waiter.Floor.Bff.Application.Handlers;
+
+public sealed class GetWaiterRecentActivityHandler(IWaiterServiceClient serviceClient) : IGetWaiterRecentActivityHandler
+{
+ public Task HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.FetchRecentActivityAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Waiter.Floor.Bff.Application/Handlers/IGetWaiterRecentActivityHandler.cs b/src/Waiter.Floor.Bff.Application/Handlers/IGetWaiterRecentActivityHandler.cs
new file mode 100644
index 0000000..a2b7658
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Application/Handlers/IGetWaiterRecentActivityHandler.cs
@@ -0,0 +1,9 @@
+using Waiter.Floor.Bff.Contracts.Requests;
+using Waiter.Floor.Bff.Contracts.Responses;
+
+namespace Waiter.Floor.Bff.Application.Handlers;
+
+public interface IGetWaiterRecentActivityHandler
+{
+ Task HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Waiter.Floor.Bff.Application/Handlers/IUpdateFloorOrderHandler.cs b/src/Waiter.Floor.Bff.Application/Handlers/IUpdateFloorOrderHandler.cs
new file mode 100644
index 0000000..6416639
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Application/Handlers/IUpdateFloorOrderHandler.cs
@@ -0,0 +1,9 @@
+using Waiter.Floor.Bff.Contracts.Requests;
+using Waiter.Floor.Bff.Contracts.Responses;
+
+namespace Waiter.Floor.Bff.Application.Handlers;
+
+public interface IUpdateFloorOrderHandler
+{
+ Task HandleAsync(UpdateFloorOrderRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Waiter.Floor.Bff.Application/Handlers/UpdateFloorOrderHandler.cs b/src/Waiter.Floor.Bff.Application/Handlers/UpdateFloorOrderHandler.cs
new file mode 100644
index 0000000..e2ad88c
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Application/Handlers/UpdateFloorOrderHandler.cs
@@ -0,0 +1,13 @@
+using Waiter.Floor.Bff.Application.Adapters;
+using Waiter.Floor.Bff.Contracts.Requests;
+using Waiter.Floor.Bff.Contracts.Responses;
+
+namespace Waiter.Floor.Bff.Application.Handlers;
+
+public sealed class UpdateFloorOrderHandler(IWaiterServiceClient serviceClient) : IUpdateFloorOrderHandler
+{
+ public Task HandleAsync(UpdateFloorOrderRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.UpdateOrderAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Waiter.Floor.Bff.Contracts/Contracts/WaiterAssignmentContract.cs b/src/Waiter.Floor.Bff.Contracts/Contracts/WaiterAssignmentContract.cs
new file mode 100644
index 0000000..57cc653
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Contracts/Contracts/WaiterAssignmentContract.cs
@@ -0,0 +1,7 @@
+namespace Waiter.Floor.Bff.Contracts.Contracts;
+
+public sealed record WaiterAssignmentContract(
+ string WaiterId,
+ string TableId,
+ string Status,
+ int ActiveOrders);
diff --git a/src/Waiter.Floor.Bff.Contracts/Requests/UpdateFloorOrderRequest.cs b/src/Waiter.Floor.Bff.Contracts/Requests/UpdateFloorOrderRequest.cs
new file mode 100644
index 0000000..be6d57f
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Contracts/Requests/UpdateFloorOrderRequest.cs
@@ -0,0 +1,7 @@
+namespace Waiter.Floor.Bff.Contracts.Requests;
+
+public sealed record UpdateFloorOrderRequest(
+ string ContextId,
+ string TableId,
+ string OrderId,
+ int ItemCount);
diff --git a/src/Waiter.Floor.Bff.Contracts/Responses/GetWaiterAssignmentsResponse.cs b/src/Waiter.Floor.Bff.Contracts/Responses/GetWaiterAssignmentsResponse.cs
index c241257..05edbac 100644
--- a/src/Waiter.Floor.Bff.Contracts/Responses/GetWaiterAssignmentsResponse.cs
+++ b/src/Waiter.Floor.Bff.Contracts/Responses/GetWaiterAssignmentsResponse.cs
@@ -1,3 +1,10 @@
+using Waiter.Floor.Bff.Contracts.Contracts;
+
namespace Waiter.Floor.Bff.Contracts.Responses;
-public sealed record GetWaiterAssignmentsResponse(string ContextId, string Summary);
+public sealed record GetWaiterAssignmentsResponse(
+ string ContextId,
+ string LocationId,
+ string Summary,
+ IReadOnlyCollection Assignments,
+ IReadOnlyCollection RecentActivity);
diff --git a/src/Waiter.Floor.Bff.Contracts/Responses/GetWaiterRecentActivityResponse.cs b/src/Waiter.Floor.Bff.Contracts/Responses/GetWaiterRecentActivityResponse.cs
new file mode 100644
index 0000000..f45831d
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Contracts/Responses/GetWaiterRecentActivityResponse.cs
@@ -0,0 +1,7 @@
+namespace Waiter.Floor.Bff.Contracts.Responses;
+
+public sealed record GetWaiterRecentActivityResponse(
+ string ContextId,
+ string LocationId,
+ string Summary,
+ IReadOnlyCollection RecentActivity);
diff --git a/src/Waiter.Floor.Bff.Contracts/Responses/SubmitFloorOrderResponse.cs b/src/Waiter.Floor.Bff.Contracts/Responses/SubmitFloorOrderResponse.cs
index fc3c9e2..d361646 100644
--- a/src/Waiter.Floor.Bff.Contracts/Responses/SubmitFloorOrderResponse.cs
+++ b/src/Waiter.Floor.Bff.Contracts/Responses/SubmitFloorOrderResponse.cs
@@ -1,6 +1,9 @@
namespace Waiter.Floor.Bff.Contracts.Responses;
public sealed record SubmitFloorOrderResponse(
+ string ContextId,
string OrderId,
bool Accepted,
- string Message);
+ string Summary,
+ string Status,
+ DateTime ProcessedAtUtc);
diff --git a/src/Waiter.Floor.Bff.Contracts/Responses/UpdateFloorOrderResponse.cs b/src/Waiter.Floor.Bff.Contracts/Responses/UpdateFloorOrderResponse.cs
new file mode 100644
index 0000000..c1ba664
--- /dev/null
+++ b/src/Waiter.Floor.Bff.Contracts/Responses/UpdateFloorOrderResponse.cs
@@ -0,0 +1,9 @@
+namespace Waiter.Floor.Bff.Contracts.Responses;
+
+public sealed record UpdateFloorOrderResponse(
+ string ContextId,
+ string OrderId,
+ bool Accepted,
+ string Summary,
+ string Status,
+ DateTime ProcessedAtUtc);
diff --git a/src/Waiter.Floor.Bff.Rest/Program.cs b/src/Waiter.Floor.Bff.Rest/Program.cs
index aaae0f1..ef40474 100644
--- a/src/Waiter.Floor.Bff.Rest/Program.cs
+++ b/src/Waiter.Floor.Bff.Rest/Program.cs
@@ -10,9 +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 operationsBaseAddress = configuration["OperationsService:BaseAddress"] ?? "http://operations-service:8080";
+ httpClient.BaseAddress = new Uri($"{operationsBaseAddress.TrimEnd('/')}/");
+});
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
builder.Services.AddHttpClient("ThalosAuth");
var app = builder.Build();
@@ -44,6 +51,24 @@ app.MapGet("/api/waiter/floor/assignments", async (
return Results.Ok(await handler.HandleAsync(request, ct));
});
+app.MapGet("/api/waiter/floor/activity", async (
+ string contextId,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ IGetWaiterRecentActivityHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ var request = new GetWaiterAssignmentsRequest(contextId);
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
app.MapPost("/api/waiter/floor/orders", async (
SubmitFloorOrderRequest request,
HttpContext context,
@@ -61,6 +86,25 @@ app.MapPost("/api/waiter/floor/orders", async (
return Results.Ok(await handler.HandleAsync(request, ct));
});
+app.MapPut("/api/waiter/floor/orders/{orderId}", async (
+ string orderId,
+ UpdateFloorOrderBody requestBody,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ IUpdateFloorOrderHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ var request = new UpdateFloorOrderRequest(requestBody.ContextId, requestBody.TableId, orderId, requestBody.ItemCount);
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
app.MapGet("/health", () => Results.Ok(new { status = "ok", service = "waiter-floor-bff" }));
app.MapGet("/healthz", () => Results.Ok(new { status = "ok", service = "waiter-floor-bff" }));
@@ -184,3 +228,4 @@ static IResult ErrorResponse(int statusCode, string code, string message, string
}
sealed record AuthErrorResponse(string Code, string Message, string CorrelationId);
+sealed record UpdateFloorOrderBody(string ContextId, string TableId, int ItemCount);
diff --git a/tests/Waiter.Floor.Bff.Application.UnitTests/OperationsWaiterServiceClientTests.cs b/tests/Waiter.Floor.Bff.Application.UnitTests/OperationsWaiterServiceClientTests.cs
new file mode 100644
index 0000000..8e7d977
--- /dev/null
+++ b/tests/Waiter.Floor.Bff.Application.UnitTests/OperationsWaiterServiceClientTests.cs
@@ -0,0 +1,120 @@
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using Waiter.Floor.Bff.Application.Adapters;
+using Waiter.Floor.Bff.Contracts.Requests;
+
+namespace Waiter.Floor.Bff.Application.UnitTests;
+
+public sealed class OperationsWaiterServiceClientTests
+{
+ [Fact]
+ public async Task FetchAssignmentsAsync_MapsAssignmentsAndRecentActivity()
+ {
+ var client = CreateClient("""
+ {
+ "contextId": "demo-context",
+ "locationId": "restaurant-demo",
+ "summary": "2 active waiter assignments are currently visible.",
+ "assignments": [
+ { "waiterId": "waiter-01", "tableId": "T-12", "status": "serving", "activeOrders": 2 }
+ ],
+ "recentActivity": [
+ "demo-context: table T-12 requested dessert menus"
+ ]
+ }
+ """);
+ var adapter = new OperationsWaiterServiceClient(client);
+
+ var response = await adapter.FetchAssignmentsAsync(new GetWaiterAssignmentsRequest("demo-context"), CancellationToken.None);
+
+ Assert.Equal("restaurant-demo", response.LocationId);
+ Assert.Single(response.Assignments);
+ Assert.Single(response.RecentActivity);
+ Assert.Equal("waiter-01", response.Assignments.Single().WaiterId);
+ }
+
+ [Fact]
+ public async Task FetchRecentActivityAsync_ProjectsActivityOnlyResponse()
+ {
+ var client = CreateClient("""
+ {
+ "contextId": "demo-context",
+ "locationId": "restaurant-demo",
+ "summary": "2 active waiter assignments are currently visible.",
+ "assignments": [],
+ "recentActivity": [
+ "demo-context: table T-08 is waiting for payment capture"
+ ]
+ }
+ """);
+ var adapter = new OperationsWaiterServiceClient(client);
+
+ var response = await adapter.FetchRecentActivityAsync(new GetWaiterAssignmentsRequest("demo-context"), CancellationToken.None);
+
+ Assert.Equal("demo-context", response.ContextId);
+ Assert.Single(response.RecentActivity);
+ }
+
+ [Fact]
+ public async Task SubmitOrderAsync_MapsOperationsPayloadToSubmitResponse()
+ {
+ var client = CreateClient("""
+ {
+ "contextId": "demo-context",
+ "orderId": "ORD-42",
+ "accepted": true,
+ "summary": "Order ORD-42 for table T-12 was accepted with 3 items.",
+ "status": "queued",
+ "submittedAtUtc": "2026-03-31T10:15:00Z"
+ }
+ """);
+ var adapter = new OperationsWaiterServiceClient(client);
+
+ var response = await adapter.SubmitOrderAsync(new SubmitFloorOrderRequest("demo-context", "T-12", "ORD-42", 3), CancellationToken.None);
+
+ Assert.True(response.Accepted);
+ Assert.Equal("queued", response.Status);
+ Assert.Equal("demo-context", response.ContextId);
+ }
+
+ [Fact]
+ public async Task UpdateOrderAsync_PrefixesAcceptedUpdateSummary()
+ {
+ var client = CreateClient("""
+ {
+ "contextId": "demo-context",
+ "orderId": "ORD-42",
+ "accepted": true,
+ "summary": "Order ORD-42 for table T-12 was accepted with 4 items.",
+ "status": "queued",
+ "submittedAtUtc": "2026-03-31T10:15:00Z"
+ }
+ """);
+ var adapter = new OperationsWaiterServiceClient(client);
+
+ var response = await adapter.UpdateOrderAsync(new UpdateFloorOrderRequest("demo-context", "T-12", "ORD-42", 4), CancellationToken.None);
+
+ Assert.Contains("Updated order ORD-42.", response.Summary);
+ Assert.Equal("queued", response.Status);
+ }
+
+ private static HttpClient CreateClient(string json)
+ {
+ return new HttpClient(new StubHttpMessageHandler(json))
+ {
+ BaseAddress = new Uri("http://operations-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")
+ });
+ }
+ }
+}
diff --git a/tests/Waiter.Floor.Bff.Application.UnitTests/Waiter.Floor.Bff.Application.UnitTests.csproj b/tests/Waiter.Floor.Bff.Application.UnitTests/Waiter.Floor.Bff.Application.UnitTests.csproj
new file mode 100644
index 0000000..2812800
--- /dev/null
+++ b/tests/Waiter.Floor.Bff.Application.UnitTests/Waiter.Floor.Bff.Application.UnitTests.csproj
@@ -0,0 +1,19 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+