Compare commits
4 Commits
feature/wa
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a347eebde4 | ||
|
|
95dc218f28 | ||
|
|
bf78706997 | ||
|
|
33616af7b8 |
@ -4,4 +4,7 @@
|
|||||||
<Project Path="src/Waiter.Floor.Bff.Contracts/Waiter.Floor.Bff.Contracts.csproj" />
|
<Project Path="src/Waiter.Floor.Bff.Contracts/Waiter.Floor.Bff.Contracts.csproj" />
|
||||||
<Project Path="src/Waiter.Floor.Bff.Rest/Waiter.Floor.Bff.Rest.csproj" />
|
<Project Path="src/Waiter.Floor.Bff.Rest/Waiter.Floor.Bff.Rest.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/tests/">
|
||||||
|
<Project Path="tests/Waiter.Floor.Bff.Application.UnitTests/Waiter.Floor.Bff.Application.UnitTests.csproj" />
|
||||||
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
31
docs/api/waiter-floor-workflows.md
Normal file
31
docs/api/waiter-floor-workflows.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 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=<value>`
|
||||||
|
- Returns assignment summary, location context, current assignments, and recent activity.
|
||||||
|
- `GET /api/waiter/floor/activity?contextId=<value>`
|
||||||
|
- 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.
|
||||||
|
- Both order mutations project into the shared restaurant lifecycle owned by `operations-service`, which means kitchen and POS can observe the same order/check state through the same runtime path.
|
||||||
|
- Assignment and recent-activity reads are already lifecycle-backed through `operations-service`, so this BFF does not need a separate summary-projection fallback for the Stage 49 propagation wave.
|
||||||
|
- Correlation IDs are preserved through Thalos session checks and operations-service calls.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# Waiter Floor Ownership Boundary
|
# Waiter Floor Ownership Boundary
|
||||||
|
|
||||||
- Execution-facing BFF for floor staff workflows.
|
- 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.
|
- Does not own policy/config control-plane concerns.
|
||||||
- Consumes service APIs only; no direct DAL access.
|
- Consumes service APIs only; no direct DAL access.
|
||||||
|
|||||||
@ -9,10 +9,11 @@ waiter-floor-bff
|
|||||||
- Epic 3: Improve observability and operational readiness for demo compose environments.
|
- Epic 3: Improve observability and operational readiness for demo compose environments.
|
||||||
|
|
||||||
## Domain-Specific Candidate Features
|
## Domain-Specific Candidate Features
|
||||||
- Order lifecycle consistency and state transitions.
|
- Waiter-facing projection over the shared restaurant order/check lifecycle.
|
||||||
- Kitchen queue and dispatch optimization hooks.
|
- Waiter assignment visibility with recent floor activity derived from persisted restaurant state.
|
||||||
|
- Waiter order update workflows that stay aligned with service-level restaurant order orchestration.
|
||||||
|
- Cross-app order continuity from waiter submission to kitchen preparation and POS payment.
|
||||||
- Operations control-plane policies (flags, service windows, overrides).
|
- Operations control-plane policies (flags, service windows, overrides).
|
||||||
- POS closeout and settlement summary alignment.
|
|
||||||
|
|
||||||
## Documentation Contract
|
## Documentation Contract
|
||||||
Any code change in this repository must include docs updates in the same branch.
|
Any code change in this repository must include docs updates in the same branch.
|
||||||
|
|||||||
@ -22,7 +22,8 @@ docker run --rm -p 8080:8080 --name waiter-floor-bff agilewebs/waiter-floor-bff:
|
|||||||
|
|
||||||
## Runtime Notes
|
## 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.
|
- Requires `ThalosAuth__BaseAddress` to resolve Thalos session introspection endpoint.
|
||||||
- Returns standardized auth failures (`401|403|503`) with `x-correlation-id` propagation.
|
- Returns standardized auth failures (`401|403|503`) with `x-correlation-id` propagation.
|
||||||
|
|
||||||
@ -38,5 +39,6 @@ docker run --rm -p 8080:8080 --name waiter-floor-bff agilewebs/waiter-floor-bff:
|
|||||||
- Integration artifact path: `greenfield/demo/restaurant/docker-compose.yml`
|
- Integration artifact path: `greenfield/demo/restaurant/docker-compose.yml`
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior.
|
- Waiter-floor now delegates workflow snapshots to `operations-service`, which in turn projects persisted shared lifecycle state from `operations-dal`.
|
||||||
|
- Kitchen-driven progression and POS payment visibility depend on the remaining Stage 46-48 restaurant flow tasks being wired end-to-end.
|
||||||
- Demo PostgreSQL seeds validate integration contracts and smoke determinism, but do not yet imply full persistence implementation parity.
|
- Demo PostgreSQL seeds validate integration contracts and smoke determinism, but do not yet imply full persistence implementation parity.
|
||||||
|
|||||||
@ -7,7 +7,9 @@ This BFF enforces authenticated access on business endpoints using Thalos sessio
|
|||||||
## Protected Endpoints
|
## Protected Endpoints
|
||||||
|
|
||||||
- `/api/waiter/floor/assignments`
|
- `/api/waiter/floor/assignments`
|
||||||
|
- `/api/waiter/floor/activity`
|
||||||
- `/api/waiter/floor/orders`
|
- `/api/waiter/floor/orders`
|
||||||
|
- `/api/waiter/floor/orders/{orderId}`
|
||||||
|
|
||||||
## Anonymous Endpoints
|
## Anonymous Endpoints
|
||||||
|
|
||||||
|
|||||||
@ -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<GetWaiterAssignmentsResponse> FetchAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return Task.FromResult(new GetWaiterAssignmentsResponse(request.ContextId, "Default service-backed response."));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<SubmitFloorOrderResponse> SubmitOrderAsync(SubmitFloorOrderRequest request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return Task.FromResult(new SubmitFloorOrderResponse(request.OrderId, true, "Order accepted by waiter-floor default adapter."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,6 +5,8 @@ namespace Waiter.Floor.Bff.Application.Adapters;
|
|||||||
|
|
||||||
public interface IWaiterServiceClient
|
public interface IWaiterServiceClient
|
||||||
{
|
{
|
||||||
Task<GetWaiterAssignmentsResponse> FetchAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
|
Task<GetWaiterAssignmentsResponse> FetchAssignmentsAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
|
||||||
|
Task<GetWaiterRecentActivityResponse> FetchRecentActivityAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
|
||||||
Task<SubmitFloorOrderResponse> SubmitOrderAsync(SubmitFloorOrderRequest request, CancellationToken cancellationToken);
|
Task<SubmitFloorOrderResponse> SubmitOrderAsync(SubmitFloorOrderRequest request, CancellationToken cancellationToken);
|
||||||
|
Task<UpdateFloorOrderResponse> UpdateOrderAsync(UpdateFloorOrderRequest request, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,121 @@
|
|||||||
|
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<GetWaiterAssignmentsResponse> 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<GetWaiterRecentActivityResponse> 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<SubmitFloorOrderResponse> 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<UpdateFloorOrderResponse> UpdateOrderAsync(UpdateFloorOrderRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var payload = await SubmitOrderPayloadAsync(
|
||||||
|
new SubmitRestaurantOrderPayload(request.ContextId, request.OrderId, request.TableId, request.ItemCount),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
// The waiter BFF deliberately reuses the shared order-write contract so both submit and update
|
||||||
|
// actions stay aligned with the canonical restaurant lifecycle owned by operations-service.
|
||||||
|
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<GetWaiterAssignmentsPayload> GetAssignmentsPayloadAsync(string contextId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var payload = await httpClient.GetFromJsonAsync<GetWaiterAssignmentsPayload>(
|
||||||
|
$"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<SubmitRestaurantOrderResponsePayload> SubmitOrderPayloadAsync(
|
||||||
|
SubmitRestaurantOrderPayload request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var response = await httpClient.PostAsJsonAsync("internal/operations/orders", request, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<SubmitRestaurantOrderResponsePayload>(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<WaiterAssignmentPayload> Assignments,
|
||||||
|
IReadOnlyCollection<string> 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);
|
||||||
|
}
|
||||||
@ -8,6 +8,6 @@ public sealed class GetWaiterAssignmentsHandler(IWaiterServiceClient serviceClie
|
|||||||
{
|
{
|
||||||
public Task<GetWaiterAssignmentsResponse> HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
|
public Task<GetWaiterAssignmentsResponse> HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return serviceClient.FetchAsync(request, cancellationToken);
|
return serviceClient.FetchAssignmentsAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<GetWaiterRecentActivityResponse> HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return serviceClient.FetchRecentActivityAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<GetWaiterRecentActivityResponse> HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@ -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<UpdateFloorOrderResponse> HandleAsync(UpdateFloorOrderRequest request, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@ -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<UpdateFloorOrderResponse> HandleAsync(UpdateFloorOrderRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return serviceClient.UpdateOrderAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
namespace Waiter.Floor.Bff.Contracts.Contracts;
|
||||||
|
|
||||||
|
public sealed record WaiterAssignmentContract(
|
||||||
|
string WaiterId,
|
||||||
|
string TableId,
|
||||||
|
string Status,
|
||||||
|
int ActiveOrders);
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
namespace Waiter.Floor.Bff.Contracts.Requests;
|
||||||
|
|
||||||
|
public sealed record UpdateFloorOrderRequest(
|
||||||
|
string ContextId,
|
||||||
|
string TableId,
|
||||||
|
string OrderId,
|
||||||
|
int ItemCount);
|
||||||
@ -1,3 +1,10 @@
|
|||||||
|
using Waiter.Floor.Bff.Contracts.Contracts;
|
||||||
|
|
||||||
namespace Waiter.Floor.Bff.Contracts.Responses;
|
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<WaiterAssignmentContract> Assignments,
|
||||||
|
IReadOnlyCollection<string> RecentActivity);
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
namespace Waiter.Floor.Bff.Contracts.Responses;
|
||||||
|
|
||||||
|
public sealed record GetWaiterRecentActivityResponse(
|
||||||
|
string ContextId,
|
||||||
|
string LocationId,
|
||||||
|
string Summary,
|
||||||
|
IReadOnlyCollection<string> RecentActivity);
|
||||||
@ -1,6 +1,9 @@
|
|||||||
namespace Waiter.Floor.Bff.Contracts.Responses;
|
namespace Waiter.Floor.Bff.Contracts.Responses;
|
||||||
|
|
||||||
public sealed record SubmitFloorOrderResponse(
|
public sealed record SubmitFloorOrderResponse(
|
||||||
|
string ContextId,
|
||||||
string OrderId,
|
string OrderId,
|
||||||
bool Accepted,
|
bool Accepted,
|
||||||
string Message);
|
string Summary,
|
||||||
|
string Status,
|
||||||
|
DateTime ProcessedAtUtc);
|
||||||
|
|||||||
@ -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);
|
||||||
@ -10,9 +10,16 @@ const string SessionAccessCookieName = "thalos_session";
|
|||||||
const string SessionRefreshCookieName = "thalos_refresh";
|
const string SessionRefreshCookieName = "thalos_refresh";
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Services.AddSingleton<IWaiterServiceClient, DefaultWaiterServiceClient>();
|
builder.Services.AddHttpClient<IWaiterServiceClient, OperationsWaiterServiceClient>((serviceProvider, httpClient) =>
|
||||||
|
{
|
||||||
|
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
|
||||||
|
var operationsBaseAddress = configuration["OperationsService:BaseAddress"] ?? "http://operations-service:8080";
|
||||||
|
httpClient.BaseAddress = new Uri($"{operationsBaseAddress.TrimEnd('/')}/");
|
||||||
|
});
|
||||||
builder.Services.AddSingleton<IGetWaiterAssignmentsHandler, GetWaiterAssignmentsHandler>();
|
builder.Services.AddSingleton<IGetWaiterAssignmentsHandler, GetWaiterAssignmentsHandler>();
|
||||||
|
builder.Services.AddSingleton<IGetWaiterRecentActivityHandler, GetWaiterRecentActivityHandler>();
|
||||||
builder.Services.AddSingleton<ISubmitFloorOrderHandler, SubmitFloorOrderHandler>();
|
builder.Services.AddSingleton<ISubmitFloorOrderHandler, SubmitFloorOrderHandler>();
|
||||||
|
builder.Services.AddSingleton<IUpdateFloorOrderHandler, UpdateFloorOrderHandler>();
|
||||||
builder.Services.AddHttpClient("ThalosAuth");
|
builder.Services.AddHttpClient("ThalosAuth");
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@ -44,6 +51,24 @@ app.MapGet("/api/waiter/floor/assignments", async (
|
|||||||
return Results.Ok(await handler.HandleAsync(request, ct));
|
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 (
|
app.MapPost("/api/waiter/floor/orders", async (
|
||||||
SubmitFloorOrderRequest request,
|
SubmitFloorOrderRequest request,
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
@ -61,6 +86,25 @@ app.MapPost("/api/waiter/floor/orders", async (
|
|||||||
return Results.Ok(await handler.HandleAsync(request, ct));
|
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("/health", () => Results.Ok(new { status = "ok", service = "waiter-floor-bff" }));
|
||||||
app.MapGet("/healthz", () => 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 AuthErrorResponse(string Code, string Message, string CorrelationId);
|
||||||
|
sealed record UpdateFloorOrderBody(string ContextId, string TableId, int ItemCount);
|
||||||
|
|||||||
@ -0,0 +1,124 @@
|
|||||||
|
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 tables currently require floor attention.",
|
||||||
|
"assignments": [
|
||||||
|
{ "waiterId": "service-pool", "tableId": "T-12", "status": "Preparing", "activeOrders": 2 }
|
||||||
|
],
|
||||||
|
"recentActivity": [
|
||||||
|
"Order ORD-1002 is currently preparing for table T-12."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
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("service-pool", response.Assignments.Single().WaiterId);
|
||||||
|
Assert.Equal("Preparing", response.Assignments.Single().Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FetchRecentActivityAsync_ProjectsActivityOnlyResponse()
|
||||||
|
{
|
||||||
|
var client = CreateClient("""
|
||||||
|
{
|
||||||
|
"contextId": "demo-context",
|
||||||
|
"locationId": "restaurant-demo",
|
||||||
|
"summary": "2 tables currently require floor attention.",
|
||||||
|
"assignments": [],
|
||||||
|
"recentActivity": [
|
||||||
|
"Order ORD-1003 was served at table T-21 and is ready 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);
|
||||||
|
Assert.Contains("floor attention", response.Summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitOrderAsync_MapsSharedLifecycleAcceptanceResponse()
|
||||||
|
{
|
||||||
|
var client = CreateClient("""
|
||||||
|
{
|
||||||
|
"contextId": "demo-context",
|
||||||
|
"orderId": "ORD-42",
|
||||||
|
"accepted": true,
|
||||||
|
"summary": "Order ORD-42 was accepted and is ready for kitchen dispatch.",
|
||||||
|
"status": "accepted",
|
||||||
|
"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("accepted", response.Status);
|
||||||
|
Assert.Equal("demo-context", response.ContextId);
|
||||||
|
Assert.Contains("kitchen dispatch", response.Summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateOrderAsync_PrefixesSharedLifecycleUpdateSummary()
|
||||||
|
{
|
||||||
|
var client = CreateClient("""
|
||||||
|
{
|
||||||
|
"contextId": "demo-context",
|
||||||
|
"orderId": "ORD-42",
|
||||||
|
"accepted": true,
|
||||||
|
"summary": "Order ORD-42 was accepted and is ready for kitchen dispatch.",
|
||||||
|
"status": "accepted",
|
||||||
|
"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.Contains("kitchen dispatch", response.Summary);
|
||||||
|
Assert.Equal("accepted", 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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../src/Waiter.Floor.Bff.Application/Waiter.Floor.Bff.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue
Block a user