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(new Dictionary { ["/internal/operations/pos/summary"] = 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(new Dictionary { ["/internal/operations/pos/transactions/CHK-1002"] = DetailPayload })); var response = await adapter.FetchDetailAsync(new GetPosTransactionDetailRequest("demo-context", "CHK-1002"), CancellationToken.None); Assert.NotNull(response.Transaction); Assert.Equal("CHK-1002", response.Transaction!.TransactionId); Assert.Equal("awaiting-payment", response.Transaction.Status); } [Fact] public async Task FetchRecentPaymentsAsync_ReturnsProjectedPaymentHistory() { var adapter = new OperationsPosTransactionsServiceClient(CreateClient(new Dictionary { ["/internal/operations/pos/summary"] = 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(new Dictionary { ["/internal/operations/pos/payments"] = """ { "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(IReadOnlyDictionary routes) { return new HttpClient(new StubHttpMessageHandler(routes)) { BaseAddress = new Uri("http://operations-service:8080/") }; } private sealed class StubHttpMessageHandler(IReadOnlyDictionary routes) : HttpMessageHandler { protected override Task 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) { Content = new StringContent(json, Encoding.UTF8, "application/json") }); } } private const string SummaryPayload = """ { "contextId": "demo-context", "summary": "2 payable checks are waiting for POS capture.", "openBalance": 37.50, "currency": "USD", "recentPayments": [ { "transactionId": "CHK-1002", "paymentMethod": "check", "amount": 25.50, "currency": "USD", "status": "awaiting-payment", "capturedAtUtc": "2026-03-31T13:30:00Z" }, { "transactionId": "CHK-1003", "paymentMethod": "check", "amount": 12.00, "currency": "USD", "status": "partial-payment", "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" } } """; }