Compare commits
No commits in common. "development" and "feature/pos-transactions-bff-auth-hardening" have entirely different histories.
developmen
...
feature/po
@ -4,7 +4,4 @@
|
|||||||
<Project Path="src/Pos.Transactions.Bff.Contracts/Pos.Transactions.Bff.Contracts.csproj" />
|
<Project Path="src/Pos.Transactions.Bff.Contracts/Pos.Transactions.Bff.Contracts.csproj" />
|
||||||
<Project Path="src/Pos.Transactions.Bff.Rest/Pos.Transactions.Bff.Rest.csproj" />
|
<Project Path="src/Pos.Transactions.Bff.Rest/Pos.Transactions.Bff.Rest.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
|
||||||
<Project Path="tests/Pos.Transactions.Bff.Application.UnitTests/Pos.Transactions.Bff.Application.UnitTests.csproj" />
|
|
||||||
</Folder>
|
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
# 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=<value>`
|
|
||||||
- Returns POS balance and recent payment summary.
|
|
||||||
- `GET /api/pos/transactions/recent-payments?contextId=<value>`
|
|
||||||
- Returns the recent POS payment activity snapshot.
|
|
||||||
- `GET /api/pos/transactions/{transactionId}?contextId=<value>`
|
|
||||||
- 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`
|
|
||||||
- `GET /internal/operations/pos/transactions/{transactionId}`
|
|
||||||
- `POST /internal/operations/pos/payments`
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- POS summary and detail are derived from payable restaurant checks exposed by `operations-service`, not from an independent payment event store.
|
|
||||||
- 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.
|
|
||||||
@ -9,10 +9,10 @@ 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.
|
||||||
|
|
||||||
## 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,8 +22,7 @@ docker run --rm -p 8080:8080 --name pos-transactions-bff agilewebs/pos-transacti
|
|||||||
|
|
||||||
## Runtime Notes
|
## Runtime Notes
|
||||||
|
|
||||||
- Exposes REST edge endpoints for transaction summary, transaction detail, recent payment activity, and payment capture.
|
- Exposes REST edge endpoints for transaction summary and payment capture.
|
||||||
- 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.
|
||||||
|
|
||||||
@ -39,6 +38,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`.
|
- Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior.
|
||||||
- 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.
|
||||||
|
|||||||
@ -7,8 +7,6 @@ This BFF enforces authenticated access on business endpoints using Thalos sessio
|
|||||||
## Protected Endpoints
|
## Protected Endpoints
|
||||||
|
|
||||||
- `/api/pos/transactions/summary`
|
- `/api/pos/transactions/summary`
|
||||||
- `/api/pos/transactions/recent-payments`
|
|
||||||
- `/api/pos/transactions/{transactionId}`
|
|
||||||
- `/api/pos/transactions/payments`
|
- `/api/pos/transactions/payments`
|
||||||
|
|
||||||
## Anonymous Endpoints
|
## Anonymous Endpoints
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
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<GetPosTransactionSummaryResponse> FetchAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new GetPosTransactionSummaryResponse(request.ContextId, "Default service-backed response."));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CapturePosPaymentResponse> CapturePaymentAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new CapturePosPaymentResponse(request.ContextId, request.TransactionId, true, "Payment captured by default adapter."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,8 +5,6 @@ namespace Pos.Transactions.Bff.Application.Adapters;
|
|||||||
|
|
||||||
public interface IPosTransactionsServiceClient
|
public interface IPosTransactionsServiceClient
|
||||||
{
|
{
|
||||||
Task<GetPosTransactionSummaryResponse> FetchSummaryAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
|
Task<GetPosTransactionSummaryResponse> FetchAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
|
||||||
Task<GetPosTransactionDetailResponse> FetchDetailAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken);
|
|
||||||
Task<GetRecentPosPaymentsResponse> FetchRecentPaymentsAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
|
|
||||||
Task<CapturePosPaymentResponse> CapturePaymentAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken);
|
Task<CapturePosPaymentResponse> CapturePaymentAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,138 +0,0 @@
|
|||||||
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<GetPosTransactionSummaryResponse> 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<GetPosTransactionDetailResponse> FetchDetailAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var payload = await GetDetailPayloadAsync(request.ContextId, request.TransactionId, cancellationToken);
|
|
||||||
|
|
||||||
return new GetPosTransactionDetailResponse(
|
|
||||||
payload.ContextId,
|
|
||||||
payload.Summary,
|
|
||||||
payload.OpenBalance,
|
|
||||||
payload.Currency,
|
|
||||||
payload.Transaction is null ? null : MapPayment(payload.Transaction));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetRecentPosPaymentsResponse> 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<CapturePosPaymentResponse> 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<GetPosTransactionSummaryPayload> GetSummaryPayloadAsync(string contextId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var payload = await httpClient.GetFromJsonAsync<GetPosTransactionSummaryPayload>(
|
|
||||||
$"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<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(
|
|
||||||
CapturePosPaymentRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using var response = await httpClient.PostAsJsonAsync("internal/operations/pos/payments", request, cancellationToken);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<CapturePosPaymentResponsePayload>(cancellationToken);
|
|
||||||
return payload ?? throw new InvalidOperationException("Operations service returned an empty POS capture payload.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyCollection<PosPaymentActivityContract> MapPayments(IReadOnlyCollection<PosPaymentActivityPayload> payments)
|
|
||||||
{
|
|
||||||
return payments
|
|
||||||
.Select(MapPayment)
|
|
||||||
.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(
|
|
||||||
string ContextId,
|
|
||||||
string Summary,
|
|
||||||
decimal OpenBalance,
|
|
||||||
string Currency,
|
|
||||||
IReadOnlyCollection<PosPaymentActivityPayload> RecentPayments);
|
|
||||||
|
|
||||||
private sealed record PosPaymentActivityPayload(
|
|
||||||
string TransactionId,
|
|
||||||
string PaymentMethod,
|
|
||||||
decimal Amount,
|
|
||||||
string Currency,
|
|
||||||
string Status,
|
|
||||||
DateTime CapturedAtUtc);
|
|
||||||
|
|
||||||
private sealed record GetPosTransactionDetailPayload(
|
|
||||||
string ContextId,
|
|
||||||
string Summary,
|
|
||||||
decimal OpenBalance,
|
|
||||||
string Currency,
|
|
||||||
PosPaymentActivityPayload? Transaction);
|
|
||||||
|
|
||||||
private sealed record CapturePosPaymentResponsePayload(
|
|
||||||
string ContextId,
|
|
||||||
string TransactionId,
|
|
||||||
bool Succeeded,
|
|
||||||
string Summary,
|
|
||||||
string Status,
|
|
||||||
DateTime CapturedAtUtc);
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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<GetPosTransactionDetailResponse> HandleAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return serviceClient.FetchDetailAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,6 +8,6 @@ public sealed class GetPosTransactionSummaryHandler(IPosTransactionsServiceClien
|
|||||||
{
|
{
|
||||||
public Task<GetPosTransactionSummaryResponse> HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
|
public Task<GetPosTransactionSummaryResponse> HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return serviceClient.FetchSummaryAsync(request, cancellationToken);
|
return serviceClient.FetchAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
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<GetRecentPosPaymentsResponse> HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return serviceClient.FetchRecentPaymentsAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
using Pos.Transactions.Bff.Contracts.Requests;
|
|
||||||
using Pos.Transactions.Bff.Contracts.Responses;
|
|
||||||
|
|
||||||
namespace Pos.Transactions.Bff.Application.Handlers;
|
|
||||||
|
|
||||||
public interface IGetPosTransactionDetailHandler
|
|
||||||
{
|
|
||||||
Task<GetPosTransactionDetailResponse> HandleAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
using Pos.Transactions.Bff.Contracts.Requests;
|
|
||||||
using Pos.Transactions.Bff.Contracts.Responses;
|
|
||||||
|
|
||||||
namespace Pos.Transactions.Bff.Application.Handlers;
|
|
||||||
|
|
||||||
public interface IGetRecentPosPaymentsHandler
|
|
||||||
{
|
|
||||||
Task<GetRecentPosPaymentsResponse> HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
namespace Pos.Transactions.Bff.Contracts.Contracts;
|
|
||||||
|
|
||||||
public sealed record PosPaymentActivityContract(
|
|
||||||
string TransactionId,
|
|
||||||
string PaymentMethod,
|
|
||||||
decimal Amount,
|
|
||||||
string Currency,
|
|
||||||
string Status,
|
|
||||||
DateTime CapturedAtUtc);
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
namespace Pos.Transactions.Bff.Contracts.Requests;
|
|
||||||
|
|
||||||
public sealed record GetPosTransactionDetailRequest(string ContextId, string TransactionId);
|
|
||||||
@ -4,6 +4,4 @@ public sealed record CapturePosPaymentResponse(
|
|||||||
string ContextId,
|
string ContextId,
|
||||||
string TransactionId,
|
string TransactionId,
|
||||||
bool Succeeded,
|
bool Succeeded,
|
||||||
string Summary,
|
string Summary);
|
||||||
string Status,
|
|
||||||
DateTime CapturedAtUtc);
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
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);
|
|
||||||
@ -1,10 +1,3 @@
|
|||||||
using Pos.Transactions.Bff.Contracts.Contracts;
|
|
||||||
|
|
||||||
namespace Pos.Transactions.Bff.Contracts.Responses;
|
namespace Pos.Transactions.Bff.Contracts.Responses;
|
||||||
|
|
||||||
public sealed record GetPosTransactionSummaryResponse(
|
public sealed record GetPosTransactionSummaryResponse(string ContextId, string Summary);
|
||||||
string ContextId,
|
|
||||||
string Summary,
|
|
||||||
decimal OpenBalance,
|
|
||||||
string Currency,
|
|
||||||
IReadOnlyCollection<PosPaymentActivityContract> RecentPayments);
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
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<PosPaymentActivityContract> RecentPayments);
|
|
||||||
@ -10,15 +10,8 @@ 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.AddHttpClient<IPosTransactionsServiceClient, OperationsPosTransactionsServiceClient>((serviceProvider, httpClient) =>
|
builder.Services.AddSingleton<IPosTransactionsServiceClient, DefaultPosTransactionsServiceClient>();
|
||||||
{
|
|
||||||
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
|
|
||||||
var operationsBaseAddress = configuration["OperationsService:BaseAddress"] ?? "http://operations-service:8080";
|
|
||||||
httpClient.BaseAddress = new Uri($"{operationsBaseAddress.TrimEnd('/')}/");
|
|
||||||
});
|
|
||||||
builder.Services.AddSingleton<IGetPosTransactionSummaryHandler, GetPosTransactionSummaryHandler>();
|
builder.Services.AddSingleton<IGetPosTransactionSummaryHandler, GetPosTransactionSummaryHandler>();
|
||||||
builder.Services.AddSingleton<IGetPosTransactionDetailHandler, GetPosTransactionDetailHandler>();
|
|
||||||
builder.Services.AddSingleton<IGetRecentPosPaymentsHandler, GetRecentPosPaymentsHandler>();
|
|
||||||
builder.Services.AddSingleton<ICapturePosPaymentHandler, CapturePosPaymentHandler>();
|
builder.Services.AddSingleton<ICapturePosPaymentHandler, CapturePosPaymentHandler>();
|
||||||
builder.Services.AddHttpClient("ThalosAuth");
|
builder.Services.AddHttpClient("ThalosAuth");
|
||||||
|
|
||||||
@ -51,43 +44,6 @@ app.MapGet("/api/pos/transactions/summary", async (
|
|||||||
return Results.Ok(await handler.HandleAsync(request, ct));
|
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 (
|
app.MapPost("/api/pos/transactions/payments", async (
|
||||||
CapturePosPaymentRequest request,
|
CapturePosPaymentRequest request,
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
|
|||||||
@ -1,146 +0,0 @@
|
|||||||
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<string, string>
|
|
||||||
{
|
|
||||||
["/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<string, string>
|
|
||||||
{
|
|
||||||
["/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<string, string>
|
|
||||||
{
|
|
||||||
["/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<string, string>
|
|
||||||
{
|
|
||||||
["/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<string, string> routes)
|
|
||||||
{
|
|
||||||
return new HttpClient(new StubHttpMessageHandler(routes))
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri("http://operations-service:8080/")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class StubHttpMessageHandler(IReadOnlyDictionary<string, string> routes) : HttpMessageHandler
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<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/Pos.Transactions.Bff.Application/Pos.Transactions.Bff.Application.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
Loading…
Reference in New Issue
Block a user