nopSolutions/nopCommerce · error · NopException

Payment token not found

Error message

Payment token not found

What it means

Thrown inside ProcessNextRecurringPaymentAsync when the PayPal vault token referenced by the initial order's generic attribute either does not exist or does not belong to the customer initiating the recurring payment. The token ID is retrieved from the InitialOrder via generic attributes, then looked up through ITokenService. This protects against charging the wrong customer or using a stale/deleted tokenization reference.

Source

Thrown at src/Plugins/Nop.Plugin.Payments.PayPalCommerce/Services/PayPalCommerceServiceManager.cs:2826

    /// </summary>
    /// <param name="settings">Plugin settings</param>
    /// <param name="paymentRequest">Payment request</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the created order; error message if exists
    /// </returns>
    public async Task<(Order Order, string Error)> ProcessNextRecurringPaymentAsync(PayPalCommerceSettings settings,
        ProcessPaymentRequest paymentRequest)
    {
        return await HandleFunctionAsync(async () =>
        {
            if (!IsConfigured(settings))
                throw new NopException("Plugin not configured");

            var tokenId = await _genericAttributeService
                .GetAttributeAsync<int>(paymentRequest.InitialOrder, PayPalCommerceDefaults.TokenIdAttributeName);
            if (await _tokenService.GetByIdAsync(tokenId) is not PayPalToken token || token.CustomerId != paymentRequest.CustomerId)
                throw new NopException("Payment token not found");

            var currencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode;
            if (string.IsNullOrEmpty(currencyCode))
                throw new NopException("Primary store currency not set");

            //prepare purchase unit
            var store = await _storeService.GetStoreByIdAsync(paymentRequest.StoreId);
            var orderGuid = paymentRequest.OrderGuid.ToString();
            var money = PrepareMoney(paymentRequest.OrderTotal, currencyCode);
            var purchaseUnit = new PurchaseUnit
            {
                CustomId = CommonHelper.EnsureMaximumLength(orderGuid, 127),
                InvoiceId = CommonHelper.EnsureMaximumLength(orderGuid, 127),
                Description = CommonHelper.EnsureMaximumLength($"Purchase at '{store.Name}'", 127),
                SoftDescriptor = CommonHelper.EnsureMaximumLength(store.Name, 22),
                Payee = new() { MerchantId = settings.MerchantId },
                Amount = new() { Value = money.Value, CurrencyCode = money.CurrencyCode }
            };

View on GitHub (pinned to 64bdf2ff08)

Solutions

  1. Verify the token still exists: navigate to Admin > Configuration > PayPal Commerce > Tokens and check the token for that customer
  2. Inspect the InitialOrder's TokenIdAttributeName generic attribute to confirm a valid token ID was stored during the original checkout
  3. If the token is gone, cancel the recurring payment schedule and have the customer complete a new tokenized checkout to vault a fresh token
  4. Ensure the recurring payment job passes the correct CustomerId matching the original tokenization

Example fix

// before
var (order, error) = await _serviceManager.ProcessNextRecurringPaymentAsync(settings, request);
if (!string.IsNullOrEmpty(error))
    _logger.Error(error);

// after — pre-validate token before invoking recurring payment
var tokenId = await _genericAttributeService
    .GetAttributeAsync<int>(request.InitialOrder, PayPalCommerceDefaults.TokenIdAttributeName);
var token = await _tokenService.GetByIdAsync(tokenId);
if (token is null || token.CustomerId != request.CustomerId)
{
    await CancelRecurringPaymentAsync(request);
    NotifyCustomerReTokenizationRequired(request.CustomerId);
    return;
}
var (order, error) = await _serviceManager.ProcessNextRecurringPaymentAsync(settings, request);
Defensive patterns

Strategy: validation

Validate before calling

// Verify token exists and belongs to customer before calling ProcessNextRecurringPaymentAsync
var tokenId = await _genericAttributeService
    .GetAttributeAsync<int>(initialOrder, PayPalCommerceDefaults.TokenIdAttributeName);
var token = await _tokenService.GetByIdAsync(tokenId);
if (token is null || token.CustomerId != customer.Id)
{
    // Do NOT call ProcessNextRecurringPaymentAsync — cancel and re-tokenize
    return;
}

Type guard

// C# pattern-based guard
var token = await _tokenService.GetByIdAsync(tokenId);
if (token is not PayPalToken { CustomerId: var cid } || cid != customerId)
    return;
// token is guaranteed non-null and owned by the customer

Try / catch

// HandleFunctionAsync catches internally; check the returned error tuple
var (order, error) = await _serviceManager
    .ProcessNextRecurringPaymentAsync(settings, paymentRequest);
if (!string.IsNullOrEmpty(error))
{
    _logger.Error($"Recurring payment failed: {error}");
    await HandleRecurringPaymentFailureAsync(paymentRequest);
}

Prevention

When it happens

Trigger: ProcessNextRecurringPaymentAsync is called; the TokenIdAttributeName generic attribute on paymentRequest.InitialOrder resolves to a tokenId that either yields no PayPalToken from GetByIdAsync, or the returned token's CustomerId does not match paymentRequest.CustomerId.

Common situations: The vaulted token was manually deleted from the PayPal Commerce token list after the original checkout; the initial order never had a token saved because tokenization failed silently; customer record was merged or the CustomerId changed; a scheduled recurring payment job fires for an order whose initial purchase used a guest checkout or a different PayPal account.

Related errors


AI-assisted analysis of nopSolutions/nopCommerce@64bdf2ff08 (2026-08-13). Data as JSON: /api/errors/d65cf28a7684f1af. Report an issue: GitHub.