using System.Xml; using System.Xml.Serialization; using Microsoft.AspNetCore.Http; using Nop.Core; using Nop.Core.Domain.Orders; using Nop.Core.Domain.Payments; using Nop.Core.Http.Extensions; using Nop.Services.Catalog; using Nop.Services.Customers; namespace Nop.Services.Payments; /// /// Payment service /// public partial class PaymentService : IPaymentService { #region Fields protected readonly ICustomerService _customerService; protected readonly IHttpContextAccessor _httpContextAccessor; protected readonly IPaymentPluginManager _paymentPluginManager; protected readonly IPriceCalculationService _priceCalculationService; protected readonly PaymentSettings _paymentSettings; protected readonly ShoppingCartSettings _shoppingCartSettings; #endregion #region Ctor public PaymentService(ICustomerService customerService, IHttpContextAccessor httpContextAccessor, IPaymentPluginManager paymentPluginManager, IPriceCalculationService priceCalculationService, PaymentSettings paymentSettings, ShoppingCartSettings shoppingCartSettings) { _customerService = customerService; _httpContextAccessor = httpContextAccessor; _paymentPluginManager = paymentPluginManager; _priceCalculationService = priceCalculationService; _paymentSettings = paymentSettings; _shoppingCartSettings = shoppingCartSettings; } #endregion #region Methods /// /// Process a payment /// /// Payment info required for an order processing /// /// A task that represents the asynchronous operation /// The task result contains the process payment result /// public virtual async Task ProcessPaymentAsync(ProcessPaymentRequest processPaymentRequest) { if (processPaymentRequest.OrderTotal == decimal.Zero) { var result = new ProcessPaymentResult { NewPaymentStatus = PaymentStatus.Paid }; return result; } //We should strip out any white space or dash in the CC number entered. if (!string.IsNullOrWhiteSpace(processPaymentRequest.CreditCardNumber)) { processPaymentRequest.CreditCardNumber = processPaymentRequest.CreditCardNumber.Replace(" ", string.Empty); processPaymentRequest.CreditCardNumber = processPaymentRequest.CreditCardNumber.Replace("-", string.Empty); } var customer = await _customerService.GetCustomerByIdAsync(processPaymentRequest.CustomerId); var paymentMethod = await _paymentPluginManager .LoadPluginBySystemNameAsync(processPaymentRequest.PaymentMethodSystemName, customer, processPaymentRequest.StoreId) ?? throw new NopException("Payment method couldn't be loaded"); return await paymentMethod.ProcessPaymentAsync(processPaymentRequest); } /// /// Post process payment (used by payment gateways that require redirecting to a third-party URL) /// /// Payment info required for an order processing /// A task that represents the asynchronous operation public virtual async Task PostProcessPaymentAsync(PostProcessPaymentRequest postProcessPaymentRequest) { //already paid or order.OrderTotal == decimal.Zero if (postProcessPaymentRequest.Order.PaymentStatus == PaymentStatus.Paid) return; var customer = await _customerService.GetCustomerByIdAsync(postProcessPaymentRequest.Order.CustomerId); var paymentMethod = await _paymentPluginManager .LoadPluginBySystemNameAsync(postProcessPaymentRequest.Order.PaymentMethodSystemName, customer, postProcessPaymentRequest.Order.StoreId) ?? throw new NopException("Payment method couldn't be loaded"); await paymentMethod.PostProcessPaymentAsync(postProcessPaymentRequest); } /// /// Gets a value indicating whether customers can complete a payment after order is placed but not completed (for redirection payment methods) /// /// Order /// /// A task that represents the asynchronous operation /// The task result contains the result /// public virtual async Task CanRePostProcessPaymentAsync(Order order) { ArgumentNullException.ThrowIfNull(order); if (!_paymentSettings.AllowRePostingPayments) return false; var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId); var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(order.PaymentMethodSystemName, customer, order.StoreId); if (paymentMethod == null) return false; //Payment method couldn't be loaded (for example, was uninstalled) if (paymentMethod.PaymentMethodType != PaymentMethodType.Redirection) return false; //this option is available only for redirection payment methods if (order.Deleted) return false; //do not allow for deleted orders if (order.OrderStatus == OrderStatus.Cancelled) return false; //do not allow for cancelled orders if (order.PaymentStatus != PaymentStatus.Pending) return false; //payment status should be Pending return await paymentMethod.CanRePostProcessPaymentAsync(order); } /// /// Gets an additional handling fee of a payment method /// /// Shopping cart /// Payment method system name /// /// A task that represents the asynchronous operation /// The task result contains the additional handling fee /// public virtual async Task GetAdditionalHandlingFeeAsync(IList cart, string paymentMethodSystemName) { if (string.IsNullOrEmpty(paymentMethodSystemName)) return decimal.Zero; var customer = await _customerService.GetCustomerByIdAsync(cart.FirstOrDefault()?.CustomerId ?? 0); var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(paymentMethodSystemName, customer, cart.FirstOrDefault()?.StoreId ?? 0); if (paymentMethod == null) return decimal.Zero; var result = await paymentMethod.GetAdditionalHandlingFeeAsync(cart); if (result < decimal.Zero) result = decimal.Zero; if (!_shoppingCartSettings.RoundPricesDuringCalculation) return result; result = await _priceCalculationService.RoundPriceAsync(result); return result; } /// /// Gets a value indicating whether capture is supported by payment method /// /// Payment method system name /// /// A task that represents the asynchronous operation /// The task result contains a value indicating whether capture is supported /// public virtual async Task SupportCaptureAsync(string paymentMethodSystemName) { var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(paymentMethodSystemName); if (paymentMethod == null) return false; return paymentMethod.SupportCapture; } /// /// Captures payment /// /// Capture payment request /// /// A task that represents the asynchronous operation /// The task result contains the capture payment result /// public virtual async Task CaptureAsync(CapturePaymentRequest capturePaymentRequest) { var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(capturePaymentRequest.Order.PaymentMethodSystemName) ?? throw new NopException("Payment method couldn't be loaded"); return await paymentMethod.CaptureAsync(capturePaymentRequest); } /// /// Gets a value indicating whether partial refund is supported by payment method /// /// Payment method system name /// /// A task that represents the asynchronous operation /// The task result contains a value indicating whether partial refund is supported /// public virtual async Task SupportPartiallyRefundAsync(string paymentMethodSystemName) { var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(paymentMethodSystemName); if (paymentMethod == null) return false; return paymentMethod.SupportPartiallyRefund; } /// /// Gets a value indicating whether refund is supported by payment method /// /// Payment method system name /// /// A task that represents the asynchronous operation /// The task result contains a value indicating whether refund is supported /// public virtual async Task SupportRefundAsync(string paymentMethodSystemName) { var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(paymentMethodSystemName); if (paymentMethod == null) return false; return paymentMethod.SupportRefund; } /// /// Refunds a payment /// /// Request /// /// A task that represents the asynchronous operation /// The task result contains the result /// public virtual async Task RefundAsync(RefundPaymentRequest refundPaymentRequest) { var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(refundPaymentRequest.Order.PaymentMethodSystemName) ?? throw new NopException("Payment method couldn't be loaded"); return await paymentMethod.RefundAsync(refundPaymentRequest); } /// /// Gets a value indicating whether void is supported by payment method /// /// Payment method system name /// /// A task that represents the asynchronous operation /// The task result contains a value indicating whether void is supported /// public virtual async Task SupportVoidAsync(string paymentMethodSystemName) { var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(paymentMethodSystemName); if (paymentMethod == null) return false; return paymentMethod.SupportVoid; } /// /// Voids a payment /// /// Request /// /// A task that represents the asynchronous operation /// The task result contains the result /// public virtual async Task VoidAsync(VoidPaymentRequest voidPaymentRequest) { var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(voidPaymentRequest.Order.PaymentMethodSystemName) ?? throw new NopException("Payment method couldn't be loaded"); return await paymentMethod.VoidAsync(voidPaymentRequest); } /// /// Gets a recurring payment type of payment method /// /// Payment method system name /// /// A task that represents the asynchronous operation /// The task result contains a recurring payment type of payment method /// public virtual async Task GetRecurringPaymentTypeAsync(string paymentMethodSystemName) { var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(paymentMethodSystemName); if (paymentMethod == null) return RecurringPaymentType.NotSupported; return paymentMethod.RecurringPaymentType; } /// /// Process recurring payment /// /// Payment info required for an order processing /// /// A task that represents the asynchronous operation /// The task result contains the process payment result /// public virtual async Task ProcessRecurringPaymentAsync(ProcessPaymentRequest processPaymentRequest) { if (processPaymentRequest.OrderTotal == decimal.Zero) { var result = new ProcessPaymentResult { NewPaymentStatus = PaymentStatus.Paid }; return result; } var customer = await _customerService.GetCustomerByIdAsync(processPaymentRequest.CustomerId); var paymentMethod = await _paymentPluginManager .LoadPluginBySystemNameAsync(processPaymentRequest.PaymentMethodSystemName, customer, processPaymentRequest.StoreId) ?? throw new NopException("Payment method couldn't be loaded"); return await paymentMethod.ProcessRecurringPaymentAsync(processPaymentRequest); } /// /// Cancels a recurring payment /// /// Request /// /// A task that represents the asynchronous operation /// The task result contains the result /// public virtual async Task CancelRecurringPaymentAsync(CancelRecurringPaymentRequest cancelPaymentRequest) { if (cancelPaymentRequest.Order.OrderTotal == decimal.Zero) return new CancelRecurringPaymentResult(); var paymentMethod = await _paymentPluginManager.LoadPluginBySystemNameAsync(cancelPaymentRequest.Order.PaymentMethodSystemName) ?? throw new NopException("Payment method couldn't be loaded"); return await paymentMethod.CancelRecurringPaymentAsync(cancelPaymentRequest); } /// /// Gets masked credit card number /// /// Credit card number /// Masked credit card number public virtual string GetMaskedCreditCardNumber(string creditCardNumber) { if (string.IsNullOrEmpty(creditCardNumber)) return string.Empty; if (creditCardNumber.Length <= 4) return creditCardNumber; var last4 = creditCardNumber[(creditCardNumber.Length - 4)..creditCardNumber.Length]; var maskedChars = string.Empty; for (var i = 0; i < creditCardNumber.Length - 4; i++) { maskedChars += "*"; } return maskedChars + last4; } /// /// Serialize CustomValues of ProcessPaymentRequest /// /// Request /// Serialized CustomValues public virtual string SerializeCustomValues(ProcessPaymentRequest request) { ArgumentNullException.ThrowIfNull(request); if (!request.CustomValues.Any()) return null; //XmlSerializer won't serialize objects that implement IDictionary by default. //http://msdn.microsoft.com/en-us/magazine/cc164135.aspx //also see http://ropox.ru/tag/ixmlserializable/ (Russian language) var ds = new DictionarySerializer(request.CustomValues); var xs = new XmlSerializer(typeof(DictionarySerializer)); using var textWriter = new StringWriter(); using (var xmlWriter = XmlWriter.Create(textWriter)) { xs.Serialize(xmlWriter, ds); } var result = textWriter.ToString(); return result; } /// /// Deserialize CustomValues of Order /// /// Order /// Serialized CustomValues CustomValues public virtual Dictionary DeserializeCustomValues(Order order) { ArgumentNullException.ThrowIfNull(order); if (string.IsNullOrWhiteSpace(order.CustomValuesXml)) return new Dictionary(); var serializer = new XmlSerializer(typeof(DictionarySerializer)); using var textReader = new StringReader(order.CustomValuesXml); using var xmlReader = XmlReader.Create(textReader); if (serializer.Deserialize(xmlReader) is DictionarySerializer ds) return ds.Dictionary; return []; } /// /// Generate an order GUID /// /// Process payment request public virtual async Task GenerateOrderGuidAsync(ProcessPaymentRequest processPaymentRequest) { if (processPaymentRequest == null) return; //we should use the same GUID for multiple payment attempts //this way a payment gateway can prevent security issues such as credit card brute-force attacks //in order to avoid any possible limitations by payment gateway we reset GUID periodically var previousPaymentRequest = await _httpContextAccessor.HttpContext.Session.GetAsync("OrderPaymentInfo"); if (_paymentSettings.RegenerateOrderGuidInterval > 0 && previousPaymentRequest != null && previousPaymentRequest.OrderGuidGeneratedOnUtc.HasValue) { var interval = DateTime.UtcNow - previousPaymentRequest.OrderGuidGeneratedOnUtc.Value; if (interval.TotalSeconds < _paymentSettings.RegenerateOrderGuidInterval) { processPaymentRequest.OrderGuid = previousPaymentRequest.OrderGuid; processPaymentRequest.OrderGuidGeneratedOnUtc = previousPaymentRequest.OrderGuidGeneratedOnUtc; } } if (processPaymentRequest.OrderGuid == Guid.Empty) { processPaymentRequest.OrderGuid = Guid.NewGuid(); processPaymentRequest.OrderGuidGeneratedOnUtc = DateTime.UtcNow; } } #endregion }