Try your search with a different keyword or use * as a wildcard.
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.WebUtilities;
using Newtonsoft.Json;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using Nop.Core.Domain.Shipping;
using Nop.Core.Domain.Stores;
using Nop.Core.Domain.Tax;
using Nop.Core.Http;
using Nop.Plugin.Payments.PayPalCommerce.Domain;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Authentication;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Identity;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Models;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Models.Enums;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Onboarding;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Orders;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Payments;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.PaymentTokens;
using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Webhooks;
using Nop.Services.Attributes;
using Nop.Services.Catalog;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Localization;
using Nop.Services.Logging;
using Nop.Services.Media;
using Nop.Services.Orders;
using Nop.Services.Payments;
using Nop.Services.Shipping;
using Nop.Services.Shipping.Pickup;
using Nop.Services.Stores;
using Nop.Services.Tax;
using Nop.Web.Framework.Mvc.Routing;
using Address = Nop.Plugin.Payments.PayPalCommerce.Services.Api.Models.Address;
using NopAddress = Nop.Core.Domain.Common.Address;
using NopOrder = Nop.Core.Domain.Orders.Order;
using NopShippingOption = Nop.Core.Domain.Shipping.ShippingOption;
using Order = Nop.Plugin.Payments.PayPalCommerce.Services.Api.Models.Order;
using ShippingOption = Nop.Plugin.Payments.PayPalCommerce.Services.Api.Models.ShippingOption;
namespace Nop.Plugin.Payments.PayPalCommerce.Services;
///
/// Represents the plugin service manager
///
public class PayPalCommerceServiceManager
{
#region Fields
private readonly CurrencySettings _currencySettings;
private readonly CustomerSettings _customerSettings;
private readonly IAddressService _addressService;
private readonly IAttributeParser _checkoutAttributeParser;
private readonly ICountryService _countryService;
private readonly ICurrencyService _currencyService;
private readonly ICustomerService _customerService;
private readonly IGenericAttributeService _genericAttributeService;
private readonly ILocalizationService _localizationService;
private readonly ILogger _logger;
private readonly INopUrlHelper _nopUrlHelper;
private readonly IOrderProcessingService _orderProcessingService;
private readonly IOrderService _orderService;
private readonly IOrderTotalCalculationService _orderTotalCalculationService;
private readonly IPaymentPluginManager _paymentPluginManager;
private readonly IPaymentService _paymentService;
private readonly IPickupPluginManager _pickupPluginManager;
private readonly IPictureService _pictureService;
private readonly IPriceCalculationService _priceCalculationService;
private readonly IProductService _productService;
private readonly IShipmentService _shipmentService;
private readonly IShippingService _shippingService;
private readonly IShoppingCartService _shoppingCartService;
private readonly IShortTermCacheManager _shortTermCacheManager;
private readonly IStateProvinceService _stateProvinceService;
private readonly IStoreContext _storeContext;
private readonly IStoreService _storeService;
private readonly ITaxService _taxService;
private readonly IWebHelper _webHelper;
private readonly IWorkContext _workContext;
private readonly OrderSettings _orderSettings;
private readonly PayPalCommerceHttpClient _httpClient;
private readonly PayPalTokenService _tokenService;
private readonly ShippingSettings _shippingSettings;
private readonly TaxSettings _taxSettings;
#endregion
#region Ctor
public PayPalCommerceServiceManager(CurrencySettings currencySettings,
CustomerSettings customerSettings,
IAddressService addressService,
IAttributeParser checkoutAttributeParser,
ICountryService countryService,
ICurrencyService currencyService,
ICustomerService customerService,
IGenericAttributeService genericAttributeService,
ILocalizationService localizationService,
ILogger logger,
INopUrlHelper nopUrlHelper,
IOrderProcessingService orderProcessingService,
IOrderService orderService,
IOrderTotalCalculationService orderTotalCalculationService,
IPaymentPluginManager paymentPluginManager,
IPaymentService paymentService,
IPickupPluginManager pickupPluginManager,
IPictureService pictureService,
IPriceCalculationService priceCalculationService,
IProductService productService,
IShipmentService shipmentService,
IShippingService shippingService,
IShoppingCartService shoppingCartService,
IShortTermCacheManager shortTermCacheManager,
IStateProvinceService stateProvinceService,
IStoreContext storeContext,
IStoreService storeService,
ITaxService taxService,
IWebHelper webHelper,
IWorkContext workContext,
OrderSettings orderSettings,
PayPalCommerceHttpClient httpClient,
PayPalTokenService tokenService,
ShippingSettings shippingSettings,
TaxSettings taxSettings)
{
_currencySettings = currencySettings;
_customerSettings = customerSettings;
_addressService = addressService;
_checkoutAttributeParser = checkoutAttributeParser;
_countryService = countryService;
_currencyService = currencyService;
_customerService = customerService;
_genericAttributeService = genericAttributeService;
_localizationService = localizationService;
_logger = logger;
_nopUrlHelper = nopUrlHelper;
_orderProcessingService = orderProcessingService;
_orderService = orderService;
_orderTotalCalculationService = orderTotalCalculationService;
_paymentPluginManager = paymentPluginManager;
_paymentService = paymentService;
_pickupPluginManager = pickupPluginManager;
_pictureService = pictureService;
_priceCalculationService = priceCalculationService;
_productService = productService;
_shipmentService = shipmentService;
_shippingService = shippingService;
_shoppingCartService = shoppingCartService;
_shortTermCacheManager = shortTermCacheManager;
_stateProvinceService = stateProvinceService;
_storeContext = storeContext;
_storeService = storeService;
_taxService = taxService;
_webHelper = webHelper;
_workContext = workContext;
_orderSettings = orderSettings;
_httpClient = httpClient;
_tokenService = tokenService;
_shippingSettings = shippingSettings;
_taxSettings = taxSettings;
}
#endregion
#region Utilities
///
/// Handle function and get result
///
/// Result type
/// Function
/// Whether to log errors
///
/// A task that represents the asynchronous operation
/// The task result contains the result; error message if exists
///
private async Task<(TResult Result, string Error)> HandleFunctionAsync(Func> function, bool logErrors = true)
{
try
{
//invoke function
return (await function(), default);
}
catch (Exception exception)
{
//log errors
if (logErrors)
{
var logMessage = $"{PayPalCommerceDefaults.SystemName} error:{Environment.NewLine}{exception.Message}";
var exceptionToLog = exception is NopException nopException ? nopException.InnerException ?? nopException : exception;
var customer = await _workContext.GetCurrentCustomerAsync();
await _logger.ErrorAsync(logMessage, exceptionToLog, customer);
}
return (default, exception.Message);
}
}
#region Components
///
/// Prepare amount value for Pay Later messages
///
/// Button placement
/// Customer
/// Currency code
/// Product id
///
/// A task that represents the asynchronous operation
/// The task result contains the amount value
///
private async Task PrepareMessagesAmountAsync(ButtonPlacement placement, Customer customer, string currencyCode, int? productId)
{
//cache result during HTTP request
return await _shortTermCacheManager.GetAsync(async () =>
{
var store = await _storeContext.GetCurrentStoreAsync();
var product = await _productService.GetProductByIdAsync(productId ?? 0);
var cart = await _shoppingCartService.GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, store.Id);
var (_, _, _, subTotal, _) = await _orderTotalCalculationService.GetShoppingCartSubTotalAsync(cart, true);
var amount = placement switch
{
ButtonPlacement.Cart => subTotal,
ButtonPlacement.Product when product is not null
=> (await _priceCalculationService.GetFinalPriceAsync(product, customer, store)).finalPrice, //+ subTotal,
ButtonPlacement.PaymentMethod
=> (await _orderTotalCalculationService.GetShoppingCartTotalAsync(cart, null, usePaymentMethodAdditionalFee: false))
.shoppingCartTotal ?? subTotal,
_ => (decimal?)null
};
return amount is not null ? PrepareMoney(amount.Value, currencyCode).Value : null;
}, new($"{PayPalCommerceDefaults.SystemName}.Messages.Amount-{productId ?? 0}"));
}
#endregion
#region Orders
///
/// Prepare money object
///
/// Amount value
/// Currency code
/// Money object
private static Money PrepareMoney(decimal value, string currencyCode)
{
var format = PayPalCommerceDefaults.CurrenciesWithoutDecimals.Contains(currencyCode.ToUpper()) ? "0" : "0.00";
return new()
{
CurrencyCode = currencyCode,
Value = value.ToString(format, CultureInfo.InvariantCulture)
};
}
///
/// Convert money object to decimal value
///
/// Amount value
/// Decimal value
private static decimal ConvertMoney(Money amount)
{
return decimal.TryParse(amount?.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : decimal.Zero;
}
///
/// Prepare shopping cart details
///
/// Button placement
/// Whether to prepare shipping details
///
/// A task that represents the asynchronous operation
/// The task result contains the cart details
///
private async Task PrepareCartDetailsAsync(ButtonPlacement placement, bool withShipping = true)
{
//get the primary store currency
var currencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode;
if (string.IsNullOrEmpty(currencyCode))
throw new NopException("Primary store currency not set");
//get customer shopping cart
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
var cart = await _shoppingCartService.GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, store.Id);
if (!cart.Any())
throw new NopException("Shopping cart is empty");
//get billing address
var billingAddress = await _addressService.GetAddressByIdAsync(customer.BillingAddressId ?? 0);
if (placement == ButtonPlacement.PaymentMethod && billingAddress is null)
throw new NopException("Customer billing address not set");
var shippingIsRequired = await _shoppingCartService.ShoppingCartRequiresShippingAsync(cart);
var details = new CartDetails
{
Placement = placement,
CurrencyCode = currencyCode,
Customer = customer,
Store = store,
Cart = cart.ToList(),
BillingAddress = billingAddress,
ShippingIsRequired = shippingIsRequired,
};
if (!withShipping)
return details;
//get shipping details
details.ShippingOption = await _genericAttributeService
.GetAttributeAsync(customer, NopCustomerDefaults.SelectedShippingOptionAttribute, store.Id);
details.PickupPoint = await _genericAttributeService
.GetAttributeAsync(customer, NopCustomerDefaults.SelectedPickupPointAttribute, store.Id);
details.IsPickup = _shippingSettings.AllowPickupInStore && details.PickupPoint is not null;
details.ShippingAddress = details.IsPickup ? new NopAddress
{
Address1 = details.PickupPoint.Address,
City = details.PickupPoint.City,
County = details.PickupPoint.County,
CountryId = (await _countryService.GetCountryByTwoLetterIsoCodeAsync(details.PickupPoint.CountryCode))?.Id,
StateProvinceId = (await _stateProvinceService.GetStateProvinceByAbbreviationAsync(details.PickupPoint.StateAbbreviation,
(await _countryService.GetCountryByTwoLetterIsoCodeAsync(details.PickupPoint.CountryCode))?.Id))?.Id,
ZipPostalCode = details.PickupPoint.ZipPostalCode,
CreatedOnUtc = DateTime.UtcNow
} : await _addressService.GetAddressByIdAsync(customer.ShippingAddressId ?? 0);
if (placement == ButtonPlacement.PaymentMethod && shippingIsRequired && details.ShippingAddress is null)
throw new NopException("Customer shipping address not set");
return details;
}
///
/// Prepare order context
///
/// Plugin settings
/// Shopping cart details
/// Order internal id
/// Apple Pay payment
/// Experience context
private ExperienceContext PrepareOrderContext(PayPalCommerceSettings settings, CartDetails details, string orderGuid, bool isApplePay = false)
{
var protocol = _webHelper.GetCurrentRequestProtocol();
var shippingPreference = ShippingPreferenceType.NO_SHIPPING.ToString().ToUpper();
if (details.ShippingIsRequired)
{
shippingPreference = details.Placement == ButtonPlacement.PaymentMethod && !isApplePay
? ShippingPreferenceType.SET_PROVIDED_ADDRESS.ToString().ToUpper()
: ShippingPreferenceType.GET_FROM_FILE.ToString().ToUpper();
}
return new()
{
//Locale = null, //PayPal auto detects this
BrandName = CommonHelper.EnsureMaximumLength(details.Store.Name, 127),
LandingPage = LandingPageType.NO_PREFERENCE.ToString().ToUpper(),
UserAction = details.Placement == ButtonPlacement.PaymentMethod && settings.SkipOrderConfirmPage
? UserActionType.PAY_NOW.ToString().ToUpper()
: UserActionType.CONTINUE.ToString().ToUpper(),
CancelUrl = details.Placement switch
{
ButtonPlacement.PaymentMethod => _nopUrlHelper.RouteUrl(PayPalCommerceDefaults.Route.PaymentInfo, null, protocol),
ButtonPlacement.Cart or ButtonPlacement.Product => _nopUrlHelper.RouteUrl(NopRouteNames.General.CART, null, protocol),
_ => null
},
ReturnUrl = _nopUrlHelper.RouteUrl(PayPalCommerceDefaults.Route.ConfirmOrder, new { token = orderGuid, approve = true }, protocol),
PaymentMethodPreference = settings.ImmediatePaymentRequired
? PaymentMethodPreferenceType.IMMEDIATE_PAYMENT_REQUIRED.ToString().ToUpper()
: PaymentMethodPreferenceType.UNRESTRICTED.ToString().ToUpper(),
ShippingPreference = shippingPreference
};
}
///
/// Prepare recurring payment context
///
/// Shopping cart details
/// Experience context
private ExperienceContext PrepareRecurringPaymentContext(CartDetails details)
{
var protocol = _webHelper.GetCurrentRequestProtocol();
return new()
{
BrandName = CommonHelper.EnsureMaximumLength(details.Store.Name, 127),
CancelUrl = _nopUrlHelper.RouteUrl(PayPalCommerceDefaults.Route.PaymentInfo, null, protocol),
ReturnUrl = _nopUrlHelper.RouteUrl(PayPalCommerceDefaults.Route.ApproveToken, null, protocol),
PaymentMethodPreference = PaymentMethodPreferenceType.IMMEDIATE_PAYMENT_REQUIRED.ToString().ToUpper(),
ShippingPreference = details.ShippingIsRequired
? ShippingPreferenceType.SET_PROVIDED_ADDRESS.ToString().ToUpper()
: ShippingPreferenceType.NO_SHIPPING.ToString().ToUpper()
};
}
///
/// Prepare order billing details
///
/// Plugin settings
/// Shopping cart details
///
/// A task that represents the asynchronous operation
/// The task result contains the payer with the billing details
///
private async Task PrepareBillingDetailsAsync(PayPalCommerceSettings settings, CartDetails details)
{
var customer = details.Customer;
var address = details.BillingAddress;
var isPaymentMethodPage = details.Placement == ButtonPlacement.PaymentMethod;
var email = CommonHelper.EnsureMaximumLength(isPaymentMethodPage ? address.Email : customer.Email, 254);
var name = new Name
{
GivenName = CommonHelper.EnsureMaximumLength(isPaymentMethodPage ? address.FirstName : customer.FirstName, 140),
Surname = CommonHelper.EnsureMaximumLength(isPaymentMethodPage ? address.LastName : customer.LastName, 140)
};
//phone number format is unpredictable
//var phone = isPaymentMethodPage
// ? (!string.IsNullOrEmpty(address.PhoneNumber) ? new Phone { PhoneNumber = new() { NationalNumber = CommonHelper.EnsureMaximumLength(CommonHelper.EnsureNumericOnly(address.PhoneNumber), 14) } } : null)
// : !string.IsNullOrEmpty(customer.Phone) ? new Phone { PhoneNumber = new() { NationalNumber = CommonHelper.EnsureMaximumLength(CommonHelper.EnsureNumericOnly(customer.Phone), 14) } } : null;
var birthDate = customer.DateOfBirth?.ToString("yyyy-MM-dd");
var country = await _countryService.GetCountryByIdAsync(isPaymentMethodPage ? address.CountryId ?? 0 : customer.CountryId);
var state = await _stateProvinceService
.GetStateProvinceByIdAsync(isPaymentMethodPage ? address.StateProvinceId ?? 0 : customer.StateProvinceId);
var billingAddress = new Address
{
AddressLine1 = CommonHelper.EnsureMaximumLength(isPaymentMethodPage ? address.Address1 : customer.StreetAddress, 300),
AddressLine2 = CommonHelper.EnsureMaximumLength(isPaymentMethodPage ? address.Address2 : customer.StreetAddress2, 300),
AdminArea2 = CommonHelper.EnsureMaximumLength(isPaymentMethodPage ? address.City : customer.City, 120),
AdminArea1 = CommonHelper.EnsureMaximumLength(state?.Abbreviation, 300),
CountryCode = CommonHelper.EnsureMaximumLength(country?.TwoLetterIsoCode, 2),
PostalCode = CommonHelper.EnsureMaximumLength(isPaymentMethodPage ? address.ZipPostalCode : customer.ZipPostalCode, 60)
};
//country is required
if (string.IsNullOrEmpty(billingAddress.CountryCode))
billingAddress = null;
//try to get Vault customer id
var id = settings.UseVault ? await GetVaultCustomerIdAsync(settings, customer.Id) : null;
return new()
{
Id = id,
EmailAddress = email,
Name = name,
BirthDate = birthDate,
Address = billingAddress,
MerchantCustomerId = string.IsNullOrEmpty(id) ? customer.Id.ToString() : null //we can only use a single identifier
};
}
///
/// Prepare order items
///
/// Shopping cart details
///
/// A task that represents the asynchronous operation
/// The task result contains the list of purchase items
///
private async Task> PrepareOrderItemsAsync(CartDetails details)
{
//cart items
var items = await details.Cart.SelectAwait(async item =>
{
var product = await _productService.GetProductByIdAsync(item.ProductId);
if (product is null)
return null;
var sku = await _productService.FormatSkuAsync(product, item.AttributesXml);
var url = await _nopUrlHelper.RouteGenericUrlAsync(product, _webHelper.GetCurrentRequestProtocol());
var picture = await _pictureService.GetProductPictureAsync(product, item.AttributesXml);
//PayPal doesn't currently support WebP images
var ext = await _pictureService.GetFileExtensionFromMimeTypeAsync(picture?.MimeType);
var (imageUrl, _) = ext != "webp" ? await _pictureService.GetPictureUrlAsync(picture) : default;
var (itemSubTotal, itemDiscount, _, _) = await _shoppingCartService.GetSubTotalAsync(item, true);
var unitPrice = itemSubTotal / item.Quantity;
var (unitPriceExclTax, _) = await _taxService.GetProductPriceAsync(product, unitPrice, false, details.Customer);
return new Item
{
Name = CommonHelper.EnsureMaximumLength(product.Name, 127),
Description = CommonHelper.EnsureMaximumLength(product.ShortDescription, 127),
Sku = CommonHelper.EnsureMaximumLength(sku, 127),
Quantity = item.Quantity.ToString(),
Category = product.IsDownload
? CategoryType.DIGITAL_GOODS.ToString().ToUpper()
: CategoryType.PHYSICAL_GOODS.ToString().ToUpper(),
Url = url,
ImageUrl = imageUrl,
UnitAmount = PrepareMoney(unitPriceExclTax, details.CurrencyCode)
};
}).Where(item => item is not null).ToListAsync();
//and checkout attributes
var checkoutAttributes = await _genericAttributeService
.GetAttributeAsync(details.Customer, NopCustomerDefaults.CheckoutAttributes, details.Store.Id);
var checkoutAttributeValues = _checkoutAttributeParser.ParseAttributeValues(checkoutAttributes);
await foreach (var (attribute, values) in checkoutAttributeValues)
{
await foreach (var attributeValue in values)
{
var (attributePriceExclTax, _) = await _taxService
.GetCheckoutAttributePriceAsync(attribute, attributeValue, false, details.Customer);
items.Add(new()
{
Name = CommonHelper.EnsureMaximumLength(attribute.Name, 127),
Description = CommonHelper.EnsureMaximumLength($"{attribute.Name} - {attributeValue.Name}", 127),
Quantity = 1.ToString(),
UnitAmount = PrepareMoney(attributePriceExclTax, details.CurrencyCode)
});
}
}
return items;
}
///
/// Prepare order amount with breakdown
///
/// Shopping cart details
/// Purchase items
///
/// A task that represents the asynchronous operation
/// The task result contains the order amount with breakdown
///
private async Task PrepareOrderMoneyAsync(CartDetails details, List- items)
{
//in some rare cases we need an additional item to adjust the order total
//this can happen due to complex discounts or a large order and related to rounding in calculations
//PayPal uses two decimal places, while nopCommerce can use more complex types of rounding (configured for each currency separately)
var adjustmentName = await _localizationService.GetResourceAsync("Plugins.Payments.PayPalCommerce.Order.Adjustment.Name");
var adjustmentDescription = await _localizationService.GetResourceAsync("Plugins.Payments.PayPalCommerce.Order.Adjustment.Description");
if (items.FirstOrDefault(item => adjustmentName.Equals(item.Name) && adjustmentDescription.Equals(item.Description)) is Item adjustmentItem)
items.Remove(adjustmentItem);
var (total, _, _, _, _, _) = await _orderTotalCalculationService
.GetShoppingCartTotalAsync(details.Cart, usePaymentMethodAdditionalFee: false);
if (total is null)
{
if (details.Placement == ButtonPlacement.PaymentMethod)
throw new NopException("Shopping cart total couldn't be calculated now");
//on product and cart pages the total is not yet calculated, so use subtotal here
var (_, _, subTotal, _, _) = await _orderTotalCalculationService.GetShoppingCartSubTotalAsync(details.Cart, includingTax: false);
total = subTotal;
}
var orderTotal = PrepareMoney(total.Value, details.CurrencyCode);
var (shippingTotal, _, _) = await _orderTotalCalculationService
.GetShoppingCartShippingTotalAsync(details.Cart, includingTax: _taxSettings.ShippingPriceIncludesTax);
var orderShippingTotal = PrepareMoney(shippingTotal ?? decimal.Zero, details.CurrencyCode);
var (taxTotal, _) = await _orderTotalCalculationService.GetTaxTotalAsync(details.Cart, usePaymentMethodAdditionalFee: false);
var orderTaxTotal = PrepareMoney(taxTotal, details.CurrencyCode);
var itemAdjustment = decimal.Zero;
var itemTotal = items.Sum(item => ConvertMoney(item.UnitAmount) * int.Parse(item.Quantity));
var discountTotal = itemTotal + ConvertMoney(orderTaxTotal) + ConvertMoney(orderShippingTotal) - ConvertMoney(orderTotal);
if (discountTotal < decimal.Zero)
{
itemAdjustment = -discountTotal;
itemTotal += itemAdjustment;
discountTotal = decimal.Zero;
}
var orderItemTotal = PrepareMoney(itemTotal, details.CurrencyCode);
var orderDiscount = PrepareMoney(discountTotal, details.CurrencyCode);
//set adjustment item if needed
if (itemAdjustment > decimal.Zero)
{
var unitAmount = PrepareMoney(itemAdjustment, details.CurrencyCode);
if (ConvertMoney(unitAmount) > decimal.Zero)
{
items.Add(new()
{
Name = adjustmentName,
Description = adjustmentDescription,
Quantity = 1.ToString(),
UnitAmount = unitAmount
});
}
}
return new()
{
CurrencyCode = details.CurrencyCode,
Value = orderTotal.Value,
Breakdown = new()
{
ItemTotal = orderItemTotal,
TaxTotal = orderTaxTotal,
Shipping = orderShippingTotal,
Discount = orderDiscount
}
};
}
///
/// Prepare order shipping details
///
/// Shopping cart details
/// Selected shipping option
/// Apple Pay payment
///
/// A task that represents the asynchronous operation
/// The task result contains the shipping details
///
private async Task PrepareShippingDetailsAsync(CartDetails details, string selectedOptionId, bool isApplePay = false)
{
if (!details.ShippingIsRequired)
return null;
var shippingAddress = details.ShippingAddress;
var fullName = shippingAddress is not null && !details.IsPickup
? await _customerService.GetCustomerFullNameAsync(new() { FirstName = shippingAddress.FirstName, LastName = shippingAddress.LastName })
: null;
if (string.IsNullOrEmpty(fullName))
fullName = await _customerService.GetCustomerFullNameAsync(details.Customer);
//if the shipping option type is set to PICKUP, then the full name should start with S2S meaning ship to store (for example, S2S My Store)
if (details.IsPickup && details.PickupPoint is not null)
fullName = $"S2S {details.PickupPoint.Name}";
var address = shippingAddress is not null ? new Address
{
AddressLine1 = CommonHelper.EnsureMaximumLength(shippingAddress.Address1, 300),
AddressLine2 = CommonHelper.EnsureMaximumLength(shippingAddress.Address2, 300),
AdminArea2 = CommonHelper.EnsureMaximumLength(shippingAddress.City, 120),
AdminArea1 = CommonHelper.EnsureMaximumLength((await _stateProvinceService
.GetStateProvinceByAddressAsync(shippingAddress))?.Abbreviation, 300),
CountryCode = (await _countryService.GetCountryByIdAsync(shippingAddress.CountryId ?? 0))?.TwoLetterIsoCode,
PostalCode = CommonHelper.EnsureMaximumLength(shippingAddress.ZipPostalCode, 60)
} : null;
var shipping = new Shipping
{
Name = new() { FullName = CommonHelper.EnsureMaximumLength(fullName, 300), },
Address = address
};
if (details.Placement == ButtonPlacement.PaymentMethod && !isApplePay)
{
shipping.Type = details.IsPickup
? ShippingType.SHIPPING.ToString().ToUpper() //PICKUP_IN_STORE option doesn't work for some reason
: ShippingType.SHIPPING.ToString().ToUpper();
return shipping;
}
var (shippingOptions, pickupPoints) = await PrepareShippingOptionsAsync(details);
if (!shippingOptions?.Any() ?? true)
throw new NopException("No available shipping options");
var selectedShippingOption = shippingOptions.FirstOrDefault();
if (!string.IsNullOrEmpty(selectedOptionId))
{
var existingOption = shippingOptions
.FirstOrDefault(option => string.Equals(option.Name, selectedOptionId, StringComparison.InvariantCultureIgnoreCase))
?? throw new NopException("Selected shipping option is unavailable");
selectedShippingOption = existingOption;
}
if (selectedShippingOption is null)
throw new NopException("Selected shipping option is unavailable");
PickupPoint pickupPoint = null;
if (selectedShippingOption.IsPickupInStore)
{
pickupPoint = await pickupPoints.FirstOrDefaultAwaitAsync(async point =>
string.Equals(await GetShippingOptionNameAsync(new() { Name = point.Name, IsPickupInStore = true }), selectedShippingOption.Name) &&
string.Equals(point.ProviderSystemName, selectedShippingOption.ShippingRateComputationMethodSystemName));
details.IsPickup = true;
details.PickupPoint = pickupPoint;
//if the shipping option type is set to PICKUP, then the full name should start with S2S meaning ship to store (for example, S2S My Store)
if (details.IsPickup && details.PickupPoint is not null)
shipping.Name.FullName = $"S2S {details.PickupPoint.Name}";
}
details.ShippingOption = selectedShippingOption;
//save selected options in attributes
await _genericAttributeService
.SaveAttributeAsync(details.Customer, NopCustomerDefaults.SelectedShippingOptionAttribute, selectedShippingOption, details.Store.Id);
await _genericAttributeService
.SaveAttributeAsync(details.Customer, NopCustomerDefaults.SelectedPickupPointAttribute, pickupPoint, details.Store.Id);
async Task convertOptionAsync(NopShippingOption option)
{
var (adjustedShippingRate, _) = await _orderTotalCalculationService
.AdjustShippingRateAsync(option.Rate, details.Cart, option.IsPickupInStore);
//var (rate, _) = await _taxService.GetShippingPriceAsync(adjustedShippingRate, details.Customer);
//PayPal currently handles taxable shipping incorrectly, so we display shipping rates without tax, but it'll be included to tax total
var rate = adjustedShippingRate;
return new ShippingOption
{
Id = CommonHelper.EnsureMaximumLength(option.Name, 127),
Label = CommonHelper.EnsureMaximumLength(option.Name, 127),
Selected = false,
Type = option.IsPickupInStore
? ShippingType.PICKUP.ToString().ToUpper()
: ShippingType.SHIPPING.ToString().ToUpper(),
Amount = PrepareMoney(rate, details.CurrencyCode)
};
}
shipping.Options = details.Placement == ButtonPlacement.PaymentMethod
? [await convertOptionAsync(selectedShippingOption)]
: await shippingOptions.SelectAwait(async option => await convertOptionAsync(option)).ToListAsync();
//set default shipping option
(shipping.Options
.FirstOrDefault(option => string.Equals(option.Id, details.ShippingOption?.Name, StringComparison.InvariantCultureIgnoreCase))
?? shipping.Options.First())
.Selected = true;
return shipping;
}
///
/// Prepare available shipping options
///
/// Shopping cart details
///
/// A task that represents the asynchronous operation
/// The task result contains the list of shipping options; list of pickup points
///
private async Task<(List ShippingOptions, List PickupPoints)> PrepareShippingOptionsAsync(CartDetails details)
{
if (!details.ShippingIsRequired)
return (null, null);
if (details.ShippingAddress is null && !_shippingSettings.AllowPickupInStore)
return (null, null);
var shippingOptions = new List();
var pickupPoints = new List();
//pickup points
if (_shippingSettings.AllowPickupInStore)
{
var pickupPointProviders = await _pickupPluginManager.LoadActivePluginsAsync(details.Customer, details.Store.Id);
if (pickupPointProviders.Any())
{
var pickupPointsResponse = await _shippingService
.GetPickupPointsAsync(details.Cart, details.BillingAddress, details.Customer, storeId: details.Store.Id);
if (pickupPointsResponse.Success)
{
shippingOptions.AddRange(await pickupPointsResponse.PickupPoints.SelectAwait(async point => new NopShippingOption
{
Name = await GetShippingOptionNameAsync(new() { Name = point.Name, IsPickupInStore = true }),
Rate = point.PickupFee,
Description = point.Description,
ShippingRateComputationMethodSystemName = point.ProviderSystemName,
IsPickupInStore = true,
DisplayOrder = point.DisplayOrder,
TransitDays = point.TransitDays
}).ToListAsync());
pickupPoints.AddRange(pickupPointsResponse.PickupPoints);
}
}
}
//and shipping options
if (details.ShippingAddress is not null)
{
var shippingOptionResponse = await _shippingService
.GetShippingOptionsAsync(details.Cart, details.ShippingAddress, details.Customer, storeId: details.Store.Id);
if (shippingOptionResponse.Success)
shippingOptions.AddRange(shippingOptionResponse.ShippingOptions);
}
//sort options
shippingOptions = (_shippingSettings.ShippingSorting switch
{
ShippingSortingEnum.ShippingCost => shippingOptions.OrderBy(option => option.Rate),
_ => shippingOptions.OrderBy(option => option.DisplayOrder)
}).ToList();
return (shippingOptions, pickupPoints);
}
///
/// Prepare the updated shipping details
///
/// Shopping cart details
/// Customer email
/// Selected shipping address
/// Selected shipping option
///
/// A task that represents the asynchronous operation
/// The task result contains the shipping details
///
private async Task PrepareUpdatedShippingAsync(CartDetails details, string email,
(string City, string State, string Country, string PostalCode) selectedAddress,
(string Id, string Type) selectedOption)
{
//change shipping address when customer selects another one
if (!string.IsNullOrEmpty(selectedAddress.City) && !string.IsNullOrEmpty(selectedAddress.State) &&
!string.IsNullOrEmpty(selectedAddress.Country) && !string.IsNullOrEmpty(selectedAddress.PostalCode))
{
var country = await _countryService.GetCountryByTwoLetterIsoCodeAsync(selectedAddress.Country);
var state = await _stateProvinceService.GetStateProvinceByAbbreviationAsync(selectedAddress.State, country?.Id);
var newShippingAddress = await PrepareCustomerAddressAsync(details.Customer, new()
{
Email = email ?? details.Customer.Email,
City = selectedAddress.City,
StateProvinceId = state?.Id,
CountryId = country?.Id,
ZipPostalCode = selectedAddress.PostalCode
});
if (newShippingAddress.Id != details.Customer.ShippingAddressId)
{
details.Customer.ShippingAddressId = newShippingAddress.Id;
await _customerService.UpdateCustomerAsync(details.Customer);
}
}
//change shipping option when customer selects another one
if (string.IsNullOrEmpty(selectedOption.Id))
{
var shippingOption = await _genericAttributeService
.GetAttributeAsync(details.Customer, NopCustomerDefaults.SelectedShippingOptionAttribute, details.Store.Id);
if (shippingOption is not null)
{
var type = shippingOption.IsPickupInStore ? ShippingType.PICKUP.ToString() : ShippingType.SHIPPING.ToString();
selectedOption = (shippingOption.Name, type);
}
}
//set new parameters to update shipping details
details.BillingAddress = await _customerService.GetCustomerBillingAddressAsync(details.Customer);
details.ShippingAddress = await _customerService.GetCustomerShippingAddressAsync(details.Customer);
details.IsPickup =
selectedOption.Type?.ToUpper() == ShippingType.PICKUP.ToString() ||
selectedOption.Type?.ToUpper() == ShippingType.PICKUP_IN_STORE.ToString() ||
selectedOption.Type?.ToUpper() == ShippingType.PICKUP_FROM_PERSON.ToString();
if (details.ShippingAddress is null && !details.IsPickup)
return null;
var shipping = await PrepareShippingDetailsAsync(details, selectedOption.Id);
return shipping;
}
///
/// Prepare the purchase unit details
///
/// Plugin settings
/// Shopping cart details
/// Order guid
///
/// A task that represents the asynchronous operation
/// The task result contains the purchase unit details
///
private async Task PreparePurchaseUnitAsync(PayPalCommerceSettings settings, CartDetails details, string orderGuid)
{
var shipping = await PrepareShippingDetailsAsync(details, details.ShippingOption?.Name);
var items = await PrepareOrderItemsAsync(details);
var orderAmount = await PrepareOrderMoneyAsync(details, items);
var cardData = new CardData
{
Level2 = new() { InvoiceId = CommonHelper.EnsureMaximumLength(orderGuid, 127) },
Level3 = new()
{
LineItems = items,
ShippingAmount = orderAmount.Breakdown.Shipping,
ShippingAddress = shipping?.Address,
ShipsFromPostalCode = CommonHelper.EnsureMaximumLength((await _addressService
.GetAddressByIdAsync(_shippingSettings.ShippingOriginAddressId))?.ZipPostalCode, 60)
},
};
return new PurchaseUnit
{
CustomId = CommonHelper.EnsureMaximumLength(orderGuid, 127),
InvoiceId = CommonHelper.EnsureMaximumLength(orderGuid, 127),
Description = CommonHelper.EnsureMaximumLength($"Purchase at '{details.Store.Name}'", 127),
SoftDescriptor = CommonHelper.EnsureMaximumLength(details.Store.Name, 22),
Payee = new() { MerchantId = settings.MerchantId },
Items = items,
Amount = orderAmount,
Shipping = shipping,
SupplementaryData = new() { Card = cardData }
};
}
///
/// Prepare patches to update an order
///
/// Purchase unit details
/// List of patch objects
private static List> PreparePatches(PurchaseUnit purchaseUnit)
{
var patches = new List>
{
new()
{
Op = PatchOpType.REPLACE.ToString().ToLower(),
Path = "/purchase_units/@reference_id=='default'/amount",
Value = purchaseUnit.Amount
},
new()
{
Op = PatchOpType.REPLACE.ToString().ToLower(),
Path = "/purchase_units/@reference_id=='default'/items",
Value = purchaseUnit.Items
},
new()
{
Op = PatchOpType.REPLACE.ToString().ToLower(),
Path = "/purchase_units/@reference_id=='default'/supplementary_data/card",
Value = purchaseUnit.SupplementaryData.Card
}
};
if (purchaseUnit.Shipping?.Name is not null)
{
patches.Add(new()
{
Op = PatchOpType.REPLACE.ToString().ToLower(),
Path = "/purchase_units/@reference_id=='default'/shipping/name",
Value = purchaseUnit.Shipping.Name
});
}
if (purchaseUnit.Shipping?.Address is not null)
{
patches.Add(new()
{
Op = PatchOpType.REPLACE.ToString().ToLower(),
Path = "/purchase_units/@reference_id=='default'/shipping/address",
Value = purchaseUnit.Shipping.Address
});
}
if (purchaseUnit.Shipping?.Options is not null)
{
patches.Add(new()
{
Op = PatchOpType.REPLACE.ToString().ToLower(),
Path = "/purchase_units/@reference_id=='default'/shipping/options",
Value = purchaseUnit.Shipping.Options
});
}
if (!string.IsNullOrEmpty(purchaseUnit.Shipping?.Type))
{
patches.Add(new()
{
Op = PatchOpType.REPLACE.ToString().ToLower(),
Path = "/purchase_units/@reference_id=='default'/shipping/type",
Value = purchaseUnit.Shipping.Type
});
}
return patches;
}
///
/// Get customer's address (existing or a new one)
///
/// Customer
/// Address to check
///
/// A task that represents the asynchronous operation
/// The task result contains the customer address
///
private async Task PrepareCustomerAddressAsync(Customer customer, NopAddress newAddress)
{
var customerAddresses = await _customerService.GetAddressesByCustomerIdAsync(customer.Id);
var query = customerAddresses.AsQueryable();
if (!string.IsNullOrEmpty(newAddress.Email))
query = query.Where(address => string.Equals(address.Email, newAddress.Email));
if (!string.IsNullOrEmpty(newAddress.FirstName))
query = query.Where(address => string.Equals(address.FirstName, newAddress.FirstName));
if (!string.IsNullOrEmpty(newAddress.LastName))
query = query.Where(address => string.Equals(address.LastName, newAddress.LastName));
if (!string.IsNullOrEmpty(newAddress.Address1))
query = query.Where(address => string.Equals(address.Address1, newAddress.Address1));
if (!string.IsNullOrEmpty(newAddress.Address2))
query = query.Where(address => string.Equals(address.Address2, newAddress.Address2));
if (!string.IsNullOrEmpty(newAddress.City))
query = query.Where(address => string.Equals(address.City, newAddress.City));
if (newAddress.StateProvinceId > 0)
query = query.Where(address => address.StateProvinceId == newAddress.StateProvinceId);
if (newAddress.CountryId > 0)
query = query.Where(address => address.CountryId == newAddress.CountryId);
if (!string.IsNullOrEmpty(newAddress.ZipPostalCode))
query = query.Where(address => string.Equals(address.ZipPostalCode, newAddress.ZipPostalCode));
var existingAddress = query.FirstOrDefault();
if (existingAddress is not null)
return existingAddress;
await _addressService.InsertAddressAsync(newAddress);
await _customerService.InsertCustomerAddressAsync(customer, newAddress);
return newAddress;
}
///
/// Get shipping option name
///
/// Shipping option
///
/// A task that represents the asynchronous operation
/// The task result contains the shipping option name
///
private async Task GetShippingOptionNameAsync(NopShippingOption option)
{
return option.IsPickupInStore
? (string.IsNullOrEmpty(option.Name)
? await _localizationService.GetResourceAsync("Checkout.PickupPoints.NullName")
: string.Format(await _localizationService.GetResourceAsync("Checkout.PickupPoints.Name"), option.Name))
: option.Name;
}
#endregion
#region Payment tokens
///
/// Prepare payment tokens with additional details
///
/// Plugin settings
/// Payment tokens
///
/// A task that represents the asynchronous operation
/// The task result contains the list of payment tokens
///
private async Task> PreparePaymentTokensAsync(PayPalCommerceSettings settings, IList tokens)
{
var paymentTokens = new List();
foreach (var token in tokens)
{
if (string.IsNullOrEmpty(token.VaultCustomerId))
continue;
//try to get payment tokens from the vault
var response = await _httpClient
.RequestAsync(new() { VaultCustomerId = token.VaultCustomerId }, settings);
paymentTokens.AddRange(response?.PaymentTokens ?? new());
}
if (paymentTokens?.Any() != true)
return new List();
return tokens.OrderBy(token => token.IsPrimaryMethod ? 0 : 1).ThenBy(token => token.Id).Select(paymentToken =>
{
var existingToken = paymentTokens
.FirstOrDefault(token => string.Equals(token.Id, paymentToken.VaultId, StringComparison.InvariantCultureIgnoreCase));
if (existingToken is null)
return null;
//set title and expiration date
paymentToken.Title = existingToken.PaymentSource?.Card is not null
? $"{existingToken.PaymentSource.Card.Brand} *{existingToken.PaymentSource.Card.LastDigits}"
: (existingToken.PaymentSource?.Venmo is not null
? existingToken.PaymentSource.Venmo.UserName
: (existingToken.PaymentSource?.PayPal is not null
? existingToken.PaymentSource.PayPal.EmailAddress
: "N/A"));
paymentToken.Expiration = existingToken.PaymentSource?.Card is not null ? existingToken.PaymentSource.Card.Expiry : "N/A";
return paymentToken;
}).Where(token => token is not null).ToList();
}
///
/// Get the unique ID for a customer in PayPal Vault
///
/// Plugin settings
/// Customer id
///
/// A task that represents the asynchronous operation
/// The task result contains the vault customer id
///
private async Task GetVaultCustomerIdAsync(PayPalCommerceSettings settings, int customerId)
{
return (await _tokenService.GetAllTokensAsync(settings.ClientId, customerId))
.OrderBy(token => token.IsPrimaryMethod ? 0 : 1)
.ThenBy(token => token.Id)
.FirstOrDefault()
?.VaultCustomerId;
}
#endregion
#region Common
///
/// Get calculated SHA256 hash for the input string
///
/// Input string for the hash
/// SHA256 hash
private static string GetSha256Hash(string stringToHash)
{
return SHA256.HashData(Encoding.Default.GetBytes(stringToHash)).Aggregate(string.Empty, (current, next) => $"{current}{next:x2}");
}
#endregion
#endregion
#region Methods
///
/// Convert object properties to a dictionary
///
/// Object to convert
/// Dictionary of properties names and values
public static Dictionary ObjectToDictionary(object data)
{
return new Dictionary(data.GetType().GetProperties().Select(property =>
{
var key = property
?.GetCustomAttributes(typeof(JsonPropertyAttribute), false).OfType().FirstOrDefault()
?.PropertyName ?? string.Empty;
var value = property.GetValue(data) is bool boolValue
? boolValue.ToString().ToLower()
: property.GetValue(data)?.ToString();
return new KeyValuePair(key, value);
}).Where(pair => !string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value)));
}
#region Configuration
///
/// Check whether the plugin is configured
///
/// Plugin settings
/// Result
public static bool IsConfigured(PayPalCommerceSettings settings)
{
//client id and secret are required to request remote services
return !string.IsNullOrEmpty(settings?.ClientId) && !string.IsNullOrEmpty(settings.SecretKey);
}
///
/// Check whether the plugin is configured and connected
///
/// Plugin settings
/// Result
public static bool IsConnected(PayPalCommerceSettings settings)
{
//webhook is required to accept notifications
return IsConfigured(settings) && !string.IsNullOrEmpty(settings.WebhookUrl);
}
///
/// Check whether the plugin is configured, connected and active
///
/// Plugin settings
///
/// A task that represents the asynchronous operation
/// The task result contains the check result; plugin instance
///
public async Task<(bool Active, IPaymentMethod paymentMethod)> IsActiveAsync(PayPalCommerceSettings settings)
{
if (!IsConnected(settings))
return (false, null);
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
var plugin = await _paymentPluginManager.LoadPluginBySystemNameAsync(PayPalCommerceDefaults.SystemName, customer, store.Id);
if (!_paymentPluginManager.IsPluginActive(plugin))
return (false, plugin);
return (true, plugin);
}
///
/// Get access token
///
/// Plugin settings
///
/// A task that represents the asynchronous operation
/// The task result contains the access token; error message if exists
///
public async Task<(AccessToken AccessToken, string Error)> GetAccessTokenAsync(PayPalCommerceSettings settings)
{
return await HandleFunctionAsync(async () =>
{
return await _httpClient.RequestAsync(new()
{
ClientId = settings.ClientId,
Secret = settings.SecretKey,
GrantType = "client_credentials"
}, settings);
});
}
#endregion
#region Components
///
/// Prepare details to render payment buttons/messages
///
/// Plugin settings
/// Button placement
/// Product id
///
/// A task that represents the asynchronous operation
/// The task result contains the script details, customer details, messages details; cart details; error message if exists
///
public async Task<(((string ScriptUrl, string ClientToken, string UserToken),
(string Email, string Name),
(string MessageConfig, string Amount),
(bool? IsRecurring, bool IsShippable)),
string Error)>
PreparePaymentDetailsAsync(PayPalCommerceSettings settings, ButtonPlacement placement, int? productId)
{
return await HandleFunctionAsync(async () =>
{
//get the primary store currency
var currencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode;
if (string.IsNullOrEmpty(currencyCode))
throw new NopException("Primary store currency not set");
//customer details
var customer = await _workContext.GetCurrentCustomerAsync();
var isGuest = await _customerService.IsGuestAsync(customer);
var address = await _customerService.GetCustomerBillingAddressAsync(customer);
var email = address is not null ? address.Email : customer.Email;
var fullName = address is not null
? await _customerService.GetCustomerFullNameAsync(new() { FirstName = address.FirstName, LastName = address.LastName })
: await _customerService.GetCustomerFullNameAsync(customer);
//prepare script components
var components = new List() { "buttons", "funding-eligibility" };
if (placement == ButtonPlacement.PaymentMethod && settings.UseCardFields)
components.Add("card-fields");
if (placement == ButtonPlacement.PaymentMethod && settings.UseAlternativePayments)
components.Add("payment-fields");
if (settings.UseApplePay && placement != ButtonPlacement.Product)
components.Add("applepay");
if (settings.UseGooglePay)
components.Add("googlepay");
if (settings.UseSandbox || settings.ConfiguratorSupported)
components.Add("messages");
var script = new Script
{
ClientId = settings.ClientId,
Currency = currencyCode.ToUpper(),
Intent = settings.PaymentType.ToString().ToLower(),
Commit = placement == ButtonPlacement.PaymentMethod && settings.SkipOrderConfirmPage,
Components = string.Join(',', components),
EnableFunding = settings.EnabledFunding,
DisableFunding = settings.DisabledFunding,
Vault = settings.UseVault && !isGuest,
Debug = false,
//BuyerCountry = null, //PayPal auto detects this
//Locale = null, //PayPal auto detects this
//IntegrationDate = null //defaults to the date when client ID was created
};
var scriptUrl = QueryHelpers.AddQueryString(PayPalCommerceDefaults.ServiceScriptUrl, ObjectToDictionary(script));
//client token is required for Advanced Credit and Debit Card Payments
string clientToken = null;
if (placement == ButtonPlacement.PaymentMethod && settings.UseCardFields)
{
var identityToken = await _httpClient.RequestAsync(new()
{
CustomerId = CommonHelper.EnsureMaximumLength(GetSha256Hash(customer.CustomerGuid.ToString()), 22)
}, settings);
clientToken = identityToken?.ClientToken;
}
//user ID token is required for Vault feature
string userToken = null;
if (settings.UseVault && !isGuest)
{
var vaultCustomerId = await GetVaultCustomerIdAsync(settings, customer.Id);
var accessToken = await _httpClient.RequestAsync(new()
{
ClientId = settings.ClientId,
Secret = settings.SecretKey,
GrantType = "client_credentials",
ResponseType = "id_token",
TargetCustomerId = vaultCustomerId
}, settings);
userToken = accessToken?.UserIdToken;
}
//Pay Later details
var amount = await PrepareMessagesAmountAsync(placement, customer, currencyCode, productId);
var payLaterConfig = new
{
cart = new MessageConfiguration(),
product = new MessageConfiguration(),
checkout = new MessageConfiguration()
};
payLaterConfig = JsonConvert.DeserializeAnonymousType(settings.PayLaterConfig ?? string.Empty, payLaterConfig);
var config = placement switch
{
ButtonPlacement.Cart => payLaterConfig?.cart,
ButtonPlacement.Product => payLaterConfig?.product,
ButtonPlacement.PaymentMethod => payLaterConfig?.checkout,
_ => null
};
var messageConfig = !string.IsNullOrEmpty(config?.Status) ? JsonConvert.SerializeObject(config, Formatting.Indented) : "{}";
//cart details
var (isRecurring, _) = await CheckShoppingCartIsRecurringAsync(placement, productId);
var (isShippable, _) = await CheckShippingIsRequiredAsync(productId);
return ((scriptUrl, clientToken, userToken), (email, fullName), (messageConfig, amount), (isRecurring, isShippable));
});
}
///
/// Prepare details to render Pay Later messages
///
/// Plugin settings
/// Button placement
///
/// A task that represents the asynchronous operation
/// The task result contains the message configuration, amount value, currency code; error message if exists
///
public async Task<((string Config, string Amount, string CurrencyCode), string Error)>
PrepareMessagesAsync(PayPalCommerceSettings settings, ButtonPlacement placement)
{
return await HandleFunctionAsync(async () =>
{
var currencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode;
if (string.IsNullOrEmpty(currencyCode))
throw new NopException("Primary store currency not set");
var customer = await _workContext.GetCurrentCustomerAsync();
var amount = await PrepareMessagesAmountAsync(placement, customer, currencyCode, null);
var payLaterConfig = new
{
cart = new MessageConfiguration(),
product = new MessageConfiguration(),
checkout = new MessageConfiguration()
};
payLaterConfig = JsonConvert.DeserializeAnonymousType(settings.PayLaterConfig ?? string.Empty, payLaterConfig);
var config = placement switch
{
ButtonPlacement.Cart => payLaterConfig?.cart,
ButtonPlacement.Product => payLaterConfig?.product,
ButtonPlacement.PaymentMethod => payLaterConfig?.checkout,
_ => null
};
var messageConfig = !string.IsNullOrEmpty(config?.Status) ? JsonConvert.SerializeObject(config, Formatting.Indented) : "{}";
return (messageConfig, amount, currencyCode);
});
}
#endregion
#region Checkout
///
/// Check whether the checkout is enabled for the customer
///
///
/// A task that represents the asynchronous operation
/// The task result contains the check results; shopping cart
///
public async Task<(bool IsEnabled, bool LoginIsRequired, IList Cart)> CheckoutIsEnabledAsync()
{
return (await HandleFunctionAsync(async () =>
{
if (_orderSettings.CheckoutDisabled)
return (false, false, null);
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
var cart = await _shoppingCartService.GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, store.Id);
if (!cart.Any())
return (false, false, null);
if (await _customerService.IsGuestAsync(customer))
{
if (!_orderSettings.AnonymousCheckoutAllowed)
return (true, true, cart);
var downloadableProductsRequireRegistration = _customerSettings.RequireRegistrationForDownloadableProducts &&
await _productService.HasAnyDownloadableProductAsync(cart.Select(item => item.ProductId).ToArray());
if (downloadableProductsRequireRegistration)
return (true, true, cart);
}
return (true, false, cart);
}, false)).Result;
}
///
/// Check whether the shopping cart is valid
///
///
/// A task that represents the asynchronous operation
/// The task result contains the validation warnings; error message if exists
///
public async Task<(IList Warnings, string Error)> ValidateShoppingCartAsync()
{
return await HandleFunctionAsync(async () =>
{
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
var cart = await _shoppingCartService.GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, store.Id);
if (!cart.Any())
throw new NopException("Shopping cart is empty");
await _customerService.ResetCheckoutDataAsync(customer, store.Id, clearShippingMethod: false);
var checkoutAttributesXml = await _genericAttributeService
.GetAttributeAsync(customer, NopCustomerDefaults.CheckoutAttributes, store.Id);
var cartWarnings = await _shoppingCartService.GetShoppingCartWarningsAsync(cart, checkoutAttributesXml, true);
if (cartWarnings.Any())
return cartWarnings;
foreach (var item in cart)
{
var product = await _productService.GetProductByIdAsync(item.ProductId);
var itemWarnings = await _shoppingCartService
.GetShoppingCartItemWarningsAsync(customer, item.ShoppingCartType, product, item.StoreId, item.AttributesXml,
item.CustomerEnteredPrice, item.RentalStartDateUtc, item.RentalEndDateUtc, item.Quantity, false, item.Id);
if (itemWarnings.Any())
return itemWarnings;
}
return null;
});
}
///
/// Check whether the shipping is required for the current cart/product
///
/// Product id
///
/// A task that represents the asynchronous operation
/// The task result contains the check result; error message if exists
///
public async Task<(bool ShippingIsRequired, string Error)> CheckShippingIsRequiredAsync(int? productId)
{
return await HandleFunctionAsync(async () =>
{
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
var cart = await _shoppingCartService.GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, store.Id);
var shippingIsRequired = await _shoppingCartService.ShoppingCartRequiresShippingAsync(cart);
if (!shippingIsRequired && await _productService.GetProductByIdAsync(productId ?? 0) is Product product)
shippingIsRequired = product.IsShipEnabled;
return shippingIsRequired;
}, false);
}
///
/// Check whether the current cart/product is recurring
///
/// Button placement
/// Product id
///
/// A task that represents the asynchronous operation
/// The task result contains the check result; error message if exists
///
public async Task<(bool? IsRecurring, string Error)> CheckShoppingCartIsRecurringAsync(ButtonPlacement placement, int? productId = null)
{
return await HandleFunctionAsync(async () =>
{
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
var cart = await _shoppingCartService.GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, store.Id);
var isRecurring = await _shoppingCartService.ShoppingCartIsRecurringAsync(cart);
if (!isRecurring && await _productService.GetProductByIdAsync(productId ?? 0) is Product product)
isRecurring = product.IsRecurring;
//we cannot start checkout process from the product or cart page (no way to get shipping details) for recurring items
if (isRecurring && (placement == ButtonPlacement.Cart || placement == ButtonPlacement.Product))
return (bool?)null;
return isRecurring;
}, false);
}
#endregion
#region Orders
///
/// Get the order by id
///
/// Plugin settings
/// Order id
///
/// A task that represents the asynchronous operation
/// The task result contains the order; error message if exists
///
public async Task<(Order Order, string Error)> GetOrderAsync(PayPalCommerceSettings settings, string orderId)
{
return await HandleFunctionAsync(async () =>
{
if (!IsConfigured(settings))
throw new NopException("Plugin not configured");
var paymentRequest = await _orderProcessingService.GetProcessPaymentRequestAsync()
?? throw new NopException("Order payment info not found");
var orderIdKey = await _localizationService.GetResourceAsync("Plugins.Payments.PayPalCommerce.Order.Id");
if (!paymentRequest.CustomValues.TryGetValue(orderIdKey, out var orderIdValue) ||
!string.Equals(orderIdValue.Value, orderId, StringComparison.InvariantCultureIgnoreCase))
{
throw new NopException("Failed to get PayPal order info");
}
var placementKey = await _localizationService.GetResourceAsync("Plugins.Payments.PayPalCommerce.Order.Placement");
if (!paymentRequest.CustomValues.TryGetValue(placementKey, out var placementValue) ||
!Enum.TryParse(placementValue.Value, out var placement))
{
throw new NopException("Failed to get PayPal order info");
}
var order = await _httpClient.RequestAsync(new GetOrderRequest { OrderId = orderId }, settings);
return order;
});
}
///
/// Get a previously created order if exists
///
/// Plugin settings
/// Payment request
/// Button placement
/// Whether the shipping is required (used for validation)
/// Payment source
///
/// A task that represents the asynchronous operation
/// The task result contains the created order; error message if exists
///
public async Task<(Order Order, string Error)> GetCreatedOrderAsync(PayPalCommerceSettings settings,
ProcessPaymentRequest paymentRequest, ButtonPlacement placement, bool shippingIsRequired, string paymentSource)
{
return await HandleFunctionAsync(async () =>
{
if (paymentRequest is null)
return null;
var orderIdKey = await _localizationService.GetResourceAsync("Plugins.Payments.PayPalCommerce.Order.Id");
if (!paymentRequest.CustomValues.TryGetValue(orderIdKey, out var orderIdValue) || string.IsNullOrEmpty(orderIdValue.Value))
return null;
var placementKey = await _localizationService.GetResourceAsync("Plugins.Payments.PayPalCommerce.Order.Placement");
if (!paymentRequest.CustomValues.TryGetValue(placementKey, out var placementValue) ||
!Enum.TryParse(placementValue.Value, out var previousPlacement) ||
previousPlacement != placement)
{
return null;
}
var order = await _httpClient
.RequestAsync(new GetOrderRequest { OrderId = orderIdValue.Value }, settings);
//we cannot use completed order
if (order.Status?.ToUpper() != OrderStatusType.CREATED.ToString() &&
order.Status?.ToUpper() != OrderStatusType.PAYER_ACTION_REQUIRED.ToString() &&
order.Status?.ToUpper() != OrderStatusType.APPROVED.ToString())
{
return null;
}
//check validity interval
if (!DateTime.TryParse(order.CreateTime, out var createTime) ||
(DateTime.UtcNow - createTime.ToUniversalTime()).TotalSeconds > settings.OrderValidityInterval)
{
return null;
}
if (order.PurchaseUnits.FirstOrDefault() is not PurchaseUnit unit || (unit.Shipping is null != !shippingIsRequired))
return null;
//payment sources must match
if (string.Equals(paymentSource, nameof(PaymentSource.PayPal), StringComparison.InvariantCultureIgnoreCase) &&
order.PaymentSource?.PayPal is null)
{
return null;
}
if (string.Equals(paymentSource, nameof(PaymentSource.Card), StringComparison.InvariantCultureIgnoreCase) &&
order.PaymentSource?.Card is null)
{
return null;
}
return order;
}, false);
}
///
/// Create an order
///
/// Plugin settings
/// Button placement
/// Payment source
/// Saved card id
/// Whether to save card payment token
///
/// A task that represents the asynchronous operation
/// The task result contains the created order; error message if exists
///
public async Task<(Order Order, string Error)> CreateOrderAsync(PayPalCommerceSettings settings,
ButtonPlacement placement, string paymentSource, int? cardId, bool saveCard)
{
return await HandleFunctionAsync(async () =>
{
if (!IsConfigured(settings))
throw new NopException("Plugin not configured");
if (string.IsNullOrEmpty(settings.MerchantId))
throw new NopException("Merchant PayPal ID not set");
var details = await PrepareCartDetailsAsync(placement);
var savedPaymentToken = await _tokenService.GetByIdAsync(cardId ?? 0);
if (savedPaymentToken is not null && savedPaymentToken.CustomerId != details.Customer.Id)
throw new NopException("Card details not found");
var isGuest = await _customerService.IsGuestAsync(details.Customer);
var isRecurring = await _shoppingCartService.ShoppingCartIsRecurringAsync(details.Cart);
if (isRecurring)
{
if (!settings.UseVault)
throw new NopException("Vault disabled");
if (isGuest)
throw new NopException("Anonymous checkout disabled for recurring items");
var (error, cycleLength, cyclePeriod, totalCycles) = await _shoppingCartService.GetRecurringCycleInfoAsync(details.Cart);
if (!string.IsNullOrEmpty(error))
throw new NopException(error);
}
var paymentRequest = await _orderProcessingService.GetProcessPaymentRequestAsync();
var (order, _) = await GetCreatedOrderAsync(settings, paymentRequest, placement, details.ShippingIsRequired, paymentSource);
if (paymentRequest is null || order is null)
paymentRequest = new();
//prepare purchase unit
var purchaseUnit = await PreparePurchaseUnitAsync(settings, details, paymentRequest.OrderGuid.ToString());
//whether we should create a new order
if (order is null || isRecurring)
{
var isCard = string.Equals(paymentSource, nameof(PaymentSource.Card), StringComparison.InvariantCultureIgnoreCase);
var isVenmo = string.Equals(paymentSource, nameof(PaymentSource.Venmo), StringComparison.InvariantCultureIgnoreCase);
var isApplepay = string.Equals(paymentSource, nameof(PaymentSource.ApplePay), StringComparison.InvariantCultureIgnoreCase);
if (isRecurring && (isVenmo || isApplepay))
throw new NopException($"Payment source '{paymentSource.ToUpper()}' not supported");
var context = PrepareOrderContext(settings, details, paymentRequest.OrderGuid.ToString(), isApplepay);
var payer = await PrepareBillingDetailsAsync(settings, details);
//only registered customers can save payment tokens
var vault = !settings.UseVault || isGuest ? null : new VaultInstruction
{
UsageType = VaultUsageType.MERCHANT.ToString().ToUpper(),
CustomerType = VaultUsageType.CONSUMER.ToString().ToUpper(),
StoreInVault = VaultInstructionType.ON_SUCCESS.ToString().ToUpper(),
PermitMultiplePaymentTokens = false,
UsagePattern = isRecurring ? UsagePatternType.INSTALLMENT_PREPAID.ToString().ToUpper() : null
};
//set payment source
var paymentSourceDetails = new PaymentSource();
if (isCard)
{
paymentSourceDetails.Card = new()
{
ExperienceContext = context,
BillingAddress = !string.IsNullOrEmpty(savedPaymentToken?.VaultId) ? null : payer.Address,
VaultId = savedPaymentToken?.VaultId,
Attributes = vault is null || !saveCard || !string.IsNullOrEmpty(savedPaymentToken?.VaultId) ? null : new()
{
Vault = vault,
Customer = payer
}
};
if (vault is not null && (saveCard || !string.IsNullOrEmpty(savedPaymentToken?.VaultId)))
{
paymentSourceDetails.Card.StoredCredential = new()
{
PaymentInitiator = PaymentInitiatorType.CUSTOMER.ToString().ToUpper(),
PaymentType = isRecurring
? Api.Models.Enums.PaymentType.RECURRING.ToString().ToUpper()
: Api.Models.Enums.PaymentType.ONE_TIME.ToString().ToUpper(),
Usage = saveCard
? StoredPaymentUsageType.FIRST.ToString().ToUpper()
: StoredPaymentUsageType.SUBSEQUENT.ToString().ToUpper(),
UsagePattern = isRecurring ? UsagePatternType.INSTALLMENT_PREPAID.ToString().ToUpper() : null
};
}
if (placement == ButtonPlacement.PaymentMethod && settings.UseCardFields)
{
paymentSourceDetails.Card.Attributes = new()
{
Vault = vault is not null && saveCard && string.IsNullOrEmpty(savedPaymentToken?.VaultId) ? vault : null,
Customer = vault is not null && saveCard && string.IsNullOrEmpty(savedPaymentToken?.VaultId) ? payer : null,
Verification = new()
{
Method = settings.CustomerAuthenticationRequired
? VerificationInstructionMethodType.SCA_ALWAYS.ToString().ToUpper()
: VerificationInstructionMethodType.SCA_WHEN_REQUIRED.ToString().ToUpper()
}
};
}
}
else if (isVenmo)
{
paymentSourceDetails.Venmo = new()
{
ExperienceContext = context,
EmailAddress = payer.EmailAddress,
Attributes = vault is not null ? new() { Vault = vault, Customer = payer } : null
};
}
else
{
paymentSourceDetails.PayPal = new()
{
ExperienceContext = context,
EmailAddress = payer.EmailAddress,
Name = payer.Name,
BirthDate = payer.BirthDate,
Address = payer.Address,
Attributes = vault is not null ? new() { Vault = vault, Customer = payer } : null
};
}
order = await _httpClient.RequestAsync(new CreateOrderRequest
{
Intent = settings.PaymentType.ToString().ToUpper(),
PaymentSource = paymentSourceDetails,
PurchaseUnits = [purchaseUnit]
}, settings);
}
else
{
//order exists, so just update some details
var patches = PreparePatches(purchaseUnit);
patches.Add(new()
{
Op = PatchOpType.REPLACE.ToString().ToLower(),
Path = "/intent",
Value = settings.PaymentType.ToString().ToUpper()
});
var updateRequest = new UpdateOrderRequest