Skip to content
Merged
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
138 changes: 108 additions & 30 deletions TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,129 @@
namespace TransactionProcessor.BusinessLogic.Common;

[ExcludeFromCodeCoverage]
public static class PolicyFactory{
public static IAsyncPolicy<Result> CreatePolicy(Int32 retryCount=5, TimeSpan? retryDelay = null, String policyTag="", Boolean withFallBack=false) {

TimeSpan retryDelayValue = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(2));
public static class PolicyFactory
{
private enum LogType
{
Retry,
Final
}

AsyncRetryPolicy<Result> retryPolicy = CreateRetryPolicy(retryCount, retryDelayValue, policyTag);
public static IAsyncPolicy<Result> CreatePolicy(
int retryCount = 5,

Check failure on line 26 in TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs#L26

Use the overloading mechanism instead of the optional parameters.
TimeSpan? retryDelay = null,
string policyTag = "",

Check failure on line 28 in TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs#L28

Use the overloading mechanism instead of the optional parameters.
bool withFallBack = false)

Check failure on line 29 in TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs#L29

Use the overloading mechanism instead of the optional parameters.
{
TimeSpan delay = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(5));
return CreateRetryPolicy(retryCount, delay, policyTag);
}

return retryPolicy;
public static IAsyncPolicy<Result<T>> CreatePolicy<T>(
int retryCount = 5,
TimeSpan? retryDelay = null,
string policyTag = "",
bool withFallBack = false)
{
TimeSpan delay = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(5));
return CreateRetryPolicy<T>(retryCount, delay, policyTag);
}

public static async Task<Result> ExecuteWithPolicyAsync(Func<Task<Result>> action, IAsyncPolicy<Result> policy, String policyTag = "")
public static async Task<Result> ExecuteWithPolicyAsync(
Func<Task<Result>> action,
IAsyncPolicy<Result> policy,
string policyTag = "")

Check failure on line 48 in TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs#L48

Use the overloading mechanism instead of the optional parameters.
{
var context = new Context();
context["RetryCount"] = 0;
Result result = await policy.ExecuteAsync(ctx => action(), context);

Result result = await policy.ExecuteAsync((ctx) => action(), context);

int retryCount = (int)context["RetryCount"];
String message = result switch
{
{ IsSuccess: true } => "Success",
{ IsSuccess: false, Message: not "" } => result.Message,
{ IsSuccess: false, Message: "", Errors: var errors } when errors.Any() => string.Join(", ", errors),
_ => "Unknown Error"
};
String retryMessage = retryCount > 0 ? $" after {retryCount} retries." : "";
// Log success if no retries were required
int retryCount = context.TryGetValue("RetryCount", out var retryObj) && retryObj is int r ? r : 0;
LogResult(policyTag, result, retryCount, LogType.Final);

Logger.LogWarning($"{policyTag} - {message} {retryMessage}");
return result;
}

public static async Task<Result<T>> ExecuteWithPolicyAsync<T>(
Func<Task<Result<T>>> action,
IAsyncPolicy<Result<T>> policy,
string policyTag = "")
{
var context = new Context();
Result<T> result = await policy.ExecuteAsync(ctx => action(), context);

int retryCount = context.TryGetValue("RetryCount", out var retryObj) && retryObj is int r ? r : 0;
LogResult(policyTag, result, retryCount, LogType.Final);

return result;
}

