Compare commits
No commits in common. "869248257bcde572af1364dedbbae90e248825fc" and "2ba208f27eed00cdfd4213b20a80aa94e2041fc3" have entirely different histories.
869248257b
...
2ba208f27e
@ -21,11 +21,9 @@ This BFF exposes POS summary, transaction detail, recent payment activity, and p
|
|||||||
- Default runtime target: `http://operations-service:8080`
|
- Default runtime target: `http://operations-service:8080`
|
||||||
- Internal upstream routes:
|
- Internal upstream routes:
|
||||||
- `GET /internal/operations/pos/summary`
|
- `GET /internal/operations/pos/summary`
|
||||||
- `GET /internal/operations/pos/transactions/{transactionId}`
|
|
||||||
- `POST /internal/operations/pos/payments`
|
- `POST /internal/operations/pos/payments`
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- POS summary and detail are derived from payable restaurant checks exposed by `operations-service`, not from an independent payment event store.
|
- Transaction detail is currently derived from the recent payment snapshot returned by `operations-service`.
|
||||||
- Transaction detail now comes from a dedicated lifecycle-backed upstream route instead of being projected from the summary snapshot.
|
|
||||||
- Correlation IDs are preserved through Thalos session checks and operations-service calls.
|
- Correlation IDs are preserved through Thalos session checks and operations-service calls.
|
||||||
|
|||||||
@ -9,10 +9,11 @@ pos-transactions-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
|
||||||
- POS projection over shared restaurant payable-check rules.
|
- Order lifecycle consistency and state transitions.
|
||||||
- POS transaction detail and recent payment activity views that stay aligned with served-and-payable restaurant state.
|
- Kitchen queue and dispatch optimization hooks.
|
||||||
- Cross-app continuity from kitchen completion to payment eligibility and final capture.
|
|
||||||
- Operations control-plane policies (flags, service windows, overrides).
|
- 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
|
## 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.
|
||||||
|
|||||||
@ -39,6 +39,5 @@ docker run --rm -p 8080:8080 --name pos-transactions-bff agilewebs/pos-transacti
|
|||||||
- Integration artifact path: `greenfield/demo/restaurant/docker-compose.yml`
|
- Integration artifact path: `greenfield/demo/restaurant/docker-compose.yml`
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- Pos-transactions now delegates workflow snapshots to `operations-service`, which projects payable shared-lifecycle checks from `operations-dal`.
|
- Pos-transactions now delegates workflow snapshots to `operations-service`, but the upstream operations adapter still serves deterministic demo data rather than database-backed state.
|
||||||
- Transaction detail remains a summary projection until a dedicated payable-check detail contract is exposed upstream.
|
|
||||||
- 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.
|
||||||
|
|||||||
@ -21,14 +21,18 @@ public sealed class OperationsPosTransactionsServiceClient(HttpClient httpClient
|
|||||||
|
|
||||||
public async Task<GetPosTransactionDetailResponse> FetchDetailAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken)
|
public async Task<GetPosTransactionDetailResponse> FetchDetailAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var payload = await GetDetailPayloadAsync(request.ContextId, request.TransactionId, 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(
|
return new GetPosTransactionDetailResponse(
|
||||||
payload.ContextId,
|
payload.ContextId,
|
||||||
payload.Summary,
|
payment is null
|
||||||
|
? $"Transaction {request.TransactionId} is not visible in the current POS summary."
|
||||||
|
: $"Transaction {request.TransactionId} is currently {payment.Status}.",
|
||||||
payload.OpenBalance,
|
payload.OpenBalance,
|
||||||
payload.Currency,
|
payload.Currency,
|
||||||
payload.Transaction is null ? null : MapPayment(payload.Transaction));
|
payment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetRecentPosPaymentsResponse> FetchRecentPaymentsAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
|
public async Task<GetRecentPosPaymentsResponse> FetchRecentPaymentsAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
|
||||||
@ -65,18 +69,6 @@ public sealed class OperationsPosTransactionsServiceClient(HttpClient httpClient
|
|||||||
return payload ?? throw new InvalidOperationException("Operations service returned an empty POS summary payload.");
|
return payload ?? throw new InvalidOperationException("Operations service returned an empty POS summary payload.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<GetPosTransactionDetailPayload> GetDetailPayloadAsync(
|
|
||||||
string contextId,
|
|
||||||
string transactionId,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var payload = await httpClient.GetFromJsonAsync<GetPosTransactionDetailPayload>(
|
|
||||||
$"internal/operations/pos/transactions/{Uri.EscapeDataString(transactionId)}?contextId={Uri.EscapeDataString(contextId)}",
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
return payload ?? throw new InvalidOperationException("Operations service returned an empty POS detail payload.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<CapturePosPaymentResponsePayload> CapturePaymentPayloadAsync(
|
private async Task<CapturePosPaymentResponsePayload> CapturePaymentPayloadAsync(
|
||||||
CapturePosPaymentRequest request,
|
CapturePosPaymentRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@ -91,21 +83,16 @@ public sealed class OperationsPosTransactionsServiceClient(HttpClient httpClient
|
|||||||
private static IReadOnlyCollection<PosPaymentActivityContract> MapPayments(IReadOnlyCollection<PosPaymentActivityPayload> payments)
|
private static IReadOnlyCollection<PosPaymentActivityContract> MapPayments(IReadOnlyCollection<PosPaymentActivityPayload> payments)
|
||||||
{
|
{
|
||||||
return payments
|
return payments
|
||||||
.Select(MapPayment)
|
.Select(static payment => new PosPaymentActivityContract(
|
||||||
|
payment.TransactionId,
|
||||||
|
payment.PaymentMethod,
|
||||||
|
payment.Amount,
|
||||||
|
payment.Currency,
|
||||||
|
payment.Status,
|
||||||
|
payment.CapturedAtUtc))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PosPaymentActivityContract MapPayment(PosPaymentActivityPayload payment)
|
|
||||||
{
|
|
||||||
return new PosPaymentActivityContract(
|
|
||||||
payment.TransactionId,
|
|
||||||
payment.PaymentMethod,
|
|
||||||
payment.Amount,
|
|
||||||
payment.Currency,
|
|
||||||
payment.Status,
|
|
||||||
payment.CapturedAtUtc);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record GetPosTransactionSummaryPayload(
|
private sealed record GetPosTransactionSummaryPayload(
|
||||||
string ContextId,
|
string ContextId,
|
||||||
string Summary,
|
string Summary,
|
||||||
@ -121,13 +108,6 @@ public sealed class OperationsPosTransactionsServiceClient(HttpClient httpClient
|
|||||||
string Status,
|
string Status,
|
||||||
DateTime CapturedAtUtc);
|
DateTime CapturedAtUtc);
|
||||||
|
|
||||||
private sealed record GetPosTransactionDetailPayload(
|
|
||||||
string ContextId,
|
|
||||||
string Summary,
|
|
||||||
decimal OpenBalance,
|
|
||||||
string Currency,
|
|
||||||
PosPaymentActivityPayload? Transaction);
|
|
||||||
|
|
||||||
private sealed record CapturePosPaymentResponsePayload(
|
private sealed record CapturePosPaymentResponsePayload(
|
||||||
string ContextId,
|
string ContextId,
|
||||||
string TransactionId,
|
string TransactionId,
|
||||||
|
|||||||
@ -10,10 +10,7 @@ public sealed class OperationsPosTransactionsServiceClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task FetchSummaryAsync_MapsBalanceCurrencyAndPayments()
|
public async Task FetchSummaryAsync_MapsBalanceCurrencyAndPayments()
|
||||||
{
|
{
|
||||||
var adapter = new OperationsPosTransactionsServiceClient(CreateClient(new Dictionary<string, string>
|
var adapter = new OperationsPosTransactionsServiceClient(CreateClient(SummaryPayload));
|
||||||
{
|
|
||||||
["/internal/operations/pos/summary"] = SummaryPayload
|
|
||||||
}));
|
|
||||||
|
|
||||||
var response = await adapter.FetchSummaryAsync(new GetPosTransactionSummaryRequest("demo-context"), CancellationToken.None);
|
var response = await adapter.FetchSummaryAsync(new GetPosTransactionSummaryRequest("demo-context"), CancellationToken.None);
|
||||||
|
|
||||||
@ -25,25 +22,18 @@ public sealed class OperationsPosTransactionsServiceClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task FetchDetailAsync_ReturnsMatchingTransactionWhenPresent()
|
public async Task FetchDetailAsync_ReturnsMatchingTransactionWhenPresent()
|
||||||
{
|
{
|
||||||
var adapter = new OperationsPosTransactionsServiceClient(CreateClient(new Dictionary<string, string>
|
var adapter = new OperationsPosTransactionsServiceClient(CreateClient(SummaryPayload));
|
||||||
{
|
|
||||||
["/internal/operations/pos/transactions/CHK-1002"] = DetailPayload
|
|
||||||
}));
|
|
||||||
|
|
||||||
var response = await adapter.FetchDetailAsync(new GetPosTransactionDetailRequest("demo-context", "CHK-1002"), CancellationToken.None);
|
var response = await adapter.FetchDetailAsync(new GetPosTransactionDetailRequest("demo-context", "POS-9001"), CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(response.Transaction);
|
Assert.NotNull(response.Transaction);
|
||||||
Assert.Equal("CHK-1002", response.Transaction!.TransactionId);
|
Assert.Equal("POS-9001", response.Transaction!.TransactionId);
|
||||||
Assert.Equal("awaiting-payment", response.Transaction.Status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task FetchRecentPaymentsAsync_ReturnsProjectedPaymentHistory()
|
public async Task FetchRecentPaymentsAsync_ReturnsProjectedPaymentHistory()
|
||||||
{
|
{
|
||||||
var adapter = new OperationsPosTransactionsServiceClient(CreateClient(new Dictionary<string, string>
|
var adapter = new OperationsPosTransactionsServiceClient(CreateClient(SummaryPayload));
|
||||||
{
|
|
||||||
["/internal/operations/pos/summary"] = SummaryPayload
|
|
||||||
}));
|
|
||||||
|
|
||||||
var response = await adapter.FetchRecentPaymentsAsync(new GetPosTransactionSummaryRequest("demo-context"), CancellationToken.None);
|
var response = await adapter.FetchRecentPaymentsAsync(new GetPosTransactionSummaryRequest("demo-context"), CancellationToken.None);
|
||||||
|
|
||||||
@ -53,9 +43,7 @@ public sealed class OperationsPosTransactionsServiceClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CapturePaymentAsync_MapsCaptureWorkflowPayload()
|
public async Task CapturePaymentAsync_MapsCaptureWorkflowPayload()
|
||||||
{
|
{
|
||||||
var adapter = new OperationsPosTransactionsServiceClient(CreateClient(new Dictionary<string, string>
|
var adapter = new OperationsPosTransactionsServiceClient(CreateClient("""
|
||||||
{
|
|
||||||
["/internal/operations/pos/payments"] = """
|
|
||||||
{
|
{
|
||||||
"contextId": "demo-context",
|
"contextId": "demo-context",
|
||||||
"transactionId": "POS-9003",
|
"transactionId": "POS-9003",
|
||||||
@ -64,8 +52,7 @@ public sealed class OperationsPosTransactionsServiceClientTests
|
|||||||
"status": "captured",
|
"status": "captured",
|
||||||
"capturedAtUtc": "2026-03-31T14:05:00Z"
|
"capturedAtUtc": "2026-03-31T14:05:00Z"
|
||||||
}
|
}
|
||||||
"""
|
"""));
|
||||||
}));
|
|
||||||
|
|
||||||
var response = await adapter.CapturePaymentAsync(
|
var response = await adapter.CapturePaymentAsync(
|
||||||
new CapturePosPaymentRequest("demo-context", "POS-9003", 19.95m, "USD", "card"),
|
new CapturePosPaymentRequest("demo-context", "POS-9003", 19.95m, "USD", "card"),
|
||||||
@ -75,24 +62,18 @@ public sealed class OperationsPosTransactionsServiceClientTests
|
|||||||
Assert.Equal("captured", response.Status);
|
Assert.Equal("captured", response.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient CreateClient(IReadOnlyDictionary<string, string> routes)
|
private static HttpClient CreateClient(string json)
|
||||||
{
|
{
|
||||||
return new HttpClient(new StubHttpMessageHandler(routes))
|
return new HttpClient(new StubHttpMessageHandler(json))
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri("http://operations-service:8080/")
|
BaseAddress = new Uri("http://operations-service:8080/")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class StubHttpMessageHandler(IReadOnlyDictionary<string, string> routes) : HttpMessageHandler
|
private sealed class StubHttpMessageHandler(string json) : HttpMessageHandler
|
||||||
{
|
{
|
||||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var path = request.RequestUri?.AbsolutePath ?? string.Empty;
|
|
||||||
if (!routes.TryGetValue(path, out var json))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"No stub payload configured for '{path}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||||
@ -103,44 +84,27 @@ public sealed class OperationsPosTransactionsServiceClientTests
|
|||||||
private const string SummaryPayload = """
|
private const string SummaryPayload = """
|
||||||
{
|
{
|
||||||
"contextId": "demo-context",
|
"contextId": "demo-context",
|
||||||
"summary": "2 payable checks are waiting for POS capture.",
|
"summary": "Open POS balance reflects one captured payment and one pending settlement.",
|
||||||
"openBalance": 37.50,
|
"openBalance": 37.50,
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"recentPayments": [
|
"recentPayments": [
|
||||||
{
|
{
|
||||||
"transactionId": "CHK-1002",
|
"transactionId": "POS-9001",
|
||||||
"paymentMethod": "check",
|
"paymentMethod": "card",
|
||||||
"amount": 25.50,
|
"amount": 25.50,
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"status": "awaiting-payment",
|
"status": "captured",
|
||||||
"capturedAtUtc": "2026-03-31T13:30:00Z"
|
"capturedAtUtc": "2026-03-31T13:30:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"transactionId": "CHK-1003",
|
"transactionId": "POS-9002",
|
||||||
"paymentMethod": "check",
|
"paymentMethod": "wallet",
|
||||||
"amount": 12.00,
|
"amount": 12.00,
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"status": "partial-payment",
|
"status": "pending",
|
||||||
"capturedAtUtc": "2026-03-31T13:42:00Z"
|
"capturedAtUtc": "2026-03-31T13:42:00Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private const string DetailPayload = """
|
|
||||||
{
|
|
||||||
"contextId": "demo-context",
|
|
||||||
"summary": "Transaction CHK-1002 maps to check CHK-1002 and is currently awaiting-payment.",
|
|
||||||
"openBalance": 37.50,
|
|
||||||
"currency": "USD",
|
|
||||||
"transaction": {
|
|
||||||
"transactionId": "CHK-1002",
|
|
||||||
"paymentMethod": "check",
|
|
||||||
"amount": 25.50,
|
|
||||||
"currency": "USD",
|
|
||||||
"status": "awaiting-payment",
|
|
||||||
"capturedAtUtc": "2026-03-31T13:30:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user