Skip to content

Commit 26dc0f7

Browse files
authored
Merge branch 'develop' into fix/ece-wc-blocks-amounts
2 parents 49e890f + 3058f59 commit 26dc0f7

File tree

5 files changed

+99
-7
lines changed

5 files changed

+99
-7
lines changed

changelog/fix-refund-on-cancel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fix
3+
4+
Refunds and fees should not be tracked for canceled authorizations

includes/class-wc-payments-order-service.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,10 @@ private function mark_payment_capture_cancelled( $order, $intent_data ) {
11421142
$this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::REVIEW_BLOCKED );
11431143
}
11441144

1145+
// Remove transaction fee since the authorization was canceled and no payment was processed.
1146+
$order->delete_meta_data( self::WCPAY_TRANSACTION_FEE_META_KEY );
1147+
$order->delete_meta_data( '_wcpay_net' );
1148+
11451149
$this->update_order_status( $order, Order_Status::CANCELLED );
11461150
$this->complete_order_processing( $order, $intent_data['intent_status'] );
11471151
}
@@ -1318,7 +1322,9 @@ private function mark_order_held_for_review_for_fraud( $order, $intent_data ) {
13181322
*/
13191323
public function attach_transaction_fee_to_order( $order, $charge ) {
13201324
try {
1321-
if ( $charge && null !== $charge->get_application_fee_amount() ) {
1325+
// Only set transaction fee if the charge was actually captured.
1326+
// Canceled authorizations should not have fees since no payment was processed.
1327+
if ( $charge && null !== $charge->get_application_fee_amount() && $charge->is_captured() ) {
13221328
$order->update_meta_data(
13231329
self::WCPAY_TRANSACTION_FEE_META_KEY,
13241330
WC_Payments_Utils::interpret_stripe_amount( $charge->get_application_fee_amount(), $charge->get_currency() )
@@ -1361,6 +1367,10 @@ public function cancel_authorizations_on_order_status_change( $order_id ) {
13611367
$intent = $request->send();
13621368

13631369
$this->post_unique_capture_cancelled_note( $order, $intent_id, $charge->get_id() );
1370+
1371+
// Remove transaction fee since the authorization was canceled and no payment was processed.
1372+
$order->delete_meta_data( self::WCPAY_TRANSACTION_FEE_META_KEY );
1373+
$order->delete_meta_data( '_wcpay_net' );
13641374
}
13651375

13661376
$this->set_intention_status_for_order( $order, $intent->get_status() );

includes/class-wc-payments-webhook-processing-service.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,14 @@ private function process_webhook_refund_triggered_externally( array $event_body
857857
return;
858858
}
859859

860+
// Check if the charge was actually captured before processing the refund.
861+
// Stripe sends charge.refunded webhooks for cancelled authorizations even though no payment was captured.
862+
// We should not create WooCommerce refund objects for these cases as they cause negative values in analytics.
863+
$captured = $event_object['captured'] ?? false;
864+
if ( ! $captured ) {
865+
return;
866+
}
867+
860868
// Fetch the details of the refund so that we can find the associated order and write a note.
861869
$charge_id = $this->read_webhook_property( $event_object, 'id' );
862870
$refund = $this->read_webhook_property( $event_object, 'refunds' )['data'][0]; // Most recent refund.

tests/unit/test-class-wc-payments-order-service.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,20 +1363,26 @@ public function test_get_order_throws_exception() {
13631363
}
13641364

13651365
public function test_attach_transaction_fee_to_order() {
1366-
$order = WC_Helper_Order::create_order();
1367-
$this->order_service->attach_transaction_fee_to_order( $order, new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, 113, [], [], 'usd' ) );
1366+
$order = WC_Helper_Order::create_order();
1367+
$charge = new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, 113, [], [], 'usd' );
1368+
$charge->set_captured( true );
1369+
$this->order_service->attach_transaction_fee_to_order( $order, $charge );
13681370
$this->assertEquals( 1.13, $order->get_meta( '_wcpay_transaction_fee', true ) );
13691371
}
13701372

13711373
public function test_attach_transaction_fee_to_order_zero_fee() {
1372-
$order = WC_Helper_Order::create_order();
1373-
$this->order_service->attach_transaction_fee_to_order( $order, new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, 0, [], [], 'eur' ) );
1374+
$order = WC_Helper_Order::create_order();
1375+
$charge = new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, 0, [], [], 'eur' );
1376+
$charge->set_captured( true );
1377+
$this->order_service->attach_transaction_fee_to_order( $order, $charge );
13741378
$this->assertEquals( 0, $order->get_meta( '_wcpay_transaction_fee', true ) );
13751379
}
13761380

13771381
public function test_attach_transaction_fee_to_order_zero_decimal_fee() {
1378-
$order = WC_Helper_Order::create_order();
1379-
$this->order_service->attach_transaction_fee_to_order( $order, new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, 30000, [], [], 'jpy' ) );
1382+
$order = WC_Helper_Order::create_order();
1383+
$charge = new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, 30000, [], [], 'jpy' );
1384+
$charge->set_captured( true );
1385+
$this->order_service->attach_transaction_fee_to_order( $order, $charge );
13801386
$this->assertEquals( 30000, $order->get_meta( '_wcpay_transaction_fee', true ) );
13811387
}
13821388