private static AsyncRetryPolicy<Result> CreateRetryPolicy(int retryCount, TimeSpan retryDelay, String policyTag)
private static AsyncRetryPolicy<Result> CreateRetryPolicy(
int retryCount,
TimeSpan retryDelay,
string policyTag)
{
return Policy<Result>
.HandleResult(result => !result.IsSuccess && String.Join("|",result.Errors).Contains("Append failed due to WrongExpectedVersion")) // Retry if the result is not successful
.OrResult(result => !result.IsSuccess && String.Join("|", result.Errors).Contains("DeadlineExceeded")) // Retry if the result is not successful
.WaitAndRetryAsync(retryCount,
_ => retryDelay, // Fixed delay
(result, timeSpan, retryCount, context) =>
.HandleResult(ShouldRetry)
.WaitAndRetryAsync(
retryCount,
_ => retryDelay,
(result, timeSpan, attempt, context) =>
{
context["RetryCount"] = attempt;
LogResult(policyTag, result.Result, attempt, LogType.Retry);
});
}

private static AsyncRetryPolicy<Result<T>> CreateRetryPolicy<T>(
int retryCount,
TimeSpan retryDelay,
string policyTag)
{
return Policy<Result<T>>
.HandleResult(ShouldRetry)
.WaitAndRetryAsync(
retryCount,
_ => retryDelay,
(result, timeSpan, attempt, context) =>

Check notice on line 100 in TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs#L100

'timeSpan' is not used. Use discard parameter instead.
{
context["RetryCount"] = retryCount;
Logger.LogWarning($"{policyTag} - Retry {retryCount} due to unsuccessful result {String.Join(".",result.Result.Errors)}. Waiting {timeSpan} before retrying...");
context["RetryCount"] = attempt;
LogResult(policyTag, result.Result, attempt, LogType.Retry);
});
}

private static bool ShouldRetry(ResultBase result)
{
return !result.IsSuccess && result.Errors.Any(e =>
e.Contains("WrongExpectedVersion", StringComparison.OrdinalIgnoreCase) ||
e.Contains("DeadlineExceeded", StringComparison.OrdinalIgnoreCase) ||
e.Contains("Cancelled"));
}

private static string FormatResultMessage(ResultBase result)
{
return result switch
{
{ IsSuccess: true } => "Success",
{ IsSuccess: false, Message: not "" } => result.Message,
{ IsSuccess: false, Errors: var errors } when errors?.Any() == true => string.Join(", ", errors),
_ => "Unknown Error"
};
}

private static void LogResult(string policyTag, ResultBase result, int retryCount, LogType type)
{
string message = FormatResultMessage(result);

switch (type)

Check failure on line 130 in TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs#L130

Add a 'default' clause to this 'switch' statement.

Check notice on line 130 in TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs#L130

Replace this 'switch' statement with 'if' statements to increase readability.
{
case LogType.Retry:
Logger.LogWarning($"{policyTag} - Retry {retryCount} due to error: {message}. Waiting before retrying...");
break;

case LogType.Final:
string retryMessage = retryCount > 0 ? $" after {retryCount} retries." : "";
Logger.LogWarning($"{policyTag} - {message}{retryMessage}");
break;
}
}
}
}
112 changes: 56 additions & 56 deletions TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@

