From 716a6daa288c973c8fa40dc799630a0328eb4521 Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Wed, 22 Apr 2026 12:30:24 +0200 Subject: [PATCH 1/4] VCST-4983: Improve refund flow fix: OuterId returned from provider is discarded fix: Modification failure wrongly rejects the refund fix: Cancel Document & manual status edit bypass the payment method --- .../RefundChangedOrderChangedEventHandler.cs | 12 ++++++++++-- .../Services/PaymentFlowService.cs | 8 ++++++++ .../Scripts/blades/operation-detail.js | 5 +++++ .../Scripts/blades/refund-detail.html | 2 +- .../VirtoCommerce.OrdersModule.Tests.csproj | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs b/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs index 5590d0480..8c1cfff3b 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs @@ -92,16 +92,22 @@ public virtual async Task ProcessRefundChangesAsync(RefundChangedJobArgument[] j { "IsUpdate", "true" }, }; + var previousStatus = refund.Status; try { var result = await payment.PaymentMethod.RefundProcessPaymentAsync(refundRequest); if (result.IsSuccess) { refund.Status = result.NewRefundStatus.ToString(); + refund.RejectReasonMessage = null; } else { - refund.Status = nameof(RefundStatus.Rejected); + // A failed modification must not reject the underlying refund: preserve the prior status + // unless the provider explicitly reports a new one. + refund.Status = result.NewRefundStatus != default + ? result.NewRefundStatus.ToString() + : previousStatus; refund.RejectReasonMessage = result.ErrorMessage; } @@ -169,7 +175,9 @@ protected virtual bool HasRefundFieldChanges(Refund oldRefund, Refund newRefund) return oldRefund.Amount != newRefund.Amount || oldRefund.ReasonCode != newRefund.ReasonCode || oldRefund.ReasonMessage != newRefund.ReasonMessage - || oldRefund.OuterId != newRefund.OuterId; + || oldRefund.OuterId != newRefund.OuterId + || oldRefund.Status != newRefund.Status + || oldRefund.IsCancelled != newRefund.IsCancelled; } } diff --git a/src/VirtoCommerce.OrdersModule.Data/Services/PaymentFlowService.cs b/src/VirtoCommerce.OrdersModule.Data/Services/PaymentFlowService.cs index cff2f7a1d..125cbe054 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Services/PaymentFlowService.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Services/PaymentFlowService.cs @@ -242,6 +242,10 @@ protected virtual async Task SaveResultToRefundDocumen if (refundResult.IsSuccess) { refund.Status = refundResult.NewRefundStatus.ToString(); + if (!string.IsNullOrEmpty(refundResult.OuterId)) + { + refund.OuterId = refundResult.OuterId; + } result.RefundStatus = refund.Status; result.Succeeded = true; } @@ -343,6 +347,10 @@ public virtual async Task SaveResultToCaptureDocument if (captureResult.IsSuccess) { capture.Status = nameof(CaptureStatus.Processed); + if (!string.IsNullOrEmpty(captureResult.OuterId)) + { + capture.OuterId = captureResult.OuterId; + } paymentInfo.Payment.Status = captureResult.NewPaymentStatus.ToString(); result.PaymentStatus = paymentInfo.Payment.Status; result.Succeeded = true; diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-detail.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-detail.js index f45e75332..eb127325b 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-detail.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-detail.js @@ -271,6 +271,11 @@ angular.module('virtoCommerce.orderModule') || blade.currentEntity.cancelledState === 'Completed' || blade.currentEntity.cancelledState === 'Requested'); break; + case 'Refund': + result = !blade.currentEntity.isCancelled + && blade.currentEntity.status !== 'Processed' + && !blade.currentEntity.outerId; + break; default: result = !blade.currentEntity.isCancelled; break; diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/refund-detail.html b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/refund-detail.html index 971618395..dd1efeb17 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/refund-detail.html +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/refund-detail.html @@ -19,7 +19,7 @@ placeholder="'orders.blades.refund-details.placeholders.status' | translate" setting="'Refund.Status'" ng-model="blade.currentEntity.status" - disabled="blade.isLocked"> + disabled="blade.isLocked || blade.currentEntity.outerId || blade.currentEntity.status === 'Processed'">
diff --git a/tests/VirtoCommerce.OrdersModule.Tests/VirtoCommerce.OrdersModule.Tests.csproj b/tests/VirtoCommerce.OrdersModule.Tests/VirtoCommerce.OrdersModule.Tests.csproj index 18ee62bed..82a5904f6 100644 --- a/tests/VirtoCommerce.OrdersModule.Tests/VirtoCommerce.OrdersModule.Tests.csproj +++ b/tests/VirtoCommerce.OrdersModule.Tests/VirtoCommerce.OrdersModule.Tests.csproj @@ -5,7 +5,7 @@ false - + runtime; build; native; contentfiles; analyzers; buildtransitive all From adf915a830e7142f903113116413c5887e8a60ff Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Wed, 22 Apr 2026 15:04:46 +0200 Subject: [PATCH 2/4] fix code review --- .../RefundChangedOrderChangedEventHandler.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs b/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs index 8c1cfff3b..a548c74ef 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs @@ -103,11 +103,10 @@ public virtual async Task ProcessRefundChangesAsync(RefundChangedJobArgument[] j } else { - // A failed modification must not reject the underlying refund: preserve the prior status - // unless the provider explicitly reports a new one. - refund.Status = result.NewRefundStatus != default - ? result.NewRefundStatus.ToString() - : previousStatus; + // A failed modification must not invalidate the underlying refund state. + // NewRefundStatus is non-nullable and defaults to Pending, so we cannot distinguish + // "provider didn't set it" from "provider explicitly reports Pending" -- preserve prior state. + refund.Status = previousStatus; refund.RejectReasonMessage = result.ErrorMessage; } @@ -172,11 +171,13 @@ protected virtual RefundChangedJobArgument[] GetJobArgumentsForChangedEntry(Gene protected virtual bool HasRefundFieldChanges(Refund oldRefund, Refund newRefund) { + // Status is intentionally NOT tracked here: the handler itself writes refund.Status from the + // provider response, and tracking it would re-trigger the handler from its own SaveChangesAsync, + // causing duplicate provider calls. Manual status edits on submitted refunds are blocked at the UI. return oldRefund.Amount != newRefund.Amount || oldRefund.ReasonCode != newRefund.ReasonCode || oldRefund.ReasonMessage != newRefund.ReasonMessage || oldRefund.OuterId != newRefund.OuterId - || oldRefund.Status != newRefund.Status || oldRefund.IsCancelled != newRefund.IsCancelled; } } From adebe6cd9413181b818fa9eb05417a6f3af8ac14 Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Wed, 22 Apr 2026 15:18:16 +0200 Subject: [PATCH 3/4] fix code review --- .../Handlers/RefundChangedOrderChangedEventHandler.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs b/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs index a548c74ef..390e3dd13 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs @@ -171,13 +171,13 @@ protected virtual RefundChangedJobArgument[] GetJobArgumentsForChangedEntry(Gene protected virtual bool HasRefundFieldChanges(Refund oldRefund, Refund newRefund) { - // Status is intentionally NOT tracked here: the handler itself writes refund.Status from the - // provider response, and tracking it would re-trigger the handler from its own SaveChangesAsync, - // causing duplicate provider calls. Manual status edits on submitted refunds are blocked at the UI. + // Status and OuterId are intentionally NOT tracked here: both are written by the provider response + // (via PaymentFlowService.SaveResultToRefundDocument or this handler), so tracking them would + // re-trigger the handler from its own SaveChangesAsync and cause duplicate provider calls. + // Manual status edits on submitted refunds are blocked at the UI. return oldRefund.Amount != newRefund.Amount || oldRefund.ReasonCode != newRefund.ReasonCode || oldRefund.ReasonMessage != newRefund.ReasonMessage - || oldRefund.OuterId != newRefund.OuterId || oldRefund.IsCancelled != newRefund.IsCancelled; } } From f34553cbb9633d55c2f0d49f54e8696334b53c0c Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Wed, 22 Apr 2026 16:01:31 +0200 Subject: [PATCH 4/4] Fix code review --- .../Handlers/RefundChangedOrderChangedEventHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs b/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs index 390e3dd13..c7a0020a9 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Handlers/RefundChangedOrderChangedEventHandler.cs @@ -99,6 +99,10 @@ public virtual async Task ProcessRefundChangesAsync(RefundChangedJobArgument[] j if (result.IsSuccess) { refund.Status = result.NewRefundStatus.ToString(); + if (!string.IsNullOrEmpty(result.OuterId)) + { + refund.OuterId = result.OuterId; + } refund.RejectReasonMessage = null; } else