Skip to content

Commit d3fcaba

Browse files
Merge branch 'master' into copilot/fix-cyclomatic-complexity-issue-again
2 parents 1929bdf + eac5e3b commit d3fcaba

2 files changed

Lines changed: 141 additions & 74 deletions

File tree

TransactionProcessor.BusinessLogic.Tests/Services/MerchantDomainServiceTests.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,25 @@ public async Task MerchantDomainService_MakeMerchantWithdrawal_NotEnoughFundsToW
722722
result.IsFailed.ShouldBeTrue();
723723
}
724724

725+
[Fact]
726+
public async Task MerchantDomainService_MakeMerchantWithdrawal_InvalidBalanceProjection_ResultIsFailed() {
727+
this.AggregateService.Setup(e => e.Get<EstateAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
728+
.ReturnsAsync(TestData.Aggregates.CreatedEstateAggregate());
729+
730+
this.AggregateService.Setup(m => m.Get<MerchantAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
731+
.ReturnsAsync(Result.Success(TestData.Aggregates.CreatedMerchantAggregate()));
732+
733+
this.AggregateService
734+
.Setup(m => m.GetLatest<MerchantDepositListAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
735+
.ReturnsAsync(Result.Success(TestData.Aggregates.CreatedMerchantDepositListAggregate()));
736+
737+
this.EventStoreContext.Setup(e => e.GetPartitionStateFromProjection(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<CancellationToken>()))
738+
.ReturnsAsync(Result.Success<String>("null"));
739+
740+
var result = await this.DomainService.MakeMerchantWithdrawal(TestData.Commands.MakeMerchantWithdrawalCommand, CancellationToken.None);
741+
result.IsFailed.ShouldBeTrue();
742+
}
743+
725744
[Fact]
726745
public async Task MerchantDomainService_AddContractToMerchant_ContractAdded() {
727746
this.AggregateService.Setup(e => e.Get<EstateAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
@@ -1909,4 +1928,4 @@ public async Task MerchantDomainService_RemoveContractFromMerchant_ExceptionThro
19091928
var result = await this.DomainService.RemoveContractFromMerchant(TestData.Commands.RemoveMerchantContractCommand, CancellationToken.None);
19101929
result.IsFailed.ShouldBeTrue();
19111930
}
1912-
}
1931+
}

TransactionProcessor.BusinessLogic/Services/MerchantDomainService.cs

Lines changed: 121 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -124,34 +124,12 @@ public async Task<Result> AssignOperatorToMerchant(MerchantCommands.AssignOperat
124124
if (result.IsFailed)
125125
return ResultHelpers.CreateFailure(result);
126126

127-
// Is the operator valid for this estate
128-
Estate estate = estateAggregate.GetEstate();
129-
Models.Estate.Operator @operator = estate.Operators?.SingleOrDefault(o => o.OperatorId == command.RequestDto.OperatorId);
130-
if (@operator == null) {
131-
return Result.Invalid($"Operator Id {command.RequestDto.OperatorId} is not supported on Estate [{estate.Name}]");
132-
}
133-
134-
Result<OperatorAggregate> operatorResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest<OperatorAggregate>(command.RequestDto.OperatorId, ct), command.RequestDto.OperatorId, cancellationToken);
135-
if (operatorResult.IsFailed)
136-
return ResultHelpers.CreateFailure(operatorResult);
137-
OperatorAggregate @operatorAggregate = operatorResult.Data;
138-
// Operator has been validated, now check the rules of the operator against the passed in data
139-
if (@operatorAggregate.RequireCustomMerchantNumber) {
140-
// requested addition must have a merchant number supplied
141-
if (String.IsNullOrEmpty(command.RequestDto.MerchantNumber)) {
142-
return Result.Invalid($"Operator Id {command.RequestDto.OperatorId} requires that a merchant number is provided");
143-
}
144-
}
145-
146-
if (@operatorAggregate.RequireCustomTerminalNumber) {
147-
// requested addition must have a terminal number supplied
148-
if (String.IsNullOrEmpty(command.RequestDto.TerminalNumber)) {
149-
return Result.Invalid($"Operator Id {command.RequestDto.OperatorId} requires that a terminal number is provided");
150-
}
151-
}
127+
(Result validationResult, String operatorName) = await this.GetOperatorAssignmentDetails(command, estateAggregate, cancellationToken);
128+
if (validationResult.IsFailed)
129+
return validationResult;
152130

153131
// Assign the operator
154-
Result stateResult = merchantAggregate.AssignOperator(command.RequestDto.OperatorId, @operator.Name, command.RequestDto.MerchantNumber, command.RequestDto.TerminalNumber);
132+
Result stateResult = merchantAggregate.AssignOperator(command.RequestDto.OperatorId, operatorName, command.RequestDto.MerchantNumber, command.RequestDto.TerminalNumber);
155133
if (stateResult.IsFailed)
156134
return stateResult;
157135

@@ -166,6 +144,46 @@ public async Task<Result> AssignOperatorToMerchant(MerchantCommands.AssignOperat
166144
}
167145
}
168146

147+
private async Task<(Result ValidationResult, String OperatorName)> GetOperatorAssignmentDetails(MerchantCommands.AssignOperatorToMerchantCommand command,
148+
EstateAggregate estateAggregate,
149+
CancellationToken cancellationToken)
150+
{
151+
Estate estate = estateAggregate.GetEstate();
152+
Models.Estate.Operator @operator = estate.Operators?.SingleOrDefault(o => o.OperatorId == command.RequestDto.OperatorId);
153+
if (@operator == null) {
154+
return (Result.Invalid($"Operator Id {command.RequestDto.OperatorId} is not supported on Estate [{estate.Name}]"), String.Empty);
155+
}
156+
157+
Result<OperatorAggregate> operatorResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest<OperatorAggregate>(command.RequestDto.OperatorId, ct), command.RequestDto.OperatorId, cancellationToken);
158+
if (operatorResult.IsFailed)
159+
return (ResultHelpers.CreateFailure(operatorResult), String.Empty);
160+
161+
Result validationResult = this.ValidateOperatorAssignment(command.RequestDto.OperatorId,
162+
command.RequestDto.MerchantNumber,
163+
command.RequestDto.TerminalNumber,
164+
operatorResult.Data);
165+
166+
return validationResult.IsFailed
167+
? (validationResult, String.Empty)
168+
: (Result.Success(), @operator.Name);
169+
}
170+
171+
private Result ValidateOperatorAssignment(Guid operatorId,
172+
String merchantNumber,
173+
String terminalNumber,
174+
OperatorAggregate operatorAggregate)
175+
{
176+
if (operatorAggregate.RequireCustomMerchantNumber && String.IsNullOrEmpty(merchantNumber)) {
177+
return Result.Invalid($"Operator Id {operatorId} requires that a merchant number is provided");
178+
}
179+
180+
if (operatorAggregate.RequireCustomTerminalNumber && String.IsNullOrEmpty(terminalNumber)) {
181+
return Result.Invalid($"Operator Id {operatorId} requires that a terminal number is provided");
182+
}
183+
184+
return Result.Success();
185+
}
186+
169187
private SettlementSchedule ConvertSettlementSchedule(DataTransferObjects.Responses.Merchant.SettlementSchedule settlementSchedule) =>
170188
settlementSchedule switch
171189
{
@@ -175,6 +193,23 @@ private SettlementSchedule ConvertSettlementSchedule(DataTransferObjects.Respons
175193
_ => SettlementSchedule.NotSet
176194
};
177195

196+
private MerchantDepositSource ConvertDepositSource(DataTransferObjects.Requests.Merchant.MerchantDepositSource depositSource) =>
197+
depositSource switch
198+
{
199+
DataTransferObjects.Requests.Merchant.MerchantDepositSource.Manual => MerchantDepositSource.Manual,
200+
_ => MerchantDepositSource.Automatic,
201+
};
202+
203+
private Result EnsureMerchantDepositListCreated(MerchantDepositListAggregate merchantDepositListAggregate,
204+
MerchantAggregate merchantAggregate,
205+
DateTime depositDateTime) {
206+
if (merchantDepositListAggregate.IsCreated) {
207+
return Result.Success();
208+
}
209+
210+
return merchantDepositListAggregate.Create(merchantAggregate, depositDateTime);
211+
}
212+
178213
public async Task<Result> CreateMerchant(MerchantCommands.CreateMerchantCommand command, CancellationToken cancellationToken)
179214
{
180215
try {
@@ -310,19 +345,12 @@ public async Task<Result> MakeMerchantDeposit(MerchantCommands.MakeMerchantDepos
310345
return ResultHelpers.CreateFailure(getDepositListResult);
311346

312347
MerchantDepositListAggregate merchantDepositListAggregate = getDepositListResult.Data;
313-
if (merchantDepositListAggregate.IsCreated == false)
314-
{
315-
Result createResult = merchantDepositListAggregate.Create(merchantAggregate, command.RequestDto.DepositDateTime);
316-
if (createResult.IsFailed)
317-
return ResultHelpers.CreateFailure(createResult);
318-
}
348+
Result createResult = this.EnsureMerchantDepositListCreated(merchantDepositListAggregate, merchantAggregate, command.RequestDto.DepositDateTime);
349+
if (createResult.IsFailed)
350+
return ResultHelpers.CreateFailure(createResult);
319351

320352
PositiveMoney amount = PositiveMoney.Create(Money.Create(command.RequestDto.Amount));
321-
MerchantDepositSource depositSource = command.DepositSource switch
322-
{
323-
DataTransferObjects.Requests.Merchant.MerchantDepositSource.Manual => MerchantDepositSource.Manual,
324-
_ => MerchantDepositSource.Automatic,
325-
};
353+
MerchantDepositSource depositSource = this.ConvertDepositSource(command.DepositSource);
326354
Result stateResult = merchantDepositListAggregate.MakeDeposit(depositSource, command.RequestDto.Reference, command.RequestDto.DepositDateTime, amount);
327355
if (stateResult.IsFailed)
328356
return ResultHelpers.CreateFailure(stateResult);
@@ -345,48 +373,17 @@ public async Task<Result> MakeMerchantWithdrawal(MerchantCommands.MakeMerchantWi
345373

346374
try
347375
{
348-
Result<EstateAggregate> estateResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.Get<EstateAggregate>(command.EstateId, ct), command.EstateId, cancellationToken);
349-
if (estateResult.IsFailed)
350-
return ResultHelpers.CreateFailure(estateResult);
351-
352-
Result<MerchantAggregate> merchantResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.Get<MerchantAggregate>(command.MerchantId, ct), command.MerchantId, cancellationToken);
353-
if (merchantResult.IsFailed)
354-
return ResultHelpers.CreateFailure(merchantResult);
355-
356-
EstateAggregate estateAggregate = estateResult.Data;
357-
MerchantAggregate merchantAggregate = merchantResult.Data;
358-
359-
Result validateResult =
360-
this.ValidateEstateAndMerchant(estateAggregate, merchantAggregate);
361-
if (validateResult.IsFailed)
362-
return ResultHelpers.CreateFailure(validateResult);
363-
364-
Result<MerchantDepositListAggregate> getDepositListResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest<MerchantDepositListAggregate>(command.MerchantId, ct), command.MerchantId, cancellationToken);
376+
Result<MerchantDepositListAggregate> getDepositListResult = await this.GetMerchantDepositListForWithdrawal(command, cancellationToken);
365377
if (getDepositListResult.IsFailed)
366378
return ResultHelpers.CreateFailure(getDepositListResult);
367-
368-
MerchantDepositListAggregate merchantDepositListAggregate = getDepositListResult.Data;
369-
if (merchantDepositListAggregate.IsCreated == false)
370-
{
371-
return Result.Invalid($"Merchant [{command.MerchantId}] has not made any deposits yet");
372-
}
373379

374-
// Now we need to check the merchants balance to ensure they have funds to withdraw
375-
Result<String> getBalanceResult = await this.EventStoreContext.GetPartitionStateFromProjection("MerchantBalanceProjection", $"MerchantBalance-{command.MerchantId:N}", cancellationToken);
376-
if (getBalanceResult.IsFailed)
377-
{
378-
return Result.Invalid($"Failed to get Merchant Balance.");
379-
}
380-
381-
MerchantBalanceProjectionState1 projectionState = JsonConvert.DeserializeObject<MerchantBalanceProjectionState1>(getBalanceResult.Data);
382-
383-
if (command.RequestDto.Amount > projectionState.merchant.balance)
384-
{
385-
return Result.Invalid($"Not enough credit available for withdrawal of [{command.RequestDto.Amount}]. Balance is {projectionState.merchant.balance}");
386-
}
380+
Result validateBalanceResult = await this.ValidateWithdrawalBalance(command, cancellationToken);
381+
if (validateBalanceResult.IsFailed)
382+
return validateBalanceResult;
387383

388384
// If we are here we have enough credit to withdraw
389385
PositiveMoney amount = PositiveMoney.Create(Money.Create(command.RequestDto.Amount));
386+
MerchantDepositListAggregate merchantDepositListAggregate = getDepositListResult.Data;
390387

391388
Result stateResult = merchantDepositListAggregate.MakeWithdrawal(command.RequestDto.WithdrawalDateTime, amount);
392389
if (stateResult.IsFailed)
@@ -716,6 +713,57 @@ private Result ValidateEstateAndMerchant(EstateAggregate estateAggregate,
716713
return Result.Success();
717714
}
718715

716+
private async Task<Result<MerchantDepositListAggregate>> GetMerchantDepositListForWithdrawal(MerchantCommands.MakeMerchantWithdrawalCommand command,
717+
CancellationToken cancellationToken)
718+
{
719+
Result<EstateAggregate> estateResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.Get<EstateAggregate>(command.EstateId, ct), command.EstateId, cancellationToken);
720+
if (estateResult.IsFailed)
721+
return ResultHelpers.CreateFailure(estateResult);
722+
723+
Result<MerchantAggregate> merchantResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.Get<MerchantAggregate>(command.MerchantId, ct), command.MerchantId, cancellationToken);
724+
if (merchantResult.IsFailed)
725+
return ResultHelpers.CreateFailure(merchantResult);
726+
727+
Result validateResult = this.ValidateEstateAndMerchant(estateResult.Data, merchantResult.Data);
728+
if (validateResult.IsFailed)
729+
return ResultHelpers.CreateFailure(validateResult);
730+
731+
Result<MerchantDepositListAggregate> getDepositListResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest<MerchantDepositListAggregate>(command.MerchantId, ct), command.MerchantId, cancellationToken);
732+
if (getDepositListResult.IsFailed)
733+
return ResultHelpers.CreateFailure(getDepositListResult);
734+
735+
MerchantDepositListAggregate merchantDepositListAggregate = getDepositListResult.Data;
736+
if (merchantDepositListAggregate.IsCreated == false)
737+
{
738+
return Result.Invalid($"Merchant [{command.MerchantId}] has not made any deposits yet");
739+
}
740+
741+
return Result.Success(merchantDepositListAggregate);
742+
}
743+
744+
private async Task<Result> ValidateWithdrawalBalance(MerchantCommands.MakeMerchantWithdrawalCommand command,
745+
CancellationToken cancellationToken)
746+
{
747+
Result<String> getBalanceResult = await this.EventStoreContext.GetPartitionStateFromProjection("MerchantBalanceProjection", $"MerchantBalance-{command.MerchantId:N}", cancellationToken);
748+
if (getBalanceResult.IsFailed)
749+
{
750+
return Result.Invalid($"Failed to get Merchant Balance.");
751+
}
752+
753+
MerchantBalanceProjectionState1 projectionState = JsonConvert.DeserializeObject<MerchantBalanceProjectionState1>(getBalanceResult.Data);
754+
if (projectionState?.merchant == null)
755+
{
756+
return Result.Invalid("Merchant Balance data is missing or invalid.");
757+
}
758+
759+
if (command.RequestDto.Amount > projectionState.merchant.balance)
760+
{
761+
return Result.Invalid($"Not enough credit available for withdrawal of [{command.RequestDto.Amount}]. Balance is {projectionState.merchant.balance}");
762+
}
763+
764+
return Result.Success();
765+
}
766+
719767
public async Task<Result> SwapMerchantDevice(MerchantCommands.SwapMerchantDeviceCommand command,
720768
CancellationToken cancellationToken)
721769
{

0 commit comments

Comments
 (0)