{"record":{"id":"fe8040b17c167e20","repo":"nopSolutions/nopCommerce","slug":"the-refund-was-cancelled","errorCode":null,"errorMessage":"The refund was cancelled","messagePattern":"The refund was cancelled","errorType":"exception","errorClass":"NopException","httpStatus":null,"severity":"warning","filePath":"src/Plugins/Nop.Plugin.Payments.PayPalCommerce/Services/PayPalCommerceServiceManager.cs","lineNumber":2576,"sourceCode":"            if (!IsConfigured(settings))\n                throw new NopException(\"Plugin not configured\");\n\n            var currencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode;\n            if (string.IsNullOrEmpty(currencyCode))\n                throw new NopException(\"Primary store currency not set\");\n\n            if (string.IsNullOrEmpty(nopOrder.CaptureTransactionId))\n                throw new NopException(\"Capture ID not set\");\n\n            var request = new CreateRefundRequest\n            {\n                CaptureId = nopOrder.CaptureTransactionId,\n                Amount = amount.HasValue ? PrepareMoney(amount.Value, currencyCode) : null\n            };\n            var refund = await _httpClient.RequestAsync<CreateRefundRequest, CreateRefundResponse>(request, settings);\n\n            if (refund.Status?.ToUpper() == RefundStatusType.CANCELLED.ToString())\n                throw new NopException(\"The refund was cancelled\");\n\n            if (refund.Status?.ToUpper() == RefundStatusType.FAILED.ToString())\n                throw new NopException(\"The refund could not be processed\");\n\n            if (refund.Status?.ToUpper() == RefundStatusType.PENDING.ToString())\n                throw new NopException($\"Capture is in {refund.Status} status due to {refund.StatusDetails?.Reason}\");\n\n            //save id to avoid double refund\n            var refundIds = await _genericAttributeService\n                .GetAttributeAsync<List<string>>(nopOrder, PayPalCommerceDefaults.RefundIdAttributeName)\n                ?? new();\n            if (!refundIds.Contains(refund.Id))\n                refundIds.Add(refund.Id);\n            await _genericAttributeService.SaveAttributeAsync(nopOrder, PayPalCommerceDefaults.RefundIdAttributeName, refundIds);\n\n            return refund;\n        });\n    }","sourceCodeStart":2558,"sourceCodeEnd":2594,"githubUrl":"https://github.com/nopSolutions/nopCommerce/blob/64bdf2ff08c8b39e65717bcf974fb43dc2ef68f2/src/Plugins/Nop.Plugin.Payments.PayPalCommerce/Services/PayPalCommerceServiceManager.cs#L2558-L2594","documentation":"Thrown by RefundAsync after a successful PayPal refund API call when the returned refund.Status equals CANCELLED. This is PayPal's own status indicating the refund was cancelled (e.g. due to a dispute reversal or PayPal action), not a local config problem. It surfaces through HandleFunctionAsync as the Error string, so the refund id is not persisted.","triggerScenarios":"PayPal returns a refund resource with status CANCELLED; the refund is initiated but then cancelled by PayPal or through a dispute process before completing.","commonSituations":"Buyer dispute causes PayPal to cancel the refund; PayPal manually reverses a refund; rare intermediary status on bank-funded refunds.","solutions":["Treat the (null refund, error) result as 'not refunded' and inform the merchant the refund was cancelled.","Check the PayPal dashboard / dispute state for the capture to understand why it was cancelled.","Retry the refund only after confirming the underlying cancellation/dispute is resolved."],"exampleFix":"// before\nvar (refund, err) = await _serviceManager.RefundAsync(settings, order, amount);\n\n// after\nvar (refund, err) = await _serviceManager.RefundAsync(settings, order, amount);\nif (!string.IsNullOrEmpty(err))\n{\n    _logger.Warning($\"Refund for order {order.Id} did not complete: {err}\");\n    // surface to admin; do NOT mark the order as refunded\n    return;\n}\nif (refund.Status?.ToUpper() == RefundStatusType.CANCELLED.ToString())\n    NotifyAdmin(\"PayPal cancelled the refund; review the dispute/capture\");","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"var (refund, error) = await mgr.RefundAsync(settings, order, amount);\nif (!string.IsNullOrEmpty(error))\n{\n    _logger.Warning($\"Refund not completed for order {order.Id}: {error}\");\n    // leave order un-refunded; let staff review the dispute/capture\n    return;\n}","preventionTips":["Never auto-mark an order refunded when the Error string is non-empty.","Monitor PayPal dispute events that can cancel refunds.","Provide staff a way to inspect the capture/dispute state before retrying."],"tags":["payments","paypal-commerce","refund","status","dispute"],"backgroundTag":null,"analyzedSha":"64bdf2ff08c8b39e65717bcf974fb43dc2ef68f2","analyzedAt":"2026-08-13T21:19:38.062Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}