@@ -1388,6 +1394,19 @@ public function test_attach_transaction_fee_to_order_null_fee() {
13881394
$this->order_service->attach_transaction_fee_to_order( $mock_order, new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, null, [], [], 'eur' ) );
13891395
}
13901396

1397+
public function test_attach_transaction_fee_to_order_uncaptured_charge() {
1398+
$mock_order = $this->createMock( 'WC_Order' );
1399+
$mock_order
1400+
->expects( $this->never() )
1401+
->method( 'update_meta_data' );
1402+
1403+
$charge = new WC_Payments_API_Charge( 'ch_mock', 1500, new DateTime(), null, null, null, null, 113, [], [], 'usd' );
1404+
$charge->set_captured( false );
1405+
1406+
// Fee should not be set for uncaptured charges.
1407+
$this->order_service->attach_transaction_fee_to_order( $mock_order, $charge );
1408+
}
1409+
13911410
public function test_add_note_and_metadata_for_created_refund_successful_fully_refunded(): void {
13921411
$order = WC_Helper_Order::create_order();
13931412
$order->save();

tests/unit/test-class-wc-payments-webhook-processing-service.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@ public function test_payment_intent_successful_adds_relevant_metadata() {
817817
'type' => 'card',
818818
],
819819
'application_fee_amount' => 100,
820+
'captured' => true,
820821
],
821822
],
822823
],
@@ -1651,6 +1652,7 @@ public function test_process_full_refund_succeeded(): void {
16511652
'status' => 'succeeded',
16521653
'amount' => 1800,
16531654
'currency' => 'usd',
1655+
'captured' => true,
16541656
];
16551657

16561658
$this->mock_order
@@ -1708,6 +1710,7 @@ public function test_process_partial_refund_succeeded(): void {
17081710
'status' => 'succeeded',
17091711
'amount' => 1800,
17101712
'currency' => 'usd',
1713+
'captured' => true,
17111714
];
17121715

17131716
$this->mock_order
@@ -1755,6 +1758,7 @@ public function test_process_refund_ignores_processed_event(): void {
17551758
'status' => 'succeeded',
17561759
'amount' => 1800,
17571760
'currency' => 'usd',
1761+
'captured' => true,
17581762
];
17591763

17601764
$this->mock_order
@@ -1857,6 +1861,7 @@ public function test_process_refund_throws_when_order_not_found(): void {
18571861
'status' => 'succeeded',
18581862
'amount' => 1800,
18591863
'currency' => 'usd',
1864+
'captured' => true,
18601865
];
18611866

18621867
$this->mock_db_wrapper
@@ -1887,6 +1892,7 @@ public function test_process_refund_throws_with_negative_amount(): void {
18871892
'status' => 'succeeded',
18881893
'amount' => -1800,
18891894
'currency' => 'usd',
1895+
'captured' => true,
18901896
];
18911897

18921898
$this->mock_order
@@ -1921,6 +1927,7 @@ public function test_process_refund_throws_with_invalid_refunded_amount(): void
19211927
'status' => 'succeeded',
19221928
'amount' => 1800,
19231929
'currency' => 'usd',
1930+
'captured' => true,
19241931
];
19251932

19261933
$this->mock_order
@@ -1938,6 +1945,50 @@ public function test_process_refund_throws_with_invalid_refunded_amount(): void
19381945
$this->webhook_processing_service->process( $this->event_body );
19391946
}
19401947

1948+
public function test_process_refund_ignores_uncaptured_charge(): void {
1949+
$this->event_body['type'] = 'charge.refunded';
1950+
$this->event_body['livemode'] = true;
1951+
$this->event_body['data']['object'] = [
1952+
'id' => 'test_charge_id',
1953+
'refunds' => [
1954+
'data' => [
1955+
[
1956+
'id' => 'test_refund_id',
1957+
'status' => Refund_Status::SUCCEEDED,
1958+
'amount' => 1800,
1959+
'currency' => 'usd',
1960+
'reason' => 'requested_by_customer',
1961+
'balance_transaction' => 'txn_123',
1962+
],
1963+
],
1964+
],
1965+
'status' => 'succeeded',
1966+
'amount' => 1800,
1967+
'currency' => 'usd',
1968+
'captured' => false, // Not captured - this is a canceled authorization.
1969+
];
1970+
1971+
// The webhook should return early before fetching the order since captured = false.
1972+
$this->mock_db_wrapper
1973+
->expects( $this->never() )
1974+
->method( 'order_from_charge_id' );
1975+
1976+
// Refund processing should be skipped for uncaptured charges.
1977+
$this->order_service
1978+
->expects( $this->never() )
1979+
->method( 'get_wcpay_refund_id_for_order' );
1980+
1981+
$this->order_service
1982+
->expects( $this->never() )
1983+
->method( 'create_refund_for_order' );
1984+
1985+
$this->order_service
1986+
->expects( $this->never() )
1987+
->method( 'add_note_and_metadata_for_created_refund' );
1988+
1989+
$this->webhook_processing_service->process( $this->event_body );
1990+
}
1991+
19411992
/**
19421993
* @dataProvider provider_mode_mismatch_detection
19431994
*/

0 commit comments

Comments
 (0)