warrenchen ce18a5eb1c TapPay v5.24 Update
- Add Cardholder English Name, Email, and phone number
2025-12-10 15:47:19 +09:00

513 lines
21 KiB
C#
Executable File

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;
/// <summary>
/// TapPay payment processor
/// </summary>
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
/// <summary>
/// Process a payment
/// </summary>
/// <param name="processPaymentRequest">Payment info required for an order processing</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the process payment result
/// </returns>
public async Task<ProcessPaymentResult> ProcessPaymentAsync(ProcessPaymentRequest processPaymentRequest)
{
var result = new ProcessPaymentResult();
//load settings for a chosen store scope
var storeScope = await _storeContext.GetActiveStoreScopeConfigurationAsync();
// var _tapPayPaymentSettings = await _settingService.LoadSettingAsync<TapPayPaymentSettings>(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<TapPayResultModel>(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;
}
/// <summary>
/// Post process payment (used by payment gateways that require redirecting to a third-party URL)
/// </summary>
/// <param name="postProcessPaymentRequest">Payment info required for an order processing</param>
/// <returns>A task that represents the asynchronous operation</returns>
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;
}
/// <summary>
/// Returns a value indicating whether payment method should be hidden during checkout
/// </summary>
/// <param name="cart">Shopping cart</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains true - hide; false - display.
/// </returns>
public Task<bool> HidePaymentMethodAsync(IList<ShoppingCartItem> 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);
}
/// <summary>
/// Gets additional handling fee
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the additional handling fee
/// </returns>
public async Task<decimal> GetAdditionalHandlingFeeAsync(IList<ShoppingCartItem> cart)
{
return await _orderTotalCalculationService.CalculatePaymentAdditionalFeeAsync(cart, 0, false);
}
/// <summary>
/// Captures payment
/// </summary>
/// <param name="capturePaymentRequest">Capture payment request</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the capture payment result
/// </returns>
public Task<CapturePaymentResult> CaptureAsync(CapturePaymentRequest capturePaymentRequest)
{
return Task.FromResult(new CapturePaymentResult { Errors = new[] { "Capture method not supported" } });
}
/// <summary>
/// Refunds a payment
/// </summary>
/// <param name="refundPaymentRequest">Request</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the result
/// </returns>
public Task<RefundPaymentResult> RefundAsync(RefundPaymentRequest refundPaymentRequest)
{
return Task.FromResult(new RefundPaymentResult { Errors = new[] { "Refund method not supported" } });
}
/// <summary>
/// Voids a payment
/// </summary>
/// <param name="voidPaymentRequest">Request</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the result
/// </returns>
public Task<VoidPaymentResult> VoidAsync(VoidPaymentRequest voidPaymentRequest)
{
return Task.FromResult(new VoidPaymentResult { Errors = new[] { "Void method not supported" } });
}
/// <summary>
/// Process recurring payment
/// </summary>
/// <param name="processPaymentRequest">Payment info required for an order processing</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the process payment result
/// </returns>
public Task<ProcessPaymentResult> ProcessRecurringPaymentAsync(ProcessPaymentRequest processPaymentRequest)
{
var result = new ProcessPaymentResult
{
AllowStoringCreditCardNumber = true
};
return Task.FromResult(result);
}
/// <summary>
/// Cancels a recurring payment
/// </summary>
/// <param name="cancelPaymentRequest">Request</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the result
/// </returns>
public Task<CancelRecurringPaymentResult> CancelRecurringPaymentAsync(CancelRecurringPaymentRequest cancelPaymentRequest)
{
//always success
return Task.FromResult(new CancelRecurringPaymentResult());
}
/// <summary>
/// Gets a value indicating whether customers can complete a payment after order is placed but not completed (for redirection payment methods)
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the result
/// </returns>
public Task<bool> CanRePostProcessPaymentAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
//it's not a redirection payment method. So we always return false
return Task.FromResult(false);
}
/// <summary>
/// Validate payment form
/// </summary>
/// <param name="form">The parsed form values</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the list of validating errors
/// </returns>
public Task<IList<string>> ValidatePaymentFormAsync(IFormCollection form)
{
var warnings = new List<string>();
return Task.FromResult<IList<string>>(warnings);
}
/// <summary>
/// Get payment information
/// </summary>
/// <param name="form">The parsed form values</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the payment info holder
/// </returns>
public Task<ProcessPaymentRequest> 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);
}
/// <summary>
/// Gets a configuration page URL
/// </summary>
public override string GetConfigurationPageUrl()
{
return $"{_webHelper.GetStoreLocation()}Admin/PaymentTapPay/Configure";
}
/// <summary>
/// Gets a type of a view component for displaying plugin in public store ("payment info" checkout step)
/// </summary>
/// <returns>View component type</returns>
public Type GetPublicViewComponent()
{
return typeof(PaymentTapPayViewComponent);
}
/// <summary>
/// Install the plugin
/// </summary>
/// <returns>A task that represents the asynchronous operation</returns>
public override async Task InstallAsync()
{
//locales
await _localizationService.AddOrUpdateLocaleResourceAsync(new Dictionary<string, string>
{
["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();
}
/// <summary>
/// Uninstall the plugin
/// </summary>
/// <returns>A task that represents the asynchronous operation</returns>
public override async Task UninstallAsync()
{
//settings
await _settingService.DeleteSettingAsync<TapPayPaymentSettings>();
//locales
await _localizationService.DeleteLocaleResourcesAsync("Plugins.Payments.TapPay");
await base.UninstallAsync();
}
/// <summary>
/// Gets a payment method description that will be displayed on checkout pages in the public store
/// </summary>
/// <remarks>
/// 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"
/// </remarks>
/// <returns>A task that represents the asynchronous operation</returns>
public async Task<string> GetPaymentMethodDescriptionAsync()
{
return await _localizationService.GetResourceAsync("Plugins.Payments.TapPay.PaymentMethodDescription");
}
#endregion
#region Properties
/// <summary>
/// Gets a value indicating whether capture is supported
/// </summary>
public bool SupportCapture => false;
/// <summary>
/// Gets a value indicating whether partial refund is supported
/// </summary>
public bool SupportPartiallyRefund => false;
/// <summary>
/// Gets a value indicating whether refund is supported
/// </summary>
public bool SupportRefund => false;
/// <summary>
/// Gets a value indicating whether void is supported
/// </summary>
public bool SupportVoid => false;
/// <summary>
/// Gets a recurring payment type of payment method
/// </summary>
public RecurringPaymentType RecurringPaymentType => RecurringPaymentType.Manual;
/// <summary>
/// Gets a payment method type
/// </summary>
public PaymentMethodType PaymentMethodType => PaymentMethodType.Standard;
/// <summary>
/// Gets a value indicating whether we should display a payment information page for this plugin
/// </summary>
public bool SkipPaymentInfo => false;
#endregion
}