using BusinessLogic.Classes; using BusinessLogic.Entities; using BusinessLogic.Enums; using BusinessLogic.Extensions; using BusinessLogic.Interfaces.Persistence; using BusinessLogic.Interfaces.Security; using BusinessLogic.Interfaces.Services; using log4net; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using IPaymentIntegrationClient = BusinessLogic.Clients.PaymentIntegrationClient.IClient; namespace BusinessLogic.Services { public class RefundService : BaseService, IRefundService { private readonly ITransactionService _transactionService; private readonly IPaymentIntegrationClient _paymentIntegrationClient; public RefundService(ILog logger , IUnitOfWork unitOfWork , ISecurityContext securityContext , ITransactionService transactionService , IPaymentIntegrationClient paymentIntegrationClient) : base(logger, unitOfWork, securityContext) { _transactionService = transactionService ?? throw new ArgumentNullException("transactionService"); _paymentIntegrationClient = paymentIntegrationClient ?? throw new ArgumentNullException("paymentIntegrationClient"); } // HIGH: We should be checking for refund permission here public RefundStatus RefundTransaction(RefundRequest refundRequest, string reason) { return RefundTransactions(new List { refundRequest }, reason); } public RefundStatus RefundTransactions(List refundRequests, string reason) { if (!SecurityContext.IsInRole(Security.Role.TransactionRefund)) return null; var internalReference = string.Empty; try { decimal totalRefundAmount = 0; var pspReference = string.Empty; var transactionDate = new DateTime(); var mopCode = string.Empty; var validRefundTransactions = new List(); if (refundRequests.Sum(x => x.RefundAmount) <= 0) { return RefundStatus.ErrorStatus("Refund amount must be greater than 0"); } // TODO: This will be string.Empty - so will always pass if (HasRefundsPending(internalReference)) { return RefundStatus.ErrorStatus("Payment has pending refunds, please wait for them to complete and try again"); } if (string.IsNullOrEmpty(reason)) { return RefundStatus.ErrorStatus("You must provide a reason for the refund"); } foreach (var refundRequest in refundRequests) { if (refundRequest.RefundAmount <= 0) continue; var transaction = _transactionService.GetTransaction(refundRequest.TransactionReference); var transactionHeader = _transactionService.GetTransactionByPspReference(transaction.PspReference); internalReference = transaction.InternalReference; transactionDate = transaction.TransactionDate ?? new DateTime(); mopCode = transaction.MopCode; var remainingAmount = transactionHeader.AmountAvailableToTransferOrRefundForTransactionLine(refundRequest.TransactionReference); if (refundRequest.RefundAmount > remainingAmount) { return RefundStatus.ErrorStatus("Refund amount is greater than remaining transaction amount"); } if (pspReference != string.Empty && pspReference != transaction.PspReference) { return RefundStatus.ErrorStatus("Transaction contains refunds for multiple PSP References"); } if (!transaction.IsRefundable()) { return RefundStatus.ErrorStatus("You can't refund a payment that hasn't been processed via the payment provider"); } if (refundRequest.RefundAmount < transaction.Amount && !SecurityContext.IsInRole(Security.Role.TransactionPartialRefund)) { return RefundStatus.ErrorStatus("You don't have permission to perform a partial refund"); } if (refundRequest.RefundAmount > 0) { var refundTransaction = new PendingTransaction() { RefundReference = transaction.InternalReference, Amount = (refundRequest.RefundAmount * -1), AccountReference = transaction.AccountReference, EntryDate = DateTime.Now, FundCode = transaction.FundCode, MopCode = transaction.MopCode, VatCode = transaction.VatCode, UserCode = SecurityContext.UserId, Narrative = reason, StatusId = (int)Enums.TransactionStatus.Pending }; totalRefundAmount += refundRequest.RefundAmount; pspReference = transaction.PspReference; validRefundTransactions.Add(refundTransaction); } } if (!validRefundTransactions.Any()) { return RefundStatus.ErrorStatus("No valid refunds found to create"); } var savePendingTransactionResult = _transactionService.SavePendingTransactions(validRefundTransactions, "backoffice"); if (!savePendingTransactionResult.Success) { return RefundStatus.ErrorStatus("Refund records could not be saved to database"); } _paymentIntegrationClient.ConfigureSettings(GetPaymentIntegration(mopCode)); var refundResponse = Task.Run(async () => await _paymentIntegrationClient.RequestRefund(new Clients.PaymentIntegrationClient.RefundRequest() { Amount = totalRefundAmount, Reference = pspReference, ImsReference = savePendingTransactionResult.PaymentId, TransactionDate = transactionDate })).Result; if (refundResponse.Success) { return RefundStatus.AcceptedStatus(totalRefundAmount); } _transactionService.MarkRefundsAsFailed(internalReference, reason); return RefundStatus.FailStatus(refundResponse.Message); } catch (Exception e) { // TODO: What if an exception occurs before the the refund(s) (pending transaction(s)) have been created? _transactionService.MarkRefundsAsFailed(internalReference, reason); return RefundStatus.ErrorStatus(e.Message); } } private bool HasRefundsPending(string internalReference) { var refundsPending = _transactionService.GetPendingRefunds(internalReference); return refundsPending.Any(); } private PaymentIntegration GetPaymentIntegration(string mopCode) { var mop = UnitOfWork.Mops.GetMop(mopCode); var paymentIntegrationId = Convert.ToInt32(mop.Metadata.First(x => x.MetadataKey.Name == MopMetadataKeys.PaymentIntegrationId).Value); return UnitOfWork.PaymentIntegrations.SingleOrDefault(x => x.Id == paymentIntegrationId); } } }