diff --git a/TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs b/TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs index b9f36322..a986c87a 100644 --- a/TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs +++ b/TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs @@ -14,51 +14,129 @@ namespace TransactionProcessor.BusinessLogic.Common; [ExcludeFromCodeCoverage] -public static class PolicyFactory{ - public static IAsyncPolicy 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 retryPolicy = CreateRetryPolicy(retryCount, retryDelayValue, policyTag); + public static IAsyncPolicy CreatePolicy( + int retryCount = 5, + TimeSpan? retryDelay = null, + string policyTag = "", + bool withFallBack = false) + { + TimeSpan delay = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(5)); + return CreateRetryPolicy(retryCount, delay, policyTag); + } - return retryPolicy; + public static IAsyncPolicy> CreatePolicy( + int retryCount = 5, + TimeSpan? retryDelay = null, + string policyTag = "", + bool withFallBack = false) + { + TimeSpan delay = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(5)); + return CreateRetryPolicy(retryCount, delay, policyTag); } - public static async Task ExecuteWithPolicyAsync(Func> action, IAsyncPolicy policy, String policyTag = "") + public static async Task ExecuteWithPolicyAsync( + Func> action, + IAsyncPolicy policy, + string policyTag = "") { 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> ExecuteWithPolicyAsync( + Func>> action, + IAsyncPolicy> policy, + string policyTag = "") + { + var context = new Context(); + Result 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 CreateRetryPolicy(int retryCount, TimeSpan retryDelay, String policyTag) + private static AsyncRetryPolicy CreateRetryPolicy( + int retryCount, + TimeSpan retryDelay, + string policyTag) { return Policy - .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> CreateRetryPolicy( + int retryCount, + TimeSpan retryDelay, + string policyTag) + { + return Policy> + .HandleResult(ShouldRetry) + .WaitAndRetryAsync( + retryCount, + _ => retryDelay, + (result, timeSpan, attempt, context) => { - 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) + { + 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; + } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs b/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs index 390c03b7..3c260189 100644 --- a/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs @@ -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 { @@ -98,56 +99,53 @@ private async Task ApplyTransactionUpdates(Func> 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> retryPolicy = PolicyFactory.CreatePolicy(policyTag: "SettlementDomainService - ProcessSettlement"); + + return await PolicyFactory.ExecuteWithPolicyAsync(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 getMerchantResult = await this.AggregateService.Get(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 getMerchantResult = await this.AggregateService.Get(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 failedResults = new(); - foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) { - Result transactionResult = await ApplyTransactionUpdates( - async (TransactionAggregate transactionAggregate) => { + List failedResults = new(); + foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) { + Result transactionResult = await ApplyTransactionUpdates(async (TransactionAggregate transactionAggregate) => { try { - transactionAggregate.AddSettledFee(feeToSettle.calculatedFee, command.SettlementDate, - settlementAggregateId); + transactionAggregate.AddSettledFee(feeToSettle.calculatedFee, command.SettlementDate, settlementAggregateId); return Result.Success(); } catch (Exception ex) { @@ -156,15 +154,17 @@ public async Task> ProcessSettlement(SettlementCommands.ProcessSett } }, 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 AddMerchantFeePendingSettlement(SettlementCommands.AddMerchantFeePendingSettlementCommand command,