Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,7 @@ public async Task MerchantDomainService_CreateMerchant_MerchantIsCreated(
Result<Guid> result = await this.DomainService.CreateMerchant(command, TestContext.Current.CancellationToken);
result.IsSuccess.ShouldBeTrue();
}

[Fact]
public async Task MerchantDomainService_CreateMerchant_MerchantIdNotSet_MerchantIsCreated() {
this.AggregateService.GetLatest<MerchantAggregate>(Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(Result.Success(TestData.Aggregates.EmptyMerchantAggregate()));
this.AggregateService
.Save(Arg<MerchantAggregate>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(Result.Success());

this.AggregateService.Get<EstateAggregate>(Arg<Guid>.Any(), Arg<CancellationToken>.Any())
.ReturnsAsync(TestData.Aggregates.CreatedEstateAggregate());

MerchantCommands.CreateMerchantCommand command = TestData.Commands.CreateMerchantCommand;
command.RequestDto.MerchantId = null;

Result<Guid> result = await this.DomainService.CreateMerchant(command, TestContext.Current.CancellationToken);
result.IsSuccess.ShouldBeTrue();
}


[Fact]
public async Task MerchantDomainService_CreateMerchant_AlreadyCreated_MerchantIsCreated() {
this.AggregateService.GetLatest<MerchantAggregate>(Arg<Guid>.Any(), Arg<CancellationToken>.Any())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class MerchantRequestHandler : IRequestHandler<MerchantQueries.GetMerchan
IRequestHandler<MerchantQueries.GetMerchantBalanceHistoryQuery, Result<List<MerchantBalanceChangedEntry>>>,
IRequestHandler<MerchantCommands.SwapMerchantDeviceCommand, Result>,
IRequestHandler<MerchantCommands.AddMerchantContractCommand, Result>,
IRequestHandler<MerchantCommands.CreateMerchantCommand, Result>,
IRequestHandler<MerchantCommands.CreateMerchantCommand, Result<Guid>>,
IRequestHandler<MerchantCommands.AssignOperatorToMerchantCommand, Result>,
IRequestHandler<MerchantCommands.AddMerchantDeviceCommand, Result>,
IRequestHandler<MerchantCommands.CreateMerchantUserCommand, Result>,
Expand Down Expand Up @@ -128,7 +128,7 @@ public async Task<Result> Handle(MerchantCommands.AddMerchantContractCommand com
return await this.MerchantDomainService.AddContractToMerchant(command, cancellationToken);
}

public async Task<Result> Handle(MerchantCommands.CreateMerchantCommand command, CancellationToken cancellationToken)
public async Task<Result<Guid>> Handle(MerchantCommands.CreateMerchantCommand command, CancellationToken cancellationToken)
{
return await this.MerchantDomainService.CreateMerchant(command, cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace TransactionProcessor.BusinessLogic.Requests
[ExcludeFromCodeCoverage]
public class MerchantCommands{

public record CreateMerchantCommand(Guid EstateId, CreateMerchantRequest RequestDto) : IRequest<Result>;
public record CreateMerchantCommand(Guid EstateId, CreateMerchantRequest RequestDto) : IRequest<Result<Guid>>;

public record AssignOperatorToMerchantCommand(Guid EstateId, Guid MerchantId, AssignOperatorRequest RequestDto) : IRequest<Result>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ 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);
String normalisedContractName = contractName.ToLowerInvariant().Replace(" ", String.Empty);

return IdGenerationService.GenerateUniqueId(new { estateId, operatorId, normalisedContractName });
}
Expand All @@ -75,5 +75,11 @@ public static Guid GenerateEstateId(String estateName) {

return IdGenerationService.GenerateUniqueId(new { normalisedEstateName });
}

public static Guid GenerateMerchantId(Guid estateId, String merchantName) {
String normalisedMerchantName = merchantName.ToLowerInvariant().Replace(" ", String.Empty);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the merchant name before generating its ID

When a creation request omits Name or explicitly sets it to null, GenerateMerchantId now throws at merchantName.ToLowerInvariant() before the existing validation in MerchantAggregate.Create can run. CreateMerchant catches that exception and returns Result.Failure instead of the previous Result.Invalid("Merchant name must be provided when registering a new merchant"). Validate the name before generating the ID so malformed requests retain the existing validation behavior.

Useful? React with 👍 / 👎.


return IdGenerationService.GenerateUniqueId(new { estateId, normalisedMerchantName });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ namespace TransactionProcessor.BusinessLogic.Services
public interface IMerchantDomainService
{
#region Methods
Task<Result> CreateMerchant(MerchantCommands.CreateMerchantCommand command, CancellationToken cancellationToken);
Task<Result<Guid>> CreateMerchant(MerchantCommands.CreateMerchantCommand command, CancellationToken cancellationToken);
Task<Result> AssignOperatorToMerchant(MerchantCommands.AssignOperatorToMerchantCommand command, CancellationToken cancellationToken);
Task<Result> CreateMerchantUser(MerchantCommands.CreateMerchantUserCommand command, CancellationToken cancellationToken);
Task<Result> AddDeviceToMerchant(MerchantCommands.AddMerchantDeviceCommand command, CancellationToken cancellationToken);
Expand Down Expand Up @@ -199,11 +199,11 @@ private Result EnsureMerchantDepositListCreated(MerchantDepositListAggregate mer
return merchantDepositListAggregate.Create(merchantAggregate, depositDateTime);
}

public async Task<Result> CreateMerchant(MerchantCommands.CreateMerchantCommand command, CancellationToken cancellationToken)
public async Task<Result<Guid>> CreateMerchant(MerchantCommands.CreateMerchantCommand command, CancellationToken cancellationToken)
{
try {
// Check if we have been sent a merchant id to use
Guid merchantId = command.RequestDto.MerchantId ?? Guid.NewGuid();
Guid merchantId = IdGenerationService.GenerateMerchantId(command.EstateId, command.RequestDto.Name);

Result<EstateAggregate> estateResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.Get<EstateAggregate>(command.EstateId, ct), command.EstateId, cancellationToken);
if (estateResult.IsFailed)
Expand All @@ -227,7 +227,7 @@ public async Task<Result> CreateMerchant(MerchantCommands.CreateMerchantCommand
// Set the settlement schedule
SettlementSchedule settlementSchedule = ConvertSettlementSchedule(command.RequestDto.SettlementSchedule);

var stateResult = merchantAggregate.Create(command.EstateId, command.RequestDto.Name, command.RequestDto.CreatedDateTime.GetValueOrDefault(DateTimeOffset.UtcNow),
Result stateResult = merchantAggregate.Create(command.EstateId, command.RequestDto.Name, command.RequestDto.CreatedDateTime.GetValueOrDefault(DateTimeOffset.UtcNow),
address, contact, settlementSchedule);
if (stateResult.IsFailed)
return stateResult;
Expand All @@ -237,7 +237,7 @@ public async Task<Result> CreateMerchant(MerchantCommands.CreateMerchantCommand
if (saveResult.IsFailed)
return ResultHelpers.CreateFailure(saveResult);

return saveResult;
return Result.Success(merchantId);
}
catch (Exception ex) {
return Result.Failure(ex.GetExceptionMessages());
Expand Down
8 changes: 4 additions & 4 deletions TransactionProcessor.Client/ITransactionProcessorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ Task<Result> CreateEstateUser(String accessToken,
CreateEstateUserRequest createEstateUserRequest,
CancellationToken cancellationToken);

Task<Result> CreateMerchant(String accessToken,
Guid estateId,
CreateMerchantRequest createMerchantRequest,
CancellationToken cancellationToken);
Task<Result<Guid>> CreateMerchant(String accessToken,
Guid estateId,
CreateMerchantRequest createMerchantRequest,
CancellationToken cancellationToken);

Task<Result> CreateMerchantSchedule(String accessToken,
Guid estateId,
Expand Down
6 changes: 3 additions & 3 deletions TransactionProcessor.Client/TransactionProcessorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -728,19 +728,19 @@ public async Task<Result> CreateEstateUser(String accessToken,
}
}

public async Task<Result> CreateMerchant(String accessToken,
public async Task<Result<Guid>> CreateMerchant(String accessToken,
Guid estateId,
CreateMerchantRequest createMerchantRequest,
CancellationToken cancellationToken) {
String requestUri = this.BuildRequestUrl($"/api/estates/{estateId}/merchants");

try {
var result = await this.Post<CreateMerchantRequest>(requestUri, createMerchantRequest, accessToken, cancellationToken);
Result<Guid> result = await this.Post<CreateMerchantRequest, Guid>(requestUri, createMerchantRequest, 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System;
using System.ComponentModel.DataAnnotations;
using TransactionProcessor.DataTransferObjects.Responses.Merchant;

namespace TransactionProcessor.DataTransferObjects.Requests.Merchant
{
/// <summary>
/// Merchant identity, contact, address, and settlement schedule used at creation.
/// </summary>
/// <example>{"address":{"addressLine1":"123 Example Street","addressLine2":"123 Example Street","addressLine3":"123 Example Street","addressLine4":"123 Example Street","country":"US","postalCode":"10001","region":"Example City","town":"Example City"},"contact":{"contactName":"Example Name","emailAddress":"customer@example.com","phoneNumber":"+15550100"},"name":"Example Name","settlementSchedule":0,"merchantId":"00000000-0000-0000-0000-000000000227","createdDateTime":"2026-01-15T10:30:00Z"}</example>
/// <example>{"address":{"addressLine1":"123 Example Street","addressLine2":"123 Example Street","addressLine3":"123 Example Street","addressLine4":"123 Example Street","country":"US","postalCode":"10001","region":"Example City","town":"Example City"},"contact":{"contactName":"Example Name","emailAddress":"customer@example.com","phoneNumber":"+15550100"},"name":"Example Name","settlementSchedule":0,"createdDateTime":"2026-01-15T10:30:00Z"}</example>
public class CreateMerchantRequest
{
/// <summary>
Expand All @@ -29,12 +28,7 @@ public class CreateMerchantRequest
/// Settlement schedule assigned to the merchant.
/// </summary>
public SettlementSchedule SettlementSchedule { get; set; }

/// <summary>
/// Optional caller-supplied merchant identifier; when omitted the service creates one.
/// </summary>
public Guid? MerchantId { get; set; }


/// <summary>
/// Optional merchant creation timestamp.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,6 @@ public static List<CreateEstateRequest> ToCreateEstateRequests(this DataTableRow
PostalCode = ReqnrollTableHelper.GetStringRowValue(tableRow, "PostalCode")
},
SettlementSchedule = schedule,
MerchantId = Guid.NewGuid()
};
requests.Add((estateDetails, createMerchantRequest, ReqnrollTableHelper.GetBooleanValue(tableRow, "AgencyBankingEnabled")));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,25 +342,24 @@ await Retry.For(async () =>
}
}

public async Task<List<MerchantResponse>> WhenICreateTheFollowingMerchants(string accessToken, List<(EstateDetails estate, CreateMerchantRequest request, Boolean enableAgencyBanking)> requests)
public async Task<Dictionary<Guid, MerchantResponse>> WhenICreateTheFollowingMerchants(string accessToken, List<(EstateDetails estate, CreateMerchantRequest request, Boolean enableAgencyBanking)> requests)
{
List<MerchantResponse> responses = new List<MerchantResponse>();
Dictionary<Guid, MerchantResponse> createdMerchants = new();

List<(Guid, Guid, String)> merchants = new List<(Guid, Guid, String)>();
List<(Guid, Guid, String)> merchants = new();

foreach ((EstateDetails estate, CreateMerchantRequest request, Boolean enableAgencyBanking) request in requests)
{

Result? result = await this.TransactionProcessorClient
Result<Guid> result = await this.TransactionProcessorClient
.CreateMerchant(accessToken, request.estate.EstateId, request.request, CancellationToken.None)
.ConfigureAwait(false);

result.IsSuccess.ShouldBeTrue(result.Message);


if (request.enableAgencyBanking) {
CreateAgentRequest createAgentRequest = new() {
AgentId = request.request.MerchantId.Value.ToString(),
AgentId = result.Data.ToString(),
DailyLimit = 100000,
Name = request.request.Name,
MinimumFloat = 10000,
Expand All @@ -383,7 +382,7 @@ await Retry.For(async () =>

// Activate the agent
ActivateAgentRequest activateAgentRequest = new ActivateAgentRequest { ActivatedBy = "INTEGRATIONTEST" };
var activateAgentResult = await this.AgencyBankingClient.ActivateAgent(createAgentRequest.AgentId, activateAgentRequest, CancellationToken.None);
Result activateAgentResult = await this.AgencyBankingClient.ActivateAgent(createAgentRequest.AgentId, activateAgentRequest, CancellationToken.None);
activateAgentResult.IsSuccess.ShouldBeTrue(activateAgentResult.Message);

await Retry.For(async () =>
Expand All @@ -399,7 +398,7 @@ await Retry.For(async () =>
ConfigureFloatRequest configureFloatRequest = new ConfigureFloatRequest {
AgentId = createAgentRequest.AgentId, DailyFloatLimit = 100000, MaximumFloat = 100000, MinimumFloat = 10000,
};
var configureFloatResult = await this.AgencyBankingClient.ConfigureFloat(configureFloatRequest, CancellationToken.None);
Result configureFloatResult = await this.AgencyBankingClient.ConfigureFloat(configureFloatRequest, CancellationToken.None);
configureFloatResult.IsSuccess.ShouldBeTrue(configureFloatResult.Message);

await Retry.For(async () =>
Expand All @@ -423,7 +422,7 @@ await Retry.For(async () =>
SourceAccount = "Test Account",
TransactionId = Guid.NewGuid().ToString()
};
var floatCreditResult = await this.AgencyBankingClient.CreditFloat(floatCreditRequest, CancellationToken.None);
Result floatCreditResult = await this.AgencyBankingClient.CreditFloat(floatCreditRequest, CancellationToken.None);
floatCreditResult.IsSuccess.ShouldBeTrue(floatCreditResult.Message);

await Retry.For(async () =>
Expand All @@ -444,7 +443,7 @@ await Retry.For(async () =>
}


merchants.Add((request.estate.EstateId, request.request.MerchantId.Value, request.request.Name));
merchants.Add((request.estate.EstateId, result.Data, request.request.Name));
}

foreach ((Guid, Guid, String) m in merchants)
Expand All @@ -471,10 +470,10 @@ await Retry.For(async () => {
getMerchantBalanceResult2.Data.ShouldNotBeNull();
});
merchant.ShouldNotBeNull();
responses.Add(merchant);
createdMerchants.Add(m.Item2, merchant);
}

return responses;
return createdMerchants;
}

public void ValidateTransactions(List<(TransactionResponse, String, String, String)> transactions)
Expand Down
10 changes: 5 additions & 5 deletions TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,12 @@ public async Task WhenICreateTheFollowingMerchants(DataTable table)
{
List<(EstateDetails estate, CreateMerchantRequest, Boolean)> requests = table.Rows.ToCreateMerchantRequests(this.TestingContext.Estates);

List<DataTransferObjects.Responses.Merchant.MerchantResponse> verifiedMerchants = await this.TransactionProcessorSteps.WhenICreateTheFollowingMerchants(this.TestingContext.AccessToken, requests);
Dictionary<Guid, MerchantResponse> verifiedMerchants = await this.TransactionProcessorSteps.WhenICreateTheFollowingMerchants(this.TestingContext.AccessToken, requests);

foreach (DataTransferObjects.Responses.Merchant.MerchantResponse verifiedMerchant in verifiedMerchants){
EstateDetails estateDetails = this.TestingContext.GetEstateDetails(verifiedMerchant.EstateId);
estateDetails.AddMerchant(verifiedMerchant);
this.TestingContext.Logger.LogInformation($"Merchant {verifiedMerchant.MerchantName} created with Id {verifiedMerchant.MerchantId} for Estate {estateDetails.EstateName}");
foreach (KeyValuePair<Guid, MerchantResponse> verifiedMerchant in verifiedMerchants){
EstateDetails estateDetails = this.TestingContext.GetEstateDetails(verifiedMerchant.Value.EstateId);
estateDetails.AddMerchant(verifiedMerchant.Value);
this.TestingContext.Logger.LogInformation($"Merchant {verifiedMerchant.Value.MerchantName} created with Id {verifiedMerchant.Key} for Estate {estateDetails.EstateName}");
}
}

Expand Down
1 change: 0 additions & 1 deletion TransactionProcessor.Testing/TestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ static TestData() {
PhoneNumber = TestData.MerchantContactPhoneNumber
},
CreatedDateTime = TestData.DateMerchantCreated,
MerchantId = TestData.MerchantId,
Name = TestData.MerchantName,
SettlementSchedule = TestData.SettlementScheduleDTO
};
Expand Down
3 changes: 1 addition & 2 deletions TransactionProcessor.Tests/HandlerTests/WebHandlersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ public static IEnumerable<object[]> MerchantCases()

yield return Case(
"CreateMerchant",
SetupSuccess<MerchantCommands.CreateMerchantCommand>(command =>
SetupSuccess<MerchantCommands.CreateMerchantCommand, Guid>(MerchantId, command =>
{
command.EstateId.ShouldBe(EstateId);
command.RequestDto.Name.ShouldBe("Merchant");
Expand All @@ -425,7 +425,6 @@ public static IEnumerable<object[]> MerchantCases()
EstateId,
new CreateMerchantRequest
{
MerchantId = MerchantId,
Name = "Merchant"
},
CancellationToken.None));
Expand Down
4 changes: 2 additions & 2 deletions TransactionProcessor/Handlers/MerchantHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ public static async Task<IResult> CreateMerchant(IMediator mediator,
CreateMerchantRequest createMerchantRequest,
CancellationToken cancellationToken) {
MerchantCommands.CreateMerchantCommand command = new(estateId, createMerchantRequest);
Result result = await mediator.Send(command, cancellationToken);
Result<Guid> result = await mediator.Send(command, cancellationToken);

return ResponseFactory.FromResult(result);
return ResponseFactory.FromResult<Guid>(result, (merchantId) => result.Data);
}

public static async Task<IResult> AssignOperator(IMediator mediator,
Expand Down
Loading