Compare commits

..

4 Commits

Author SHA1 Message Date
José René White Enciso
869248257b feat(pos-transactions-bff): align lifecycle transaction detail 2026-03-31 19:59:32 -06:00
José René White Enciso
2ea4e9f553 docs(pos-transactions-bff): align payable check workflows 2026-03-31 18:54:42 -06:00
José René White Enciso
2ba208f27e feat(pos-transactions-bff): add pos workflow endpoints 2026-03-31 16:33:40 -06:00
José René White Enciso
a4e5f0252d merge: integrate pos-transactions-bff auth and web updates 2026-03-11 12:39:31 -06:00
22 changed files with 482 additions and 27 deletions

View File

@ -4,4 +4,7 @@
<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>

View File

@ -0,0 +1,31 @@
# 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.

View File

@ -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
- Order lifecycle consistency and state transitions. - POS projection over shared restaurant payable-check rules.
- Kitchen queue and dispatch optimization hooks. - POS transaction detail and recent payment activity views that stay aligned with served-and-payable restaurant state.
- 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.

View File

@ -22,7 +22,8 @@ docker run --rm -p 8080:8080 --name pos-transactions-bff agilewebs/pos-transacti
## Runtime Notes ## 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. - 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 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
- Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior. - Pos-transactions now delegates workflow snapshots to `operations-service`, which projects payable shared-lifecycle checks from `operations-dal`.
- 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.

View File

@ -7,6 +7,8 @@ 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

View File

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

View File

@ -5,6 +5,8 @@ namespace Pos.Transactions.Bff.Application.Adapters;
public interface IPosTransactionsServiceClient public interface IPosTransactionsServiceClient
{ {
Task<GetPosTransactionSummaryResponse> FetchAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken); Task<GetPosTransactionSummaryResponse> FetchSummaryAsync(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);
} }

View File

@ -0,0 +1,138 @@
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);
}

View File

@ -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<GetPosTransactionDetailResponse> HandleAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken)
{
return serviceClient.FetchDetailAsync(request, cancellationToken);
}
}

View File

@ -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.FetchAsync(request, cancellationToken); return serviceClient.FetchSummaryAsync(request, cancellationToken);
} }
} }

View File

@ -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<GetRecentPosPaymentsResponse> HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
{
return serviceClient.FetchRecentPaymentsAsync(request, cancellationToken);
}
}

View File

@ -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<GetPosTransactionDetailResponse> HandleAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken);
}

View File

@ -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<GetRecentPosPaymentsResponse> HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
}

View File

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

View File

@ -0,0 +1,3 @@
namespace Pos.Transactions.Bff.Contracts.Requests;
public sealed record GetPosTransactionDetailRequest(string ContextId, string TransactionId);

View File

@ -4,4 +4,6 @@ public sealed record CapturePosPaymentResponse(
string ContextId, string ContextId,
string TransactionId, string TransactionId,
bool Succeeded, bool Succeeded,
string Summary); string Summary,
string Status,
DateTime CapturedAtUtc);

View File

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

View File

@ -1,3 +1,10 @@
using Pos.Transactions.Bff.Contracts.Contracts;
namespace Pos.Transactions.Bff.Contracts.Responses; 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<PosPaymentActivityContract> RecentPayments);

View File

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

View File

@ -10,8 +10,15 @@ 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<IPosTransactionsServiceClient, DefaultPosTransactionsServiceClient>(); builder.Services.AddHttpClient<IPosTransactionsServiceClient, OperationsPosTransactionsServiceClient>((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<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");
@ -44,6 +51,43 @@ 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,

View File

@ -0,0 +1,146 @@
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"
}
}
""";
}

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/Pos.Transactions.Bff.Application/Pos.Transactions.Bff.Application.csproj" />
</ItemGroup>
</Project>