Compare commits

..

4 Commits

Author SHA1 Message Date
José René White Enciso
a347eebde4 docs(waiter-floor-bff): align lifecycle assignment reads 2026-03-31 19:59:32 -06:00
José René White Enciso
95dc218f28 docs(waiter-floor-bff): align waiter workflows to shared lifecycle 2026-03-31 18:50:18 -06:00
José René White Enciso
bf78706997 feat(waiter-floor-bff): add waiter workflow endpoints 2026-03-31 16:26:52 -06:00
José René White Enciso
33616af7b8 merge: integrate waiter-floor-bff auth and web updates 2026-03-11 12:39:53 -06:00
23 changed files with 445 additions and 28 deletions

View File

@ -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>

View 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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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."));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
namespace Waiter.Floor.Bff.Contracts.Contracts;
public sealed record WaiterAssignmentContract(
string WaiterId,
string TableId,
string Status,
int ActiveOrders);

View File

@ -0,0 +1,7 @@
namespace Waiter.Floor.Bff.Contracts.Requests;
public sealed record UpdateFloorOrderRequest(
string ContextId,
string TableId,
string OrderId,
int ItemCount);

View File

@ -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);

View File

@ -0,0 +1,7 @@
namespace Waiter.Floor.Bff.Contracts.Responses;
public sealed record GetWaiterRecentActivityResponse(
string ContextId,
string LocationId,
string Summary,
IReadOnlyCollection<string> RecentActivity);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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")
});
}
}
}

View File

@ -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>