diff --git a/Pos.Transactions.Bff.slnx b/Pos.Transactions.Bff.slnx
index 7b2afad..bd9944b 100644
--- a/Pos.Transactions.Bff.slnx
+++ b/Pos.Transactions.Bff.slnx
@@ -4,4 +4,7 @@
+
+
+
diff --git a/docs/api/pos-transaction-workflows.md b/docs/api/pos-transaction-workflows.md
new file mode 100644
index 0000000..00e92b1
--- /dev/null
+++ b/docs/api/pos-transaction-workflows.md
@@ -0,0 +1,29 @@
+# POS Transaction Workflow API
+
+## Purpose
+
+This BFF exposes POS summary, transaction detail, recent payment activity, and payment capture workflows over REST while delegating orchestration to `operations-service`.
+
+## Endpoints
+
+- `GET /api/pos/transactions/summary?contextId=`
+ - Returns POS balance and recent payment summary.
+- `GET /api/pos/transactions/recent-payments?contextId=`
+ - Returns the recent POS payment activity snapshot.
+- `GET /api/pos/transactions/{transactionId}?contextId=`
+ - Returns the current detail projection for a single recent payment activity item.
+- `POST /api/pos/transactions/payments`
+ - Captures a POS payment.
+
+## Upstream Dependency
+
+- Base address configuration: `OperationsService:BaseAddress`
+- Default runtime target: `http://operations-service:8080`
+- Internal upstream routes:
+ - `GET /internal/operations/pos/summary`
+ - `POST /internal/operations/pos/payments`
+
+## Notes
+
+- Transaction detail is currently derived from the recent payment snapshot returned by `operations-service`.
+- 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 37285b1..d997620 100644
--- a/docs/roadmap/feature-epics.md
+++ b/docs/roadmap/feature-epics.md
@@ -13,6 +13,7 @@ pos-transactions-bff
- Kitchen queue and dispatch optimization hooks.
- Operations control-plane policies (flags, service windows, overrides).
- POS closeout and settlement summary alignment.
+- POS transaction detail and recent payment activity views that stay aligned with operations-service workflows.
## Documentation Contract
Any code change in this repository must include docs updates in the same branch.
diff --git a/docs/runbooks/containerization.md b/docs/runbooks/containerization.md
index fae9bd6..1c93680 100644
--- a/docs/runbooks/containerization.md
+++ b/docs/runbooks/containerization.md
@@ -22,7 +22,8 @@ docker run --rm -p 8080:8080 --name pos-transactions-bff agilewebs/pos-transacti
## Runtime Notes
-- Exposes REST edge endpoints for transaction summary and payment capture.
+- Exposes REST edge endpoints for transaction summary, transaction detail, recent payment activity, and payment capture.
+- 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 pos-transactions-bff agilewebs/pos-transacti
- Integration artifact path: `greenfield/demo/restaurant/docker-compose.yml`
## Known Limitations
-- Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior.
+- Pos-transactions 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 7fb8a6f..6a66fe5 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/pos/transactions/summary`
+- `/api/pos/transactions/recent-payments`
+- `/api/pos/transactions/{transactionId}`
- `/api/pos/transactions/payments`
## Anonymous Endpoints
diff --git a/src/Pos.Transactions.Bff.Application/Adapters/DefaultPosTransactionsServiceClient.cs b/src/Pos.Transactions.Bff.Application/Adapters/DefaultPosTransactionsServiceClient.cs
deleted file mode 100644
index 66440af..0000000
--- a/src/Pos.Transactions.Bff.Application/Adapters/DefaultPosTransactionsServiceClient.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Pos.Transactions.Bff.Contracts.Requests;
-using Pos.Transactions.Bff.Contracts.Responses;
-
-namespace Pos.Transactions.Bff.Application.Adapters;
-
-public sealed class DefaultPosTransactionsServiceClient : IPosTransactionsServiceClient
-{
- public Task FetchAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
- {
- return Task.FromResult(new GetPosTransactionSummaryResponse(request.ContextId, "Default service-backed response."));
- }
-
- public Task CapturePaymentAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken)
- {
- return Task.FromResult(new CapturePosPaymentResponse(request.ContextId, request.TransactionId, true, "Payment captured by default adapter."));
- }
-}
diff --git a/src/Pos.Transactions.Bff.Application/Adapters/IPosTransactionsServiceClient.cs b/src/Pos.Transactions.Bff.Application/Adapters/IPosTransactionsServiceClient.cs
index 9168951..67e041b 100644
--- a/src/Pos.Transactions.Bff.Application/Adapters/IPosTransactionsServiceClient.cs
+++ b/src/Pos.Transactions.Bff.Application/Adapters/IPosTransactionsServiceClient.cs
@@ -5,6 +5,8 @@ namespace Pos.Transactions.Bff.Application.Adapters;
public interface IPosTransactionsServiceClient
{
- Task FetchAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
+ Task FetchSummaryAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
+ Task FetchDetailAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken);
+ Task FetchRecentPaymentsAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
Task CapturePaymentAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken);
}
diff --git a/src/Pos.Transactions.Bff.Application/Adapters/OperationsPosTransactionsServiceClient.cs b/src/Pos.Transactions.Bff.Application/Adapters/OperationsPosTransactionsServiceClient.cs
new file mode 100644
index 0000000..ea64a25
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Application/Adapters/OperationsPosTransactionsServiceClient.cs
@@ -0,0 +1,118 @@
+using System.Net.Http.Json;
+using Pos.Transactions.Bff.Contracts.Contracts;
+using Pos.Transactions.Bff.Contracts.Requests;
+using Pos.Transactions.Bff.Contracts.Responses;
+
+namespace Pos.Transactions.Bff.Application.Adapters;
+
+public sealed class OperationsPosTransactionsServiceClient(HttpClient httpClient) : IPosTransactionsServiceClient
+{
+ public async Task FetchSummaryAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await GetSummaryPayloadAsync(request.ContextId, cancellationToken);
+
+ return new GetPosTransactionSummaryResponse(
+ payload.ContextId,
+ payload.Summary,
+ payload.OpenBalance,
+ payload.Currency,
+ MapPayments(payload.RecentPayments));
+ }
+
+ public async Task FetchDetailAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await GetSummaryPayloadAsync(request.ContextId, cancellationToken);
+ // operations-service currently exposes recent-payment snapshots rather than a dedicated transaction detail endpoint.
+ var payment = MapPayments(payload.RecentPayments).FirstOrDefault(activity => activity.TransactionId == request.TransactionId);
+
+ return new GetPosTransactionDetailResponse(
+ payload.ContextId,
+ payment is null
+ ? $"Transaction {request.TransactionId} is not visible in the current POS summary."
+ : $"Transaction {request.TransactionId} is currently {payment.Status}.",
+ payload.OpenBalance,
+ payload.Currency,
+ payment);
+ }
+
+ public async Task FetchRecentPaymentsAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await GetSummaryPayloadAsync(request.ContextId, cancellationToken);
+
+ return new GetRecentPosPaymentsResponse(
+ payload.ContextId,
+ payload.Summary,
+ payload.OpenBalance,
+ payload.Currency,
+ MapPayments(payload.RecentPayments));
+ }
+
+ public async Task CapturePaymentAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken)
+ {
+ var payload = await CapturePaymentPayloadAsync(request, cancellationToken);
+
+ return new CapturePosPaymentResponse(
+ payload.ContextId,
+ payload.TransactionId,
+ payload.Succeeded,
+ payload.Summary,
+ payload.Status,
+ payload.CapturedAtUtc);
+ }
+
+ private async Task GetSummaryPayloadAsync(string contextId, CancellationToken cancellationToken)
+ {
+ var payload = await httpClient.GetFromJsonAsync(
+ $"internal/operations/pos/summary?contextId={Uri.EscapeDataString(contextId)}",
+ cancellationToken);
+
+ return payload ?? throw new InvalidOperationException("Operations service returned an empty POS summary payload.");
+ }
+
+ private async Task CapturePaymentPayloadAsync(
+ CapturePosPaymentRequest request,
+ CancellationToken cancellationToken)
+ {
+ using var response = await httpClient.PostAsJsonAsync("internal/operations/pos/payments", request, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ var payload = await response.Content.ReadFromJsonAsync(cancellationToken);
+ return payload ?? throw new InvalidOperationException("Operations service returned an empty POS capture payload.");
+ }
+
+ private static IReadOnlyCollection MapPayments(IReadOnlyCollection payments)
+ {
+ return payments
+ .Select(static payment => new PosPaymentActivityContract(
+ payment.TransactionId,
+ payment.PaymentMethod,
+ payment.Amount,
+ payment.Currency,
+ payment.Status,
+ payment.CapturedAtUtc))
+ .ToArray();
+ }
+
+ private sealed record GetPosTransactionSummaryPayload(
+ string ContextId,
+ string Summary,
+ decimal OpenBalance,
+ string Currency,
+ IReadOnlyCollection RecentPayments);
+
+ private sealed record PosPaymentActivityPayload(
+ string TransactionId,
+ string PaymentMethod,
+ decimal Amount,
+ string Currency,
+ string Status,
+ DateTime CapturedAtUtc);
+
+ private sealed record CapturePosPaymentResponsePayload(
+ string ContextId,
+ string TransactionId,
+ bool Succeeded,
+ string Summary,
+ string Status,
+ DateTime CapturedAtUtc);
+}
diff --git a/src/Pos.Transactions.Bff.Application/Handlers/GetPosTransactionDetailHandler.cs b/src/Pos.Transactions.Bff.Application/Handlers/GetPosTransactionDetailHandler.cs
new file mode 100644
index 0000000..a627412
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Application/Handlers/GetPosTransactionDetailHandler.cs
@@ -0,0 +1,13 @@
+using Pos.Transactions.Bff.Application.Adapters;
+using Pos.Transactions.Bff.Contracts.Requests;
+using Pos.Transactions.Bff.Contracts.Responses;
+
+namespace Pos.Transactions.Bff.Application.Handlers;
+
+public sealed class GetPosTransactionDetailHandler(IPosTransactionsServiceClient serviceClient) : IGetPosTransactionDetailHandler
+{
+ public Task HandleAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.FetchDetailAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Pos.Transactions.Bff.Application/Handlers/GetPosTransactionSummaryHandler.cs b/src/Pos.Transactions.Bff.Application/Handlers/GetPosTransactionSummaryHandler.cs
index aa99a1c..7149d56 100644
--- a/src/Pos.Transactions.Bff.Application/Handlers/GetPosTransactionSummaryHandler.cs
+++ b/src/Pos.Transactions.Bff.Application/Handlers/GetPosTransactionSummaryHandler.cs
@@ -8,6 +8,6 @@ public sealed class GetPosTransactionSummaryHandler(IPosTransactionsServiceClien
{
public Task HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
{
- return serviceClient.FetchAsync(request, cancellationToken);
+ return serviceClient.FetchSummaryAsync(request, cancellationToken);
}
}
diff --git a/src/Pos.Transactions.Bff.Application/Handlers/GetRecentPosPaymentsHandler.cs b/src/Pos.Transactions.Bff.Application/Handlers/GetRecentPosPaymentsHandler.cs
new file mode 100644
index 0000000..f1f4df1
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Application/Handlers/GetRecentPosPaymentsHandler.cs
@@ -0,0 +1,13 @@
+using Pos.Transactions.Bff.Application.Adapters;
+using Pos.Transactions.Bff.Contracts.Requests;
+using Pos.Transactions.Bff.Contracts.Responses;
+
+namespace Pos.Transactions.Bff.Application.Handlers;
+
+public sealed class GetRecentPosPaymentsHandler(IPosTransactionsServiceClient serviceClient) : IGetRecentPosPaymentsHandler
+{
+ public Task HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
+ {
+ return serviceClient.FetchRecentPaymentsAsync(request, cancellationToken);
+ }
+}
diff --git a/src/Pos.Transactions.Bff.Application/Handlers/IGetPosTransactionDetailHandler.cs b/src/Pos.Transactions.Bff.Application/Handlers/IGetPosTransactionDetailHandler.cs
new file mode 100644
index 0000000..b9b2bd4
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Application/Handlers/IGetPosTransactionDetailHandler.cs
@@ -0,0 +1,9 @@
+using Pos.Transactions.Bff.Contracts.Requests;
+using Pos.Transactions.Bff.Contracts.Responses;
+
+namespace Pos.Transactions.Bff.Application.Handlers;
+
+public interface IGetPosTransactionDetailHandler
+{
+ Task HandleAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Pos.Transactions.Bff.Application/Handlers/IGetRecentPosPaymentsHandler.cs b/src/Pos.Transactions.Bff.Application/Handlers/IGetRecentPosPaymentsHandler.cs
new file mode 100644
index 0000000..6bb3d2a
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Application/Handlers/IGetRecentPosPaymentsHandler.cs
@@ -0,0 +1,9 @@
+using Pos.Transactions.Bff.Contracts.Requests;
+using Pos.Transactions.Bff.Contracts.Responses;
+
+namespace Pos.Transactions.Bff.Application.Handlers;
+
+public interface IGetRecentPosPaymentsHandler
+{
+ Task HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
+}
diff --git a/src/Pos.Transactions.Bff.Contracts/Contracts/PosPaymentActivityContract.cs b/src/Pos.Transactions.Bff.Contracts/Contracts/PosPaymentActivityContract.cs
new file mode 100644
index 0000000..41be78d
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Contracts/Contracts/PosPaymentActivityContract.cs
@@ -0,0 +1,9 @@
+namespace Pos.Transactions.Bff.Contracts.Contracts;
+
+public sealed record PosPaymentActivityContract(
+ string TransactionId,
+ string PaymentMethod,
+ decimal Amount,
+ string Currency,
+ string Status,
+ DateTime CapturedAtUtc);
diff --git a/src/Pos.Transactions.Bff.Contracts/Requests/GetPosTransactionDetailRequest.cs b/src/Pos.Transactions.Bff.Contracts/Requests/GetPosTransactionDetailRequest.cs
new file mode 100644
index 0000000..dc076df
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Contracts/Requests/GetPosTransactionDetailRequest.cs
@@ -0,0 +1,3 @@
+namespace Pos.Transactions.Bff.Contracts.Requests;
+
+public sealed record GetPosTransactionDetailRequest(string ContextId, string TransactionId);
diff --git a/src/Pos.Transactions.Bff.Contracts/Responses/CapturePosPaymentResponse.cs b/src/Pos.Transactions.Bff.Contracts/Responses/CapturePosPaymentResponse.cs
index 683f96a..f4c1114 100644
--- a/src/Pos.Transactions.Bff.Contracts/Responses/CapturePosPaymentResponse.cs
+++ b/src/Pos.Transactions.Bff.Contracts/Responses/CapturePosPaymentResponse.cs
@@ -4,4 +4,6 @@ public sealed record CapturePosPaymentResponse(
string ContextId,
string TransactionId,
bool Succeeded,
- string Summary);
+ string Summary,
+ string Status,
+ DateTime CapturedAtUtc);
diff --git a/src/Pos.Transactions.Bff.Contracts/Responses/GetPosTransactionDetailResponse.cs b/src/Pos.Transactions.Bff.Contracts/Responses/GetPosTransactionDetailResponse.cs
new file mode 100644
index 0000000..163979a
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Contracts/Responses/GetPosTransactionDetailResponse.cs
@@ -0,0 +1,10 @@
+using Pos.Transactions.Bff.Contracts.Contracts;
+
+namespace Pos.Transactions.Bff.Contracts.Responses;
+
+public sealed record GetPosTransactionDetailResponse(
+ string ContextId,
+ string Summary,
+ decimal OpenBalance,
+ string Currency,
+ PosPaymentActivityContract? Transaction);
diff --git a/src/Pos.Transactions.Bff.Contracts/Responses/GetPosTransactionSummaryResponse.cs b/src/Pos.Transactions.Bff.Contracts/Responses/GetPosTransactionSummaryResponse.cs
index 9bbc25e..d0e21ea 100644
--- a/src/Pos.Transactions.Bff.Contracts/Responses/GetPosTransactionSummaryResponse.cs
+++ b/src/Pos.Transactions.Bff.Contracts/Responses/GetPosTransactionSummaryResponse.cs
@@ -1,3 +1,10 @@
+using Pos.Transactions.Bff.Contracts.Contracts;
+
namespace Pos.Transactions.Bff.Contracts.Responses;
-public sealed record GetPosTransactionSummaryResponse(string ContextId, string Summary);
+public sealed record GetPosTransactionSummaryResponse(
+ string ContextId,
+ string Summary,
+ decimal OpenBalance,
+ string Currency,
+ IReadOnlyCollection RecentPayments);
diff --git a/src/Pos.Transactions.Bff.Contracts/Responses/GetRecentPosPaymentsResponse.cs b/src/Pos.Transactions.Bff.Contracts/Responses/GetRecentPosPaymentsResponse.cs
new file mode 100644
index 0000000..7654b35
--- /dev/null
+++ b/src/Pos.Transactions.Bff.Contracts/Responses/GetRecentPosPaymentsResponse.cs
@@ -0,0 +1,10 @@
+using Pos.Transactions.Bff.Contracts.Contracts;
+
+namespace Pos.Transactions.Bff.Contracts.Responses;
+
+public sealed record GetRecentPosPaymentsResponse(
+ string ContextId,
+ string Summary,
+ decimal OpenBalance,
+ string Currency,
+ IReadOnlyCollection RecentPayments);
diff --git a/src/Pos.Transactions.Bff.Rest/Program.cs b/src/Pos.Transactions.Bff.Rest/Program.cs
index 254eaa0..063a6be 100644
--- a/src/Pos.Transactions.Bff.Rest/Program.cs
+++ b/src/Pos.Transactions.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/pos/transactions/summary", async (
return Results.Ok(await handler.HandleAsync(request, ct));
});
+app.MapGet("/api/pos/transactions/recent-payments", async (
+ string contextId,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ IGetRecentPosPaymentsHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ var request = new GetPosTransactionSummaryRequest(contextId);
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
+app.MapGet("/api/pos/transactions/{transactionId}", async (
+ string transactionId,
+ string contextId,
+ HttpContext context,
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ IGetPosTransactionDetailHandler handler,
+ CancellationToken ct) =>
+{
+ var authError = await EnforceSessionAsync(context, httpClientFactory, configuration, ct);
+ if (authError is not null)
+ {
+ return authError;
+ }
+
+ var request = new GetPosTransactionDetailRequest(contextId, transactionId);
+ return Results.Ok(await handler.HandleAsync(request, ct));
+});
+
app.MapPost("/api/pos/transactions/payments", async (
CapturePosPaymentRequest request,
HttpContext context,
diff --git a/tests/Pos.Transactions.Bff.Application.UnitTests/OperationsPosTransactionsServiceClientTests.cs b/tests/Pos.Transactions.Bff.Application.UnitTests/OperationsPosTransactionsServiceClientTests.cs
new file mode 100644
index 0000000..baf2d22
--- /dev/null
+++ b/tests/Pos.Transactions.Bff.Application.UnitTests/OperationsPosTransactionsServiceClientTests.cs
@@ -0,0 +1,110 @@
+using System.Net;
+using System.Text;
+using Pos.Transactions.Bff.Application.Adapters;
+using Pos.Transactions.Bff.Contracts.Requests;
+
+namespace Pos.Transactions.Bff.Application.UnitTests;
+
+public sealed class OperationsPosTransactionsServiceClientTests
+{
+ [Fact]
+ public async Task FetchSummaryAsync_MapsBalanceCurrencyAndPayments()
+ {
+ var adapter = new OperationsPosTransactionsServiceClient(CreateClient(SummaryPayload));
+
+ var response = await adapter.FetchSummaryAsync(new GetPosTransactionSummaryRequest("demo-context"), CancellationToken.None);
+
+ Assert.Equal(37.50m, response.OpenBalance);
+ Assert.Equal("USD", response.Currency);
+ Assert.Equal(2, response.RecentPayments.Count);
+ }
+
+ [Fact]
+ public async Task FetchDetailAsync_ReturnsMatchingTransactionWhenPresent()
+ {
+ var adapter = new OperationsPosTransactionsServiceClient(CreateClient(SummaryPayload));
+
+ var response = await adapter.FetchDetailAsync(new GetPosTransactionDetailRequest("demo-context", "POS-9001"), CancellationToken.None);
+
+ Assert.NotNull(response.Transaction);
+ Assert.Equal("POS-9001", response.Transaction!.TransactionId);
+ }
+
+ [Fact]
+ public async Task FetchRecentPaymentsAsync_ReturnsProjectedPaymentHistory()
+ {
+ var adapter = new OperationsPosTransactionsServiceClient(CreateClient(SummaryPayload));
+
+ var response = await adapter.FetchRecentPaymentsAsync(new GetPosTransactionSummaryRequest("demo-context"), CancellationToken.None);
+
+ Assert.Equal(2, response.RecentPayments.Count);
+ }
+
+ [Fact]
+ public async Task CapturePaymentAsync_MapsCaptureWorkflowPayload()
+ {
+ var adapter = new OperationsPosTransactionsServiceClient(CreateClient("""
+ {
+ "contextId": "demo-context",
+ "transactionId": "POS-9003",
+ "succeeded": true,
+ "summary": "Captured 19.95 USD using card.",
+ "status": "captured",
+ "capturedAtUtc": "2026-03-31T14:05:00Z"
+ }
+ """));
+
+ var response = await adapter.CapturePaymentAsync(
+ new CapturePosPaymentRequest("demo-context", "POS-9003", 19.95m, "USD", "card"),
+ CancellationToken.None);
+
+ Assert.True(response.Succeeded);
+ Assert.Equal("captured", 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 SummaryPayload = """
+ {
+ "contextId": "demo-context",
+ "summary": "Open POS balance reflects one captured payment and one pending settlement.",
+ "openBalance": 37.50,
+ "currency": "USD",
+ "recentPayments": [
+ {
+ "transactionId": "POS-9001",
+ "paymentMethod": "card",
+ "amount": 25.50,
+ "currency": "USD",
+ "status": "captured",
+ "capturedAtUtc": "2026-03-31T13:30:00Z"
+ },
+ {
+ "transactionId": "POS-9002",
+ "paymentMethod": "wallet",
+ "amount": 12.00,
+ "currency": "USD",
+ "status": "pending",
+ "capturedAtUtc": "2026-03-31T13:42:00Z"
+ }
+ ]
+ }
+ """;
+}
diff --git a/tests/Pos.Transactions.Bff.Application.UnitTests/Pos.Transactions.Bff.Application.UnitTests.csproj b/tests/Pos.Transactions.Bff.Application.UnitTests/Pos.Transactions.Bff.Application.UnitTests.csproj
new file mode 100644
index 0000000..50f3eb8
--- /dev/null
+++ b/tests/Pos.Transactions.Bff.Application.UnitTests/Pos.Transactions.Bff.Application.UnitTests.csproj
@@ -0,0 +1,19 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+