From 151ef8df68e97f03b9facec348f6732cc5f1a938 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 4 Nov 2026 11:49:56 +0000 Subject: [PATCH 1/2] Auto Generate Estate Id --- .../Mediator/DummyEstateDomainService.cs | 3 ++- .../Services/EstateDomainServiceTests.cs | 13 ++++++------ .../RequestHandlers/EstateRequestHandler.cs | 5 +++-- .../Requests/EstateCommands.cs | 2 +- .../Services/EstateDomainService.cs | 12 ++++++----- .../Services/IdGenerationService.cs | 6 +++++- .../ITransactionProcessorClient.cs | 2 +- .../TransactionProcessorClient.cs | 6 +++--- .../Requests/Estate/CreateEstateRequest.cs | 10 ++-------- .../SpecflowExtensions.cs | 1 - .../TransactionProcessorSteps.cs | 20 ++++++++++--------- TransactionProcessor.Testing/TestData.cs | 8 +++----- .../HandlerTests/WebHandlersTests.cs | 4 +--- .../Endpoints/EstateEndpoints.cs | 4 +++- .../Handlers/EstateHandlers.cs | 4 ++-- docs/api-reference.md | 4 ++-- 16 files changed, 53 insertions(+), 51 deletions(-) diff --git a/TransactionProcessor.BusinessLogic.Tests/Mediator/DummyEstateDomainService.cs b/TransactionProcessor.BusinessLogic.Tests/Mediator/DummyEstateDomainService.cs index bf3aaad6..edb69680 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Mediator/DummyEstateDomainService.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Mediator/DummyEstateDomainService.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using SimpleResults; +using System; using TransactionProcessor.BusinessLogic.Requests; using TransactionProcessor.BusinessLogic.Services; @@ -8,7 +9,7 @@ namespace TransactionProcessor.BusinessLogic.Tests.Mediator; public class DummyEstateDomainService : IEstateDomainService { - public async Task CreateEstate(EstateCommands.CreateEstateCommand command, CancellationToken cancellationToken) => Result.Success(); + public async Task> CreateEstate(EstateCommands.CreateEstateCommand command, CancellationToken cancellationToken) => Result.Success(Guid.NewGuid()); public async Task AddOperatorToEstate(EstateCommands.AddOperatorToEstateCommand command, CancellationToken cancellationToken) => Result.Success(); diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/EstateDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/EstateDomainServiceTests.cs index b5aa4563..ffe69971 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/EstateDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/EstateDomainServiceTests.cs @@ -51,8 +51,9 @@ public async Task EstateDomainService_CreateEstate_EstateIsCreated() { .Save(Arg.Any(), Arg.Any()) .ReturnsAsync(SimpleResults.Result.Success()); - Result result = await this.DomainService.CreateEstate(TestData.Commands.CreateEstateCommand, TestContext.Current.CancellationToken); + Result result = await this.DomainService.CreateEstate(TestData.Commands.CreateEstateCommand, TestContext.Current.CancellationToken); result.IsSuccess.ShouldBeTrue(); + result.Data.ShouldNotBe(Guid.Empty); } @@ -274,7 +275,7 @@ public async Task EstateDomainService_CreateEstate_GetEstateFailed_ResultIsFaile this.AggregateService.GetLatest(Arg.Any(), Arg.Any()) .ReturnsAsync(Result.Failure()); - Result result = await this.DomainService.CreateEstate(TestData.Commands.CreateEstateCommand, TestContext.Current.CancellationToken); + Result result = await this.DomainService.CreateEstate(TestData.Commands.CreateEstateCommand, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); } @@ -285,9 +286,9 @@ public async Task EstateDomainService_CreateEstate_EstateNameEmpty_ResultIsFaile .ReturnsAsync(SimpleResults.Result.Success(new EstateAggregate())); EstateCommands.CreateEstateCommand emptyNameCommand = new EstateCommands.CreateEstateCommand( - new CreateEstateRequest { EstateId = TestData.EstateId, EstateName = String.Empty }); + new CreateEstateRequest { EstateName = String.Empty }); - Result result = await this.DomainService.CreateEstate(emptyNameCommand, TestContext.Current.CancellationToken); + Result result = await this.DomainService.CreateEstate(emptyNameCommand, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); } @@ -300,7 +301,7 @@ public async Task EstateDomainService_CreateEstate_SaveFailed_ResultIsFailed() .Save(Arg.Any(), Arg.Any()) .ReturnsAsync(Result.Failure()); - Result result = await this.DomainService.CreateEstate(TestData.Commands.CreateEstateCommand, TestContext.Current.CancellationToken); + Result result = await this.DomainService.CreateEstate(TestData.Commands.CreateEstateCommand, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); } @@ -402,7 +403,7 @@ public async Task EstateDomainService_CreateEstate_ExceptionThrown_ResultIsFaile this.AggregateService.GetLatest(Arg.Any(), Arg.Any()) .ThrowsAsync(new Exception()); - Result result = await this.DomainService.CreateEstate(TestData.Commands.CreateEstateCommand, TestContext.Current.CancellationToken); + Result result = await this.DomainService.CreateEstate(TestData.Commands.CreateEstateCommand, TestContext.Current.CancellationToken); result.IsFailed.ShouldBeTrue(); } diff --git a/TransactionProcessor.BusinessLogic/RequestHandlers/EstateRequestHandler.cs b/TransactionProcessor.BusinessLogic/RequestHandlers/EstateRequestHandler.cs index 45d6beb1..a6a8cc34 100644 --- a/TransactionProcessor.BusinessLogic/RequestHandlers/EstateRequestHandler.cs +++ b/TransactionProcessor.BusinessLogic/RequestHandlers/EstateRequestHandler.cs @@ -3,13 +3,14 @@ using System.Threading.Tasks; using MediatR; using SimpleResults; +using System; using TransactionProcessor.BusinessLogic.Manager; using TransactionProcessor.BusinessLogic.Requests; using TransactionProcessor.BusinessLogic.Services; namespace TransactionProcessor.BusinessLogic.RequestHandlers { - public class EstateRequestHandler : IRequestHandler, + public class EstateRequestHandler : IRequestHandler>, IRequestHandler, IRequestHandler, IRequestHandler, @@ -35,7 +36,7 @@ public EstateRequestHandler(IEstateDomainService estateDomainService, ITransacti #region Methods - public async Task Handle(EstateCommands.CreateEstateCommand command, CancellationToken cancellationToken) + public async Task> Handle(EstateCommands.CreateEstateCommand command, CancellationToken cancellationToken) { return await this.EstateDomainService.CreateEstate(command, cancellationToken); } diff --git a/TransactionProcessor.BusinessLogic/Requests/EstateCommands.cs b/TransactionProcessor.BusinessLogic/Requests/EstateCommands.cs index 156c1819..c7d1fe37 100644 --- a/TransactionProcessor.BusinessLogic/Requests/EstateCommands.cs +++ b/TransactionProcessor.BusinessLogic/Requests/EstateCommands.cs @@ -9,7 +9,7 @@ namespace TransactionProcessor.BusinessLogic.Requests [ExcludeFromCodeCoverage] public class EstateCommands { - public record CreateEstateCommand(CreateEstateRequest RequestDto) : IRequest; + public record CreateEstateCommand(CreateEstateRequest RequestDto) : IRequest>; public record CreateEstateUserCommand(Guid EstateId, CreateEstateUserRequest RequestDto) : IRequest; diff --git a/TransactionProcessor.BusinessLogic/Services/EstateDomainService.cs b/TransactionProcessor.BusinessLogic/Services/EstateDomainService.cs index 0e42945f..45f0890f 100644 --- a/TransactionProcessor.BusinessLogic/Services/EstateDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/EstateDomainService.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using SecurityService.Client; using SecurityService.DataTransferObjects; -using Shared.DomainDrivenDesign.EventSourcing; using Shared.EventStore.Aggregate; using Shared.Exceptions; using Shared.Results; @@ -20,7 +19,7 @@ public interface IEstateDomainService { #region Methods - Task CreateEstate(EstateCommands.CreateEstateCommand command, CancellationToken cancellationToken); + Task> CreateEstate(EstateCommands.CreateEstateCommand command, CancellationToken cancellationToken); Task AddOperatorToEstate(EstateCommands.AddOperatorToEstateCommand command, CancellationToken cancellationToken); @@ -54,10 +53,13 @@ public EstateDomainService(Func aggregateService, #region Methods - public async Task CreateEstate(EstateCommands.CreateEstateCommand command, + public async Task> CreateEstate(EstateCommands.CreateEstateCommand command, CancellationToken cancellationToken) { try { - Result estateResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest(command.RequestDto.EstateId, ct), command.RequestDto.EstateId, cancellationToken, false); + + Guid estateId = IdGenerationService.GenerateEstateId(command.RequestDto.EstateName); + + Result estateResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest(estateId, ct), estateId, cancellationToken, false); if (estateResult.IsFailed) return ResultHelpers.CreateFailure(estateResult); @@ -71,7 +73,7 @@ public async Task CreateEstate(EstateCommands.CreateEstateCommand comman if (saveResult.IsFailed) return ResultHelpers.CreateFailure(saveResult); - return Result.Success(); + return Result.Success(estateId); } catch (Exception ex) { return Result.Failure(ex.GetExceptionMessages()); diff --git a/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs b/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs index 1115c49b..66bd378f 100644 --- a/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs +++ b/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs @@ -65,11 +65,15 @@ public static Guid GenerateMerchantScheduleAggregateId(Guid estateId, Guid merch }); public static Guid GenerateContractId(Guid estateId, Guid operatorId, String contractName) { - String normalisedContractName = contractName.ToLower().Replace(" ", String.Empty); return IdGenerationService.GenerateUniqueId(new { estateId, operatorId, normalisedContractName }); } + public static Guid GenerateEstateId(String estateName) { + String normalisedEstateName = estateName.ToLower().Replace(" ", String.Empty); + + return IdGenerationService.GenerateUniqueId(new { normalisedEstateName }); + } } } diff --git a/TransactionProcessor.Client/ITransactionProcessorClient.cs b/TransactionProcessor.Client/ITransactionProcessorClient.cs index 3c833dfc..e926be92 100644 --- a/TransactionProcessor.Client/ITransactionProcessorClient.cs +++ b/TransactionProcessor.Client/ITransactionProcessorClient.cs @@ -106,7 +106,7 @@ Task>> GetEstates(String accessToken, Guid estateId, CancellationToken cancellationToken); - Task CreateEstate(String accessToken, + Task> CreateEstate(String accessToken, CreateEstateRequest createEstateRequest, CancellationToken cancellationToken); diff --git a/TransactionProcessor.Client/TransactionProcessorClient.cs b/TransactionProcessor.Client/TransactionProcessorClient.cs index 0e112214..43de9411 100644 --- a/TransactionProcessor.Client/TransactionProcessorClient.cs +++ b/TransactionProcessor.Client/TransactionProcessorClient.cs @@ -66,18 +66,18 @@ public async Task> GetEstate(String accessToken, } } - public async Task CreateEstate(String accessToken, + public async Task> CreateEstate(String accessToken, CreateEstateRequest createEstateRequest, CancellationToken cancellationToken) { String requestUri = this.BuildRequestUrl("/api/estates/"); try { - var result = await this.Post(requestUri, createEstateRequest, accessToken, cancellationToken); + Result result = await this.Post(requestUri, createEstateRequest, accessToken, cancellationToken); if (result.IsFailed) return ResultHelpers.CreateFailure(result); - return Result.Success(); + return Result.Success(result.Data); } catch (Exception ex) { // An exception has occurred, add some additional information to the message diff --git a/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateRequest.cs index 49a6fc2e..2050275f 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateRequest.cs @@ -1,19 +1,13 @@ using System; -using System.ComponentModel.DataAnnotations; namespace TransactionProcessor.DataTransferObjects.Requests.Estate { /// - /// Estate identifier and display name used to create an estate. + /// Display name used to create an estate. The estate identifier is generated by the service. /// - /// {"estateId":"00000000-0000-0000-0000-000000000023","estateName":"Example Name"} + /// {"estateName":"Example Name"} public class CreateEstateRequest { - /// - /// Estate that owns or scopes this operation. - /// - public Guid EstateId { get; set; } - /// /// Display name for the estate. /// diff --git a/TransactionProcessor.IntegrationTesting.Helpers/SpecflowExtensions.cs b/TransactionProcessor.IntegrationTesting.Helpers/SpecflowExtensions.cs index 08366f37..b8139d00 100644 --- a/TransactionProcessor.IntegrationTesting.Helpers/SpecflowExtensions.cs +++ b/TransactionProcessor.IntegrationTesting.Helpers/SpecflowExtensions.cs @@ -935,7 +935,6 @@ public static List ToCreateEstateRequests(this DataTableRow { CreateEstateRequest createEstateRequest = new CreateEstateRequest { - EstateId = Guid.NewGuid(), EstateName = ReqnrollTableHelper.GetStringRowValue(tableRow, "EstateName") }; requests.Add(createEstateRequest); diff --git a/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs b/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs index 4079317b..7a64f653 100644 --- a/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs +++ b/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs @@ -719,24 +719,26 @@ await this.TransactionProcessorClient.GetSettlementByDate(accessToken, } } - public async Task> WhenICreateTheFollowingEstatesX(String accessToken, List requests) - { + public async Task> WhenICreateTheFollowingEstatesX(String accessToken, List requests) { + Dictionary createdEstates = new(); + foreach (CreateEstateRequest createEstateRequest in requests) { - Result? result = await this.TransactionProcessorClient - .CreateEstate(accessToken, createEstateRequest, CancellationToken.None) - .ConfigureAwait(false); + Result result = await this.TransactionProcessorClient + .CreateEstate(accessToken, createEstateRequest, CancellationToken.None) + .ConfigureAwait(false); result.IsSuccess.ShouldBeTrue(result.Message); + createdEstates.Add(result.Data, createEstateRequest); } List results = new List(); - foreach (CreateEstateRequest createEstateRequest in requests) + foreach (KeyValuePair createEstateRequest in createdEstates) { EstateResponse estate = null; await Retry.For(async () => { Result>? getEstatesResult = await this.TransactionProcessorClient - .GetEstates(accessToken, createEstateRequest.EstateId, CancellationToken.None).ConfigureAwait(false); + .GetEstates(accessToken, createEstateRequest.Key, CancellationToken.None).ConfigureAwait(false); List? estates = getEstatesResult.Data; try @@ -745,13 +747,13 @@ await Retry.For(async () => { estates.ShouldNotBeNull(); estates.Count.ShouldBe(1); estate = estates.Single(); - estate.EstateName.ShouldBe(createEstateRequest.EstateName); + estate.EstateName.ShouldBe(createEstateRequest.Value.EstateName); estate.EstateReference.ShouldNotBeNullOrWhiteSpace(); } catch { StringBuilder snapshot = new StringBuilder(); - snapshot.Append($"estateId={createEstateRequest.EstateId}, expectedName={createEstateRequest.EstateName}, "); + snapshot.Append($"estateId={createEstateRequest.Key}, expectedName={createEstateRequest.Value.EstateName}, "); snapshot.Append($"isSuccess={getEstatesResult.IsSuccess}, "); snapshot.Append($"rows={(estates == null ? "null" : estates.Count.ToString())}"); diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index 8b940867..d7b75f0e 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -1791,11 +1791,9 @@ public static VoucherAggregate GetVoucherAggregateWithRecipientMobile() public static Guid SecurityUserId = Guid.Parse("45B74A2E-BF92-44E9-A300-08E5CDEACFE3"); public static CreateEstateRequest CreateEstateRequest => - new CreateEstateRequest - { - EstateId = TestData.EstateId, - EstateName = TestData.EstateName, - }; + new() { + EstateName = TestData.EstateName, + }; public static CreateEstateUserRequest CreateEstateUserRequest => new CreateEstateUserRequest { diff --git a/TransactionProcessor.Tests/HandlerTests/WebHandlersTests.cs b/TransactionProcessor.Tests/HandlerTests/WebHandlersTests.cs index da0ea661..12e7c3d9 100644 --- a/TransactionProcessor.Tests/HandlerTests/WebHandlersTests.cs +++ b/TransactionProcessor.Tests/HandlerTests/WebHandlersTests.cs @@ -251,16 +251,14 @@ public static IEnumerable EstateCases() { yield return Case( "CreateEstate", - SetupSuccess(command => + SetupSuccess(EstateId, command => { - command.RequestDto.EstateId.ShouldBe(EstateId); command.RequestDto.EstateName.ShouldBe("Estate"); }), mediator => EstateHandlers.CreateEstate(mediator, new DefaultHttpContext(), new CreateEstateRequest { - EstateId = EstateId, EstateName = "Estate" }, CancellationToken.None)); diff --git a/TransactionProcessor/Endpoints/EstateEndpoints.cs b/TransactionProcessor/Endpoints/EstateEndpoints.cs index 375d5fe7..19f3a6c7 100644 --- a/TransactionProcessor/Endpoints/EstateEndpoints.cs +++ b/TransactionProcessor/Endpoints/EstateEndpoints.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Routing; using Shared.Authorisation; using Shared.General; +using System; using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using TransactionProcessor.Handlers; @@ -27,7 +28,8 @@ public static IEndpointRouteBuilder MapEstateEndpoints(this IEndpointRouteBuilde estateGroup.MapPost("/", EstateHandlers.CreateEstate) .WithName("CreateEstate").WithSummary("Create an estate") - .WithDescription("Creates an estate using the supplied estate ID and display name."); + .WithDescription("Creates an estate from the supplied display name. The service generates and returns the estate ID.") + .Produces(); estateGroup.MapGet("/{estateId:guid}", EstateHandlers.GetEstate) .WithName("GetEstate").WithSummary("Get an estate") diff --git a/TransactionProcessor/Handlers/EstateHandlers.cs b/TransactionProcessor/Handlers/EstateHandlers.cs index 50b4535d..0e465363 100644 --- a/TransactionProcessor/Handlers/EstateHandlers.cs +++ b/TransactionProcessor/Handlers/EstateHandlers.cs @@ -17,9 +17,9 @@ public static class EstateHandlers public static async Task CreateEstate(IMediator mediator, HttpContext context, CreateEstateRequest createEstateRequest, CancellationToken cancellationToken) { EstateCommands.CreateEstateCommand command = new(createEstateRequest); - Result result = await mediator.Send(command, cancellationToken); + Result result = await mediator.Send(command, cancellationToken); - return ResponseFactory.FromResult(result); + return ResponseFactory.FromResult(result, (estateId) => result.Data); } public static async Task GetEstate(IMediator mediator, HttpContext context, Guid estateId, CancellationToken cancellationToken) diff --git a/docs/api-reference.md b/docs/api-reference.md index c2d1deca..516a347f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -22,7 +22,7 @@ This reference covers the routes currently registered by `TransactionProcessor` | Method | Path | Query | Request body | Success response | |---|---|---|---|---| -| POST | `/api/estates/` | — | `CreateEstateRequest` | Result envelope/body from command result. | +| POST | `/api/estates/` | — | `CreateEstateRequest` (`EstateName: string`) | Generated estate ID (`Guid`). | | GET | `/api/estates/{estateId}` | — | — | `EstateResponse` | | GET | `/api/estates/{estateId}/all` | — | — | `EstateResponse[]` | | PATCH | `/api/estates/{estateId}/users` | — | `CreateEstateUserRequest` | Result envelope/body from command result. | @@ -133,7 +133,7 @@ Fields below are the declared DTO properties. Requiredness is not inferred from | DTO | Fields | |---|---| -| `CreateEstateRequest` | `EstateId: Guid`, `EstateName: string` | +| `CreateEstateRequest` | `EstateName: string` (the service generates `EstateId` from the request data) | | `CreateEstateUserRequest` / `CreateMerchantUserRequest` | `EmailAddress: string`, `Password: string`, `GivenName: string`, `MiddleName: string`, `FamilyName: string` | | `Estate.AssignOperatorRequest` | `OperatorId: Guid` | | `CreateContractRequest` | `OperatorId: Guid`, `Description: string` | From 05c2613981c9b359bb4439631de346ec4220ce49 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 4 Nov 2026 12:19:48 +0000 Subject: [PATCH 2/2] Use invariant casing for estate IDs --- .../Services/IdGenerationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs b/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs index 66bd378f..68d98519 100644 --- a/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs +++ b/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs @@ -71,7 +71,7 @@ public static Guid GenerateContractId(Guid estateId, Guid operatorId, String con } public static Guid GenerateEstateId(String estateName) { - String normalisedEstateName = estateName.ToLower().Replace(" ", String.Empty); + String normalisedEstateName = estateName.ToLowerInvariant().Replace(" ", String.Empty); return IdGenerationService.GenerateUniqueId(new { normalisedEstateName }); }