Confirmed Stripe: Partial refunds still set PAYMENT_REVERSED state

DragonByte Tech

Well-known member
Licensed customer
Affected version
2.3.13
If you issue a partial refund in Stripe, this is not checked and is instead treated as a full refund. It is crucial that partial refunds do not count as a full refund, because partial refunds are granted if the user was accidentally charged too much.

This is the relevant data as per the payment provider log:
Code:
["amount_captured"] => int(5942)
["amount_refunded"] => int(1048)
["eventType"] => string(15) "charge.refunded"

A payment of €59.42 was refunded €10.48.

Suggested fix:
Diff:
--- a/src/XF/Payment/AbstractProvider.php
+++ b/src/XF/Payment/AbstractProvider.php
@@ -232,8 +232,8 @@
                 break;
 
             default:
-                $state->logType = 'info';
-                $state->logMessage = 'OK, no action';
+                $state->logType = $state->logType ?: 'info';
+                $state->logMessage = $state->logMessage ?: 'OK, no action';
                 break;
         }
 
--- a/src/XF/Payment/Stripe.php
+++ b/src/XF/Payment/Stripe.php
@@ -1209,6 +1209,25 @@
                 break;
 
             case 'charge.refunded':
+                $amountCaptured = $state->event['amount_captured'] ?? $state->event['amount'];
+                $amountRefunded = $state->event['amount_refunded'] ?? $amountCaptured;
+
+                if ($amountRefunded < $amountCaptured)
+                {
+                    // a partial refund leaves the purchase in place; a later refund of the remainder reverses it
+                    $state->logType = 'info';
+                    $state->logMessage = sprintf(
+                        'Partial refund (%d of %d %s refunded), purchase not reversed.',
+                        $amountRefunded,
+                        $amountCaptured,
+                        strtoupper($state->event['currency'])
+                    );
+                    break;
+                }
+
+                $state->paymentResult = CallbackState::PAYMENT_REVERSED;
+                break;
+
             case 'charge.dispute.funds_withdrawn':
                 $state->paymentResult = CallbackState::PAYMENT_REVERSED;
                 break;
 
Back
Top Bottom