using Microsoft.AspNetCore.Http; using Nop.Core; using Nop.Core.Domain.Orders; using Nop.Core.Domain.Payments; using Nop.Plugin.Payments.TapPay.Components; using Nop.Plugin.Payments.TapPay.Domain; using Nop.Plugin.Payments.TapPay.Models; using Nop.Plugin.Payments.TapPay.Services; using Nop.Services.Configuration; using Nop.Services.Customers; using Nop.Services.Localization; using Nop.Services.Logging; using Nop.Services.Orders; using Nop.Services.Payments; using Nop.Services.Plugins; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; namespace Nop.Plugin.Payments.TapPay; /// /// TapPay payment processor /// public class TapPayPaymentProcessor : BasePlugin, IPaymentMethod { #region Fields protected readonly ILocalizationService _localizationService; protected readonly IOrderTotalCalculationService _orderTotalCalculationService; protected readonly ISettingService _settingService; protected readonly ICustomerService _customerService; protected readonly IOrderService _orderService; protected readonly IStoreContext _storeContext; protected readonly ITapPayResultService _tapPayResultService; protected readonly IWebHelper _webHelper; protected readonly IHttpContextAccessor _httpContextAccessor; protected readonly ILogger _logger; protected readonly TapPayPaymentSettings _tapPayPaymentSettings; #endregion #region Ctor public TapPayPaymentProcessor(ILocalizationService localizationService, IOrderTotalCalculationService orderTotalCalculationService, ISettingService settingService, ICustomerService customerService, IOrderService orderService, IStoreContext storeContext, ITapPayResultService tapPayResultService, IWebHelper webHelper, IHttpContextAccessor httpContextAccessor, ILogger logger, TapPayPaymentSettings tapPayPaymentSettings) { _localizationService = localizationService; _orderTotalCalculationService = orderTotalCalculationService; _settingService = settingService; _customerService = customerService; _orderService = orderService; _storeContext = storeContext; _tapPayResultService = tapPayResultService; _webHelper = webHelper; _httpContextAccessor = httpContextAccessor; _logger = logger; _tapPayPaymentSettings = tapPayPaymentSettings; } #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 async Task ProcessPaymentAsync(ProcessPaymentRequest processPaymentRequest) { var result = new ProcessPaymentResult(); //load settings for a chosen store scope var storeScope = await _storeContext.GetActiveStoreScopeConfigurationAsync(); // var _tapPayPaymentSettings = await _settingService.LoadSettingAsync(storeScope); var customer = await _customerService.GetCustomerByIdAsync(processPaymentRequest.CustomerId); var customerBillingAddress = await _customerService.GetCustomerBillingAddressAsync(customer); string GetCustomValue(string key) { if (processPaymentRequest.CustomValues.TryGetValue(key, out var value) && value != null) return value.ToString().Trim(); return string.Empty; } var prime = GetCustomValue("Prime"); if (string.IsNullOrEmpty(prime)) { result.AddError("TapPay prime is missing."); return result; } var cardholderEmail = GetCustomValue("CardholderEmail"); var cardholderPhone = GetCustomValue("CardholderPhoneNumber"); var cardholderPhoneCode = GetCustomValue("CardholderPhoneNumberCountryCode"); var cardholderNameEn = GetCustomValue("CardholderNameEn"); var requestBody = JsonSerializer.Serialize( new { partner_key = _tapPayPaymentSettings.PartnerKey, prime = prime, amount = (int)processPaymentRequest.OrderTotal, merchant_id = _tapPayPaymentSettings.Merchant.ToString(), order_number = processPaymentRequest.OrderGuid.ToString(), //bank_transaction_id = processPaymentRequest.OrderGuid.ToString("N"), three_domain_secure = true, result_url = new { frontend_redirect_url = _tapPayPaymentSettings.RedirectUrl, backend_notify_url = _tapPayPaymentSettings.NotifyUrl, go_back_url = _tapPayPaymentSettings.GoBackUrl }, details = "AI Hardware / APP", cardholder = new { phone_number = cardholderPhone, phone_number_country_code = string.IsNullOrWhiteSpace(cardholderPhoneCode) ? "886" : cardholderPhoneCode, name_en = cardholderNameEn, name = (customerBillingAddress?.LastName ?? string.Empty) + (customerBillingAddress?.FirstName ?? string.Empty), email = cardholderEmail, zip_code = customerBillingAddress?.ZipPostalCode, address = (customerBillingAddress?.City ?? string.Empty) + (customerBillingAddress?.Address1 ?? string.Empty) + (customerBillingAddress?.Address2 ?? string.Empty), member_id = processPaymentRequest.CustomerId.ToString() } } ); using(var client = new HttpClient()) { var request = new HttpRequestMessage { RequestUri = new Uri(_tapPayPaymentSettings.ServiceEndpoint), Method = HttpMethod.Post, Headers = { { "X-Version", "1" }, { "x-api-key", _tapPayPaymentSettings.PartnerKey }, }, Content = new StringContent(requestBody, Encoding.UTF8, "application/json") }; var response = await client.SendAsync(request); try { response.EnsureSuccessStatusCode(); } catch { var failedResult = new TapPayResult() { order_number = processPaymentRequest.OrderGuid, RequestHeader = request.ToString(), RequestBody = requestBody, ResponseHeader = response.ToString(), CreatedOnUtc = DateTime.UtcNow, }; await _tapPayResultService.InsertTapPayResultAsync(failedResult); throw; } var responseBody = await response.Content.ReadAsStringAsync(); var responseBodyObject = JsonSerializer.Deserialize(responseBody); try { var paymentResult = new TapPayResult() { order_number = new Guid(responseBodyObject.order_number), status = responseBodyObject.status, msg = responseBodyObject.msg, rec_trade_id = responseBodyObject.rec_trade_id, bank_transaction_id = responseBodyObject.bank_transaction_id, bank_order_number = responseBodyObject.bank_order_number, auth_code = responseBodyObject.auth_code, card_secret = JsonSerializer.Serialize(responseBodyObject.card_secret), amount = responseBodyObject.amount, currency = responseBodyObject.currency, card_info = JsonSerializer.Serialize(responseBodyObject.card_info), acquirer = responseBodyObject.acquirer, transaction_time_millis = responseBodyObject.transaction_time_millis, bank_transaction_time = JsonSerializer.Serialize(responseBodyObject.bank_transaction_time), bank_result_code = responseBodyObject.bank_result_code, bank_result_msg = responseBodyObject.bank_result_msg, payment_url = responseBodyObject.payment_url, instalment_info = JsonSerializer.Serialize(responseBodyObject.instalment_info), redeem_info = JsonSerializer.Serialize(responseBodyObject.redeem_info), card_identifier = responseBodyObject.card_identifier, merchant_reference_info = JsonSerializer.Serialize(responseBodyObject.merchant_reference_info), event_code = responseBodyObject.event_code, is_rba_verified = responseBodyObject.is_rba_verified, transaction_method_details = JsonSerializer.Serialize(responseBodyObject.transaction_method_details), RequestHeader = request.ToString(), RequestBody = requestBody, ResponseHeader = response.ToString(), ResponseBody = responseBody, CreatedOnUtc = DateTime.UtcNow, }; await _tapPayResultService.InsertTapPayResultAsync(paymentResult); } catch { await _logger.InformationAsync(request.ToString() + "\nBody:\n" + requestBody); await _logger.InformationAsync(response.ToString() + "\nBody:\n" + responseBody); } if(responseBodyObject.status != 0) { result.AddError(responseBodyObject.msg); throw new NopException(responseBodyObject.msg); } else { if(string.IsNullOrEmpty(responseBodyObject.payment_url)) { result.NewPaymentStatus = PaymentStatus.Paid; } } } return result; } /// /// 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 Task PostProcessPaymentAsync(PostProcessPaymentRequest postProcessPaymentRequest) { if(postProcessPaymentRequest.Order.PaymentStatus == PaymentStatus.Pending) { var lastPaymentResult = _tapPayResultService.GetByOrderGuidAsync(postProcessPaymentRequest.Order.OrderGuid).Result; if(lastPaymentResult != null && !string.IsNullOrEmpty(lastPaymentResult.payment_url)) { _httpContextAccessor.HttpContext.Response.Redirect(lastPaymentResult.payment_url); } } return Task.CompletedTask; } /// /// Returns a value indicating whether payment method should be hidden during checkout /// /// Shopping cart /// /// A task that represents the asynchronous operation /// The task result contains true - hide; false - display. /// public Task HidePaymentMethodAsync(IList cart) { //you can put any logic here //for example, hide this payment method if all products in the cart are downloadable //or hide this payment method if current customer is from certain country return Task.FromResult(false); } /// /// Gets additional handling fee /// /// /// A task that represents the asynchronous operation /// The task result contains the additional handling fee /// public async Task GetAdditionalHandlingFeeAsync(IList cart) { return await _orderTotalCalculationService.CalculatePaymentAdditionalFeeAsync(cart, 0, false); } /// /// Captures payment /// /// Capture payment request /// /// A task that represents the asynchronous operation /// The task result contains the capture payment result /// public Task CaptureAsync(CapturePaymentRequest capturePaymentRequest) { return Task.FromResult(new CapturePaymentResult { Errors = new[] { "Capture method not supported" } }); } /// /// Refunds a payment /// /// Request /// /// A task that represents the asynchronous operation /// The task result contains the result /// public Task RefundAsync(RefundPaymentRequest refundPaymentRequest) { return Task.FromResult(new RefundPaymentResult { Errors = new[] { "Refund method not supported" } }); } /// /// Voids a payment /// /// Request /// /// A task that represents the asynchronous operation /// The task result contains the result /// public Task VoidAsync(VoidPaymentRequest voidPaymentRequest) { return Task.FromResult(new VoidPaymentResult { Errors = new[] { "Void method not supported" } }); } /// /// 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 Task ProcessRecurringPaymentAsync(ProcessPaymentRequest processPaymentRequest) { var result = new ProcessPaymentResult { AllowStoringCreditCardNumber = true }; return Task.FromResult(result); } /// /// Cancels a recurring payment /// /// Request /// /// A task that represents the asynchronous operation /// The task result contains the result /// public Task CancelRecurringPaymentAsync(CancelRecurringPaymentRequest cancelPaymentRequest) { //always success return Task.FromResult(new CancelRecurringPaymentResult()); } /// /// 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 Task CanRePostProcessPaymentAsync(Order order) { ArgumentNullException.ThrowIfNull(order); //it's not a redirection payment method. So we always return false return Task.FromResult(false); } /// /// Validate payment form /// /// The parsed form values /// /// A task that represents the asynchronous operation /// The task result contains the list of validating errors /// public Task> ValidatePaymentFormAsync(IFormCollection form) { var warnings = new List(); return Task.FromResult>(warnings); } /// /// Get payment information /// /// The parsed form values /// /// A task that represents the asynchronous operation /// The task result contains the payment info holder /// public Task GetPaymentInfoAsync(IFormCollection form) { var paymentRequest = new ProcessPaymentRequest(); paymentRequest.CustomValues.Add("Prime", form["Prime"].ToString()); paymentRequest.CustomValues.Add("CardholderEmail", form["CardholderEmail"].ToString()); paymentRequest.CustomValues.Add("CardholderPhoneNumber", form["CardholderPhoneNumber"].ToString()); paymentRequest.CustomValues.Add("CardholderPhoneNumberCountryCode", form["CardholderPhoneNumberCountryCode"].ToString()); paymentRequest.CustomValues.Add("CardholderNameEn", form["CardholderNameEn"].ToString()); return Task.FromResult(paymentRequest); } /// /// Gets a configuration page URL /// public override string GetConfigurationPageUrl() { return $"{_webHelper.GetStoreLocation()}Admin/PaymentTapPay/Configure"; } /// /// Gets a type of a view component for displaying plugin in public store ("payment info" checkout step) /// /// View component type public Type GetPublicViewComponent() { return typeof(PaymentTapPayViewComponent); } /// /// Install the plugin /// /// A task that represents the asynchronous operation public override async Task InstallAsync() { //locales await _localizationService.AddOrUpdateLocaleResourceAsync(new Dictionary { ["Plugins.Payments.TapPay.Instructions"] = "TapPay App", ["Plugins.Payments.TapPay.PartnerKey"] = "Partner Key", ["Plugins.Payments.TapPay.AppId"] = "App ID", ["Plugins.Payments.TapPay.AppKey"] = "App Key", ["Plugins.Payments.TapPay.ServerType"] = "Sandbox / Production", ["Plugins.Payments.TapPay.ServiceEndpoint"] = "TapPay payment service endpoint", ["Plugins.Payments.TapPay.RecordEndpoint"] = "TapPay record api endpoint", ["Plugins.Payments.TapPay.Merchant"] = "Merchant Store", ["Plugins.Payments.TapPay.PaymentMethodDescription"] = "TapPay payment", ["Plugins.Payments.TapPay.RedirectUrl"] = "Frontend Redirect Url", ["Plugins.Payments.TapPay.NotifyUrl"] = "Backend Notify Url", ["Plugins.Payments.TapPay.GoBackUrl"] = "Go Back Url", }); await base.InstallAsync(); } /// /// Uninstall the plugin /// /// A task that represents the asynchronous operation public override async Task UninstallAsync() { //settings await _settingService.DeleteSettingAsync(); //locales await _localizationService.DeleteLocaleResourcesAsync("Plugins.Payments.TapPay"); await base.UninstallAsync(); } /// /// Gets a payment method description that will be displayed on checkout pages in the public store /// /// /// return description of this payment method to be display on "payment method" checkout step. good practice is to make it localizable /// for example, for a redirection payment method, description may be like this: "You will be redirected to PayPal site to complete the payment" /// /// A task that represents the asynchronous operation public async Task GetPaymentMethodDescriptionAsync() { return await _localizationService.GetResourceAsync("Plugins.Payments.TapPay.PaymentMethodDescription"); } #endregion #region Properties /// /// Gets a value indicating whether capture is supported /// public bool SupportCapture => false; /// /// Gets a value indicating whether partial refund is supported /// public bool SupportPartiallyRefund => false; /// /// Gets a value indicating whether refund is supported /// public bool SupportRefund => false; /// /// Gets a value indicating whether void is supported /// public bool SupportVoid => false; /// /// Gets a recurring payment type of payment method /// public RecurringPaymentType RecurringPaymentType => RecurringPaymentType.Manual; /// /// Gets a payment method type /// public PaymentMethodType PaymentMethodType => PaymentMethodType.Standard; /// /// Gets a value indicating whether we should display a payment information page for this plugin /// public bool SkipPaymentInfo => false; #endregion }