diff --git a/Customer.Orders.Bff.slnx b/Customer.Orders.Bff.slnx
index d4b6450..44e245f 100644
--- a/Customer.Orders.Bff.slnx
+++ b/Customer.Orders.Bff.slnx
@@ -4,4 +4,7 @@
+
+
+
diff --git a/docs/api/customer-order-workflows.md b/docs/api/customer-order-workflows.md
new file mode 100644
index 0000000..4d5ca7d
--- /dev/null
+++ b/docs/api/customer-order-workflows.md
@@ -0,0 +1,29 @@
+# Customer Order Workflow API
+
+## Purpose
+
+This BFF exposes customer-facing order submission, status, detail, and history workflows over REST while delegating orchestration to `operations-service`.
+
+## Endpoints
+
+- `GET /api/customer/orders/status?contextId=`
+ - Returns the current customer order status snapshot.
+- `GET /api/customer/orders/history?contextId=`
+ - Returns the recent customer order history snapshot.
+- `GET /api/customer/orders/{orderId}?contextId=`
+ - Returns the current detail projection for a single order within the active customer context.
+- `POST /api/customer/orders`
+ - Submits a customer order snapshot for processing.
+
+## Upstream Dependency
+
+- Base address configuration: `OperationsService:BaseAddress`
+- Default runtime target: `http://operations-service:8080`
+- Internal upstream routes:
+ - `GET /internal/operations/customer/status`
+ - `POST /internal/operations/orders`
+
+## Notes
+
+- Customer order submission currently maps `ItemIds.Count` into the upstream restaurant order workflow because the internal service contract still accepts aggregate item counts.
+- Correlation IDs are preserved through Thalos session checks and operations-service calls.
diff --git a/docs/roadmap/feature-epics.md b/docs/roadmap/feature-epics.md
index 05b7966..6e660f5 100644
--- a/docs/roadmap/feature-epics.md
+++ b/docs/roadmap/feature-epics.md
@@ -10,6 +10,7 @@ customer-orders-bff
## Domain-Specific Candidate Features
- Order lifecycle consistency and state transitions.
+- Customer order detail and history views that stay aligned with operations-service workflows.
- 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 13d294f..cd8a94b 100644
--- a/docs/runbooks/containerization.md
+++ b/docs/runbooks/containerization.md
@@ -23,6 +23,7 @@ docker run --rm -p 8080:8080 --name customer-orders-bff agilewebs/customer-order
## Runtime Notes
- Exposes REST edge endpoints for customer order lifecycle 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 customer-orders-bff agilewebs/customer-order
- Integration artifact path: `greenfield/demo/restaurant/docker-compose.yml`
## Known Limitations
-- Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior.
+- Customer-orders 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 981a858..fb02a2e 100644
--- a/docs/security/auth-enforcement.md
+++ b/docs/security/auth-enforcement.md
@@ -7,6 +7,8 @@ This BFF enforces authenticated access on business endpoints using Thalos sessio
## Protected Endpoints
- `/api/customer/orders/status`
+- `/api/customer/orders/history`
+- `/api/customer/orders/{orderId}`
- `/api/customer/orders`
## Anonymous Endpoints
diff --git a/src/Customer.Orders.Bff.Application/Adapters/DefaultCustomerOrdersServiceClient.cs b/src/Customer.Orders.Bff.Application/Adapters/DefaultCustomerOrdersServiceClient.cs
deleted file mode 100644
index 6786e85..0000000
--- a/src/Customer.Orders.Bff.Application/Adapters/DefaultCustomerOrdersServiceClient.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Customer.Orders.Bff.Contracts.Requests;
-using Customer.Orders.Bff.Contracts.Responses;
-
-namespace Customer.Orders.Bff.Application.Adapters;
-
-public sealed class DefaultCustomerOrdersServiceClient : ICustomerOrdersServiceClient
-{
- public Task FetchAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken)
- {
- return Task.FromResult(new GetCustomerOrderStatusResponse(request.ContextId, "Default service-backed response."));
- }
-
- public Task SubmitOrderAsync(SubmitCustomerOrderRequest request, CancellationToken cancellationToken)
- {
- return Task.FromResult(new SubmitCustomerOrderResponse(request.ContextId, request.OrderId, true, "Order accepted by default adapter."));
- }
-}
diff --git a/src/Customer.Orders.Bff.Application/Adapters/ICustomerOrdersServiceClient.cs b/src/Customer.Orders.Bff.Application/Adapters/ICustomerOrdersServiceClient.cs
index 88c94fa..ed526a9 100644
--- a/src/Customer.Orders.Bff.Application/Adapters/ICustomerOrdersServiceClient.cs
+++ b/src/Customer.Orders.Bff.Application/Adapters/ICustomerOrdersServiceClient.cs
@@ -5,6 +5,8 @@ namespace Customer.Orders.Bff.Application.Adapters;
public interface ICustomerOrdersServiceClient
{
- Task FetchAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken);
+ Task FetchStatusAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken);
+ Task FetchDetailAsync(GetCustomerOrderDetailRequest request, CancellationToken cancellationToken);
+ Task FetchHistoryAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken);
Task SubmitOrderAsync(SubmitCustomerOrderRequest request, CancellationToken cancellationToken);
}
diff --git a/src/Customer.Orders.Bff.Application/Adapters/OperationsCustomerOrdersServiceClient.cs b/src/Customer.Orders.Bff.Application/Adapters/OperationsCustomerOrdersServiceClient.cs
new file mode 100644
index 0000000..1d523c3
--- /dev/null
+++ b/src/Customer.Orders.Bff.Application/Adapters/OperationsCustomerOrdersServiceClient.cs
@@ -0,0 +1,120 @@
+using System.Net.Http.Json;
+using Customer.Orders.Bff.Contracts.Contracts;
+using Customer.Orders.Bff.Contracts.Requests;
+using Customer.Orders.Bff.Contracts.Responses;
+
+namespace Customer.Orders.Bff.Application.Adapters;
+
+public sealed class OperationsCustomerOrdersServiceClient(HttpClient httpClient) : ICustomerOrdersServiceClient
+{
+ public async Task FetchStatusAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await GetStatusPayloadAsync(request.ContextId, cancellationToken);
+
+ return new GetCustomerOrderStatusResponse(
+ payload.ContextId,
+ payload.Summary,
+ MapOrders(payload.Orders),
+ payload.RecentEvents);
+ }
+
+ public async Task FetchDetailAsync(GetCustomerOrderDetailRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await GetStatusPayloadAsync(request.ContextId, cancellationToken);
+ var orders = MapOrders(payload.Orders);
+ var matchingOrder = orders.FirstOrDefault(summary => summary.OrderId == request.OrderId);
+
+ return new GetCustomerOrderDetailResponse(
+ payload.ContextId,
+ matchingOrder is null
+ ? $"Order {request.OrderId} is not visible in the current customer context."
+ : $"Order {request.OrderId} is currently {matchingOrder.Status}.",
+ matchingOrder,
+ payload.RecentEvents);
+ }
+
+ public async Task FetchHistoryAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await GetStatusPayloadAsync(request.ContextId, cancellationToken);
+
+ return new GetCustomerOrderHistoryResponse(
+ payload.ContextId,
+ payload.Summary,
+ MapOrders(payload.Orders),
+ payload.RecentEvents);
+ }
+
+ public async Task SubmitOrderAsync(SubmitCustomerOrderRequest request, CancellationToken cancellationToken)
+ {
+ // operations-service still accepts aggregate item counts for restaurant order submission.
+ var payload = await SubmitOrderPayloadAsync(
+ new SubmitRestaurantOrderPayload(request.ContextId, request.OrderId, request.TableId, request.ItemIds.Count),
+ cancellationToken);
+
+ return new SubmitCustomerOrderResponse(
+ payload.ContextId,
+ payload.OrderId,
+ payload.Accepted,
+ payload.Summary,
+ payload.Status);
+ }
+
+ private async Task GetStatusPayloadAsync(string contextId, CancellationToken cancellationToken)
+ {
+ var payload = await httpClient.GetFromJsonAsync(
+ $"internal/operations/customer/status?contextId={Uri.EscapeDataString(contextId)}",
+ cancellationToken);
+
+ return payload ?? throw new InvalidOperationException("Operations service returned an empty customer order status 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 customer order workflow payload.");
+ }
+
+ private static IReadOnlyCollection MapOrders(IReadOnlyCollection orders)
+ {
+ return orders
+ .Select(static order => new CustomerOrderSummaryContract(
+ order.OrderId,
+ order.TableId,
+ order.Status,
+ order.GuestCount,
+ order.ItemIds))
+ .ToArray();
+ }
+
+ private sealed record GetCustomerOrderStatusPayload(
+ string ContextId,
+ string Summary,
+ IReadOnlyCollection Orders,
+ IReadOnlyCollection RecentEvents);
+
+ private sealed record CustomerOrderPayload(
+ string OrderId,
+ string TableId,
+ string Status,
+ int GuestCount,
+ IReadOnlyCollection ItemIds);
+
+ 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/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderDetailHandler.cs b/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderDetailHandler.cs
new file mode 100644
index 0000000..7a0b47d
--- /dev/null
+++ b/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderDetailHandler.cs
@@ -0,0 +1,13 @@
+using Customer.Orders.Bff.Application.Adapters;
+using Customer.Orders.Bff.Contracts.Requests;
+using Customer.Orders.Bff.Contracts.Responses;
+
+namespace Customer.Orders.Bff.Application.Handlers;
+
+public sealed class GetCustomerOrderDetailHandler(ICustomerOrdersServiceClient serviceClient) : IGetCustomerOrderDetailHandler
+{
+ public Task HandleAsync(GetCustomerOrderDetailRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.FetchDetailAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderHistoryHandler.cs b/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderHistoryHandler.cs
new file mode 100644
index 0000000..d9fed44
--- /dev/null
+++ b/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderHistoryHandler.cs
@@ -0,0 +1,13 @@
+using Customer.Orders.Bff.Application.Adapters;
+using Customer.Orders.Bff.Contracts.Requests;
+using Customer.Orders.Bff.Contracts.Responses;
+
+namespace Customer.Orders.Bff.Application.Handlers;
+
+public sealed class GetCustomerOrderHistoryHandler(ICustomerOrdersServiceClient serviceClient) : IGetCustomerOrderHistoryHandler
+{
+ public Task HandleAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.FetchHistoryAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderStatusHandler.cs b/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderStatusHandler.cs
index 48de8b9..28db0d5 100644
--- a/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderStatusHandler.cs
+++ b/src/Customer.Orders.Bff.Application/Handlers/GetCustomerOrderStatusHandler.cs
@@ -8,6 +8,6 @@ public sealed class GetCustomerOrderStatusHandler(ICustomerOrdersServiceClient s
{
public Task HandleAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken)
{
- return serviceClient.FetchAsync(request, cancellationToken);
+ return serviceClient.FetchStatusAsync(request, cancellationToken);
}
}
diff --git a/src/Customer.Orders.Bff.Application/Handlers/IGetCustomerOrderDetailHandler.cs b/src/Customer.Orders.Bff.Application/Handlers/IGetCustomerOrderDetailHandler.cs
new file mode 100644
index 0000000..bf889d2
--- /dev/null
+++ b/src/Customer.Orders.Bff.Application/Handlers/IGetCustomerOrderDetailHandler.cs
@@ -0,0 +1,9 @@
+using Customer.Orders.Bff.Contracts.Requests;
+using Customer.Orders.Bff.Contracts.Responses;
+
+namespace Customer.Orders.Bff.Application.Handlers;
+
+public interface IGetCustomerOrderDetailHandler
+{
+ Task HandleAsync(GetCustomerOrderDetailRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Customer.Orders.Bff.Application/Handlers/IGetCustomerOrderHistoryHandler.cs b/src/Customer.Orders.Bff.Application/Handlers/IGetCustomerOrderHistoryHandler.cs
new file mode 100644
index 0000000..cf4b27c
--- /dev/null
+++ b/src/Customer.Orders.Bff.Application/Handlers/IGetCustomerOrderHistoryHandler.cs
@@ -0,0 +1,9 @@
+using Customer.Orders.Bff.Contracts.Requests;
+using Customer.Orders.Bff.Contracts.Responses;
+
+namespace Customer.Orders.Bff.Application.Handlers;
+
+public interface IGetCustomerOrderHistoryHandler
+{
+ Task HandleAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Customer.Orders.Bff.Contracts/Contracts/CustomerOrderSummaryContract.cs b/src/Customer.Orders.Bff.Contracts/Contracts/CustomerOrderSummaryContract.cs
new file mode 100644
index 0000000..5ab26e0
--- /dev/null
+++ b/src/Customer.Orders.Bff.Contracts/Contracts/CustomerOrderSummaryContract.cs
@@ -0,0 +1,8 @@
+namespace Customer.Orders.Bff.Contracts.Contracts;
+
+public sealed record CustomerOrderSummaryContract(
+ string OrderId,
+ string TableId,
+ string Status,
+ int GuestCount,
+ IReadOnlyCollection ItemIds);
diff --git a/src/Customer.Orders.Bff.Contracts/Requests/GetCustomerOrderDetailRequest.cs b/src/Customer.Orders.Bff.Contracts/Requests/GetCustomerOrderDetailRequest.cs
new file mode 100644
index 0000000..ced782a
--- /dev/null
+++ b/src/Customer.Orders.Bff.Contracts/Requests/GetCustomerOrderDetailRequest.cs
@@ -0,0 +1,3 @@
+namespace Customer.Orders.Bff.Contracts.Requests;
+
+public sealed record GetCustomerOrderDetailRequest(string ContextId, string OrderId);
diff --git a/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderDetailResponse.cs b/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderDetailResponse.cs
new file mode 100644
index 0000000..3587f8a
--- /dev/null
+++ b/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderDetailResponse.cs
@@ -0,0 +1,9 @@
+using Customer.Orders.Bff.Contracts.Contracts;
+
+namespace Customer.Orders.Bff.Contracts.Responses;
+
+public sealed record GetCustomerOrderDetailResponse(
+ string ContextId,
+ string Summary,
+ CustomerOrderSummaryContract? Order,
+ IReadOnlyCollection RecentEvents);
diff --git a/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderHistoryResponse.cs b/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderHistoryResponse.cs
new file mode 100644
index 0000000..7944c6d
--- /dev/null
+++ b/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderHistoryResponse.cs
@@ -0,0 +1,9 @@
+using Customer.Orders.Bff.Contracts.Contracts;
+
+namespace Customer.Orders.Bff.Contracts.Responses;
+
+public sealed record GetCustomerOrderHistoryResponse(
+ string ContextId,
+ string Summary,
+ IReadOnlyCollection Orders,
+ IReadOnlyCollection RecentEvents);
diff --git a/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderStatusResponse.cs b/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderStatusResponse.cs
index e01c065..6a3aea1 100644
--- a/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderStatusResponse.cs
+++ b/src/Customer.Orders.Bff.Contracts/Responses/GetCustomerOrderStatusResponse.cs
@@ -1,3 +1,9 @@
+using Customer.Orders.Bff.Contracts.Contracts;
+
namespace Customer.Orders.Bff.Contracts.Responses;
-public sealed record GetCustomerOrderStatusResponse(string ContextId, string Summary);
+public sealed record GetCustomerOrderStatusResponse(
+ string ContextId,
+ string Summary,
+ IReadOnlyCollection Orders,
+ IReadOnlyCollection RecentEvents);
diff --git a/src/Customer.Orders.Bff.Contracts/Responses/SubmitCustomerOrderResponse.cs b/src/Customer.Orders.Bff.Contracts/Responses/SubmitCustomerOrderResponse.cs
index 4791ece..df45f19 100644
--- a/src/Customer.Orders.Bff.Contracts/Responses/SubmitCustomerOrderResponse.cs
+++ b/src/Customer.Orders.Bff.Contracts/Responses/SubmitCustomerOrderResponse.cs
@@ -4,4 +4,5 @@ public sealed record SubmitCustomerOrderResponse(
string ContextId,
string OrderId,
bool Accepted,
- string Summary);
+ string Summary,
+ string Status);
diff --git a/src/Customer.Orders.Bff.Rest/Program.cs b/src/Customer.Orders.Bff.Rest/Program.cs
index 8cccb99..6262ec8 100644
--- a/src/Customer.Orders.Bff.Rest/Program.cs
+++ b/src/Customer.Orders.Bff.Rest/Program.cs
@@ -10,8 +10,15 @@ 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");
@@ -44,6 +51,43 @@ app.MapGet("/api/customer/orders/status", async (
return Results.Ok(await handler.HandleAsync(request, ct));
});
+app.MapGet("/api/customer/orders/history", async (
+ string contextId,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ IGetCustomerOrderHistoryHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ var request = new GetCustomerOrderStatusRequest(contextId);
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
+app.MapGet("/api/customer/orders/{orderId}", async (
+ string orderId,
+ string contextId,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ IGetCustomerOrderDetailHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ var request = new GetCustomerOrderDetailRequest(contextId, orderId);
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
app.MapPost("/api/customer/orders", async (
SubmitCustomerOrderRequest request,
HttpContext context,
diff --git a/tests/Customer.Orders.Bff.Application.UnitTests/Customer.Orders.Bff.Application.UnitTests.csproj b/tests/Customer.Orders.Bff.Application.UnitTests/Customer.Orders.Bff.Application.UnitTests.csproj
new file mode 100644
index 0000000..70d0375
--- /dev/null
+++ b/tests/Customer.Orders.Bff.Application.UnitTests/Customer.Orders.Bff.Application.UnitTests.csproj
@@ -0,0 +1,19 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Customer.Orders.Bff.Application.UnitTests/OperationsCustomerOrdersServiceClientTests.cs b/tests/Customer.Orders.Bff.Application.UnitTests/OperationsCustomerOrdersServiceClientTests.cs
new file mode 100644
index 0000000..7a1dd14
--- /dev/null
+++ b/tests/Customer.Orders.Bff.Application.UnitTests/OperationsCustomerOrdersServiceClientTests.cs
@@ -0,0 +1,111 @@
+using System.Net;
+using System.Text;
+using Customer.Orders.Bff.Application.Adapters;
+using Customer.Orders.Bff.Contracts.Requests;
+
+namespace Customer.Orders.Bff.Application.UnitTests;
+
+public sealed class OperationsCustomerOrdersServiceClientTests
+{
+ [Fact]
+ public async Task FetchStatusAsync_MapsOrdersAndEvents()
+ {
+ var adapter = new OperationsCustomerOrdersServiceClient(CreateClient(StatusPayload));
+
+ var response = await adapter.FetchStatusAsync(new GetCustomerOrderStatusRequest("demo-context"), CancellationToken.None);
+
+ Assert.Equal("demo-context", response.ContextId);
+ Assert.NotEmpty(response.Orders);
+ Assert.NotEmpty(response.RecentEvents);
+ }
+
+ [Fact]
+ public async Task FetchDetailAsync_ReturnsMatchingOrderWhenPresent()
+ {
+ var adapter = new OperationsCustomerOrdersServiceClient(CreateClient(StatusPayload));
+
+ var response = await adapter.FetchDetailAsync(new GetCustomerOrderDetailRequest("demo-context", "CO-1001"), CancellationToken.None);
+
+ Assert.NotNull(response.Order);
+ Assert.Equal("CO-1001", response.Order!.OrderId);
+ }
+
+ [Fact]
+ public async Task FetchHistoryAsync_ReturnsProjectedHistorySnapshot()
+ {
+ var adapter = new OperationsCustomerOrdersServiceClient(CreateClient(StatusPayload));
+
+ var response = await adapter.FetchHistoryAsync(new GetCustomerOrderStatusRequest("demo-context"), CancellationToken.None);
+
+ Assert.Equal(2, response.Orders.Count);
+ Assert.Equal(2, response.RecentEvents.Count);
+ }
+
+ [Fact]
+ public async Task SubmitOrderAsync_MapsSubmitPayloadUsingItemCount()
+ {
+ var adapter = new OperationsCustomerOrdersServiceClient(CreateClient("""
+ {
+ "contextId": "demo-context",
+ "orderId": "CO-1009",
+ "accepted": true,
+ "summary": "Order CO-1009 for table T-18 was accepted with 3 items.",
+ "status": "queued",
+ "submittedAtUtc": "2026-03-31T12:30:00Z"
+ }
+ """));
+
+ var response = await adapter.SubmitOrderAsync(
+ new SubmitCustomerOrderRequest("demo-context", "CO-1009", "T-18", 4, ["ITEM-101", "ITEM-202", "ITEM-303"]),
+ CancellationToken.None);
+
+ Assert.True(response.Accepted);
+ 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")
+ });
+ }
+ }
+
+ private const string StatusPayload = """
+ {
+ "contextId": "demo-context",
+ "summary": "2 recent customer orders are visible for the active context.",
+ "orders": [
+ {
+ "orderId": "CO-1001",
+ "tableId": "T-08",
+ "status": "preparing",
+ "guestCount": 2,
+ "itemIds": [ "ITEM-101", "ITEM-202" ]
+ },
+ {
+ "orderId": "CO-1002",
+ "tableId": "T-15",
+ "status": "ready",
+ "guestCount": 4,
+ "itemIds": [ "ITEM-301", "ITEM-404", "ITEM-405" ]
+ }
+ ],
+ "recentEvents": [
+ "CO-1001 moved to preparing at kitchen hot-line station.",
+ "CO-1002 is ready for table pickup."
+ ]
+ }
+ """;
+}