namespace TransactionProcessor.BusinessLogic.Services
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Common;
using Models;
using Polly;
using Shared.DomainDrivenDesign.EventSourcing;
using Shared.EventStore.Aggregate;
using Shared.Exceptions;
using Shared.Logger;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public interface ISettlementDomainService
{
Expand Down Expand Up @@ -98,56 +99,53 @@
// TODO: Add in a Get Settlement

public async Task<Result<Guid>> ProcessSettlement(SettlementCommands.ProcessSettlementCommand command,
CancellationToken cancellationToken)
{
Guid settlementAggregateId = Helpers.CalculateSettlementAggregateId(command.SettlementDate, command.MerchantId,command.EstateId);
List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> feesToBeSettled = new();
CancellationToken cancellationToken) {
IAsyncPolicy<Result<Guid>> retryPolicy = PolicyFactory.CreatePolicy<Guid>(policyTag: "SettlementDomainService - ProcessSettlement");

return await PolicyFactory.ExecuteWithPolicyAsync<Guid>(async () => {
Guid settlementAggregateId = Helpers.CalculateSettlementAggregateId(command.SettlementDate, command.MerchantId, command.EstateId);
List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> feesToBeSettled = new();

Result settlementResult = await ApplySettlementUpdates(async (SettlementAggregate settlementAggregate) => {
if (settlementAggregate.IsCreated == false) {
Logger.LogInformation($"No pending settlement for {command.SettlementDate:yyyy-MM-dd}");
// Not pending settlement for this date
return Result.Success();
}

Result<MerchantAggregate> getMerchantResult = await this.AggregateService.Get<MerchantAggregate>(command.MerchantId, cancellationToken);
if (getMerchantResult.IsFailed)
return ResultHelpers.CreateFailure(getMerchantResult);

MerchantAggregate merchant = getMerchantResult.Data;
if (merchant.SettlementSchedule == SettlementSchedule.Immediate) {
// Mark the settlement as completed
settlementAggregate.StartProcessing(DateTime.Now);
settlementAggregate.ManuallyComplete();
Result result = await this.AggregateService.Save(settlementAggregate, cancellationToken);
return result;
}

feesToBeSettled = settlementAggregate.GetFeesToBeSettled();

if (feesToBeSettled.Any()) {
// Record the process call
settlementAggregate.StartProcessing(DateTime.Now);
return await this.AggregateService.Save(settlementAggregate, cancellationToken);
}

Result settlementResult = await ApplySettlementUpdates(async (SettlementAggregate settlementAggregate) => {
if (settlementAggregate.IsCreated == false)
{
Logger.LogInformation($"No pending settlement for {command.SettlementDate:yyyy-MM-dd}");
// Not pending settlement for this date
return Result.Success();
}

Result<MerchantAggregate> getMerchantResult = await this.AggregateService.Get<MerchantAggregate>(command.MerchantId, cancellationToken);
if (getMerchantResult.IsFailed)
return ResultHelpers.CreateFailure(getMerchantResult);

MerchantAggregate merchant = getMerchantResult.Data;
if (merchant.SettlementSchedule == SettlementSchedule.Immediate)
{
// Mark the settlement as completed
settlementAggregate.StartProcessing(DateTime.Now);
settlementAggregate.ManuallyComplete();
Result result = await this.AggregateService.Save(settlementAggregate, cancellationToken);
return result;
}
}, settlementAggregateId, cancellationToken);

feesToBeSettled = settlementAggregate.GetFeesToBeSettled();

if (feesToBeSettled.Any())
{
// Record the process call
settlementAggregate.StartProcessing(DateTime.Now);
return await this.AggregateService.Save(settlementAggregate, cancellationToken);
}
if (settlementResult.IsFailed)
return settlementResult;

return Result.Success();

}, settlementAggregateId, cancellationToken);

if (settlementResult.IsFailed)
return settlementResult;

List<Result> failedResults = new();
foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) {
Result transactionResult = await ApplyTransactionUpdates(
async (TransactionAggregate transactionAggregate) => {
List<Result> failedResults = new();
foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) {
Result transactionResult = await ApplyTransactionUpdates(async (TransactionAggregate transactionAggregate) => {

Check notice on line 146 in TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs#L146

Remove the type specification; it is redundant.
try {
transactionAggregate.AddSettledFee(feeToSettle.calculatedFee, command.SettlementDate,
settlementAggregateId);
transactionAggregate.AddSettledFee(feeToSettle.calculatedFee, command.SettlementDate, settlementAggregateId);
return Result.Success();
}
catch (Exception ex) {
Expand All @@ -156,15 +154,17 @@
}
}, feeToSettle.transactionId, cancellationToken);

if (transactionResult.IsFailed) {
failedResults.Add(transactionResult);
if (transactionResult.IsFailed) {
failedResults.Add(transactionResult);
}
}
}

if (failedResults.Any()) {
return Result.Failure($"Not all fees were processed successfully {failedResults.Count} have failed");
}
return Result.Success(settlementAggregateId);
if (failedResults.Any()) {
return Result.Failure($"Not all fees were processed successfully {failedResults.Count} have failed");
}

return Result.Success(settlementAggregateId);
}, retryPolicy, "SettlementDomainService - ProcessSettlement");
}

public async Task<Result> AddMerchantFeePendingSettlement(SettlementCommands.AddMerchantFeePendingSettlementCommand command,
Expand Down
Loading