From 3318ac8c17e885d04afae651df741e0b8b9c1eec Mon Sep 17 00:00:00 2001 From: Anurag Bhandari Date: Thu, 18 Jan 2024 16:08:59 +0530 Subject: [PATCH] Store balance transaction ID in order metadata (#7945) Co-authored-by: Cvetan Cvetanov Co-authored-by: Naman Malhotra Co-authored-by: Dan Paun <82826872+dpaun1985@users.noreply.github.com> --- ...pdate-7856-add-charge-txn-id-to-order-meta | 4 ++ ...ass-wc-rest-payments-orders-controller.php | 14 +---- includes/class-wc-payment-gateway-wcpay.php | 6 +- includes/class-wc-payments-order-service.php | 57 ++++++++++++++++++- .../class-wc-payments-invoice-service.php | 10 +--- src/Internal/Service/OrderService.php | 18 +++--- ...ass-wc-rest-payments-orders-controller.php | 21 +------ .../helpers/class-wc-helper-intention.php | 10 +++- .../test-class-upe-split-payment-gateway.php | 2 +- .../src/Internal/Service/OrderServiceTest.php | 6 +- ...-payment-gateway-wcpay-process-payment.php | 8 +-- .../test-class-wc-payments-order-service.php | 36 +++++++++--- 12 files changed, 125 insertions(+), 67 deletions(-) create mode 100644 changelog/update-7856-add-charge-txn-id-to-order-meta diff --git a/changelog/update-7856-add-charge-txn-id-to-order-meta b/changelog/update-7856-add-charge-txn-id-to-order-meta new file mode 100644 index 00000000000..38037b5a6d1 --- /dev/null +++ b/changelog/update-7856-add-charge-txn-id-to-order-meta @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Store balance transaction ID in order metadata. diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php index a833fc9092f..e23f1ebfcff 100644 --- a/includes/admin/class-wc-rest-payments-orders-controller.php +++ b/includes/admin/class-wc-rest-payments-orders-controller.php @@ -189,19 +189,7 @@ public function capture_terminal_payment( WP_REST_Request $request ) { // Update the order: set the payment method and attach intent attributes. $order->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID ); $order->set_payment_method_title( __( 'WooCommerce In-Person Payments', 'woocommerce-payments' ) ); - $intent_id = $intent->get_id(); - $intent_status = $intent->get_status(); - $charge = $intent->get_charge(); - $charge_id = $charge ? $charge->get_id() : null; - $this->order_service->attach_intent_info_to_order( - $order, - $intent_id, - $intent_status, - $intent->get_payment_method_id(), - $intent->get_customer_id(), - $charge_id, - $intent->get_currency() - ); + $this->order_service->attach_intent_info_to_order( $order, $intent ); $this->order_service->update_order_status_from_intent( $order, $intent ); // Certain payments (eg. Interac) are captured on the client-side (mobile app). diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index d6594de86bd..db6ebb6bb45 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -1689,7 +1689,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul } } - $this->order_service->attach_intent_info_to_order( $order, $intent_id, $status, $payment_method, $customer_id, $charge_id, $currency ); + $this->order_service->attach_intent_info_to_order( $order, $intent ); $this->attach_exchange_info_to_order( $order, $charge_id ); if ( Intent_Status::SUCCEEDED === $status ) { $this->duplicate_payment_prevention_service->remove_session_processing_order( $order->get_id() ); @@ -1884,7 +1884,7 @@ public function process_redirect_payment( $order, $intent_id, $save_payment_meth } } - $this->order_service->attach_intent_info_to_order( $order, $intent_id, $status, $payment_method_id, $customer_id, $charge_id, $currency ); + $this->order_service->attach_intent_info_to_order( $order, $intent ); $this->attach_exchange_info_to_order( $order, $charge_id ); if ( Intent_Status::SUCCEEDED === $status ) { $this->duplicate_payment_prevention_service->remove_session_processing_order( $order->get_id() ); @@ -3425,7 +3425,7 @@ public function update_order_status() { $charge_id = ! empty( $charge ) ? $charge->get_id() : null; $this->attach_exchange_info_to_order( $order, $charge_id ); - $this->order_service->attach_intent_info_to_order( $order, $intent_id, $status, $intent->get_payment_method_id(), $intent->get_customer_id(), $charge_id, $intent->get_currency() ); + $this->order_service->attach_intent_info_to_order( $order, $intent ); $this->order_service->attach_transaction_fee_to_order( $order, $charge ); } else { // For $0 orders, fetch the Setup Intent instead. diff --git a/includes/class-wc-payments-order-service.php b/includes/class-wc-payments-order-service.php index 04ea3f9cad7..21b549768a7 100644 --- a/includes/class-wc-payments-order-service.php +++ b/includes/class-wc-payments-order-service.php @@ -113,6 +113,13 @@ class WC_Payments_Order_Service { */ const WCPAY_MODE_META_KEY = '_wcpay_mode'; + /** + * Meta key used to store payment transaction Id. + * + * @const string + */ + const WCPAY_PAYMENT_TRANSACTION_ID_META_KEY = '_wcpay_payment_transaction_id'; + /** * Client for making requests to the WooCommerce Payments API * @@ -531,6 +538,23 @@ public function set_charge_id_for_order( $order, $charge_id ) { $order->save_meta_data(); } + /** + * Set the payment metadata for payment transaction id. + * + * @param mixed $order The order. + * @param string $payment_transaction_id The value to be set. + * + * @throws Order_Not_Found_Exception + */ + public function set_payment_transaction_id_for_order( $order, $payment_transaction_id ) { + if ( ! isset( $payment_transaction_id ) || null === $payment_transaction_id ) { + return; + } + $order = $this->get_order( $order ); + $order->update_meta_data( self::WCPAY_PAYMENT_TRANSACTION_ID_META_KEY, $payment_transaction_id ); + $order->save_meta_data(); + } + /** * Get the payment metadata for charge id. * @@ -773,6 +797,35 @@ public function get_fraud_meta_box_type_for_order( $order ) : string { /** * Given the payment intent data, adds it to the given order as metadata and parses any notes that need to be added * + * @param WC_Order $order The order. + * @param WC_Payments_API_Payment_Intention|WC_Payments_API_Setup_Intention $intent The payment or setup intention object. + * + * @throws Order_Not_Found_Exception + */ + public function attach_intent_info_to_order( WC_Order $order, $intent ) { + // We don't want to allow metadata for a successful payment to be disrupted. + if ( Intent_Status::SUCCEEDED === $this->get_intention_status_for_order( $order ) ) { + return; + } + // first, let's prepare all the metadata needed for refunds, required for status change etc. + $intent_id = $intent->get_id(); + $intent_status = $intent->get_status(); + $payment_method = $intent->get_payment_method_id(); + $customer_id = $intent->get_customer_id(); + $currency = $intent instanceof WC_Payments_API_Payment_Intention ? $intent->get_currency() : $order->get_currency(); + $charge = $intent instanceof WC_Payments_API_Payment_Intention ? $intent->get_charge() : null; + $charge_id = $charge ? $charge->get_id() : null; + $payment_transaction = $charge ? $charge->get_balance_transaction() : null; + $payment_transaction_id = $payment_transaction['id'] ?? ''; + // next, save it in order meta. + $this->attach_intent_info_to_order__legacy( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency, $payment_transaction_id ); + } + + /** + * Legacy version of the attach_intent_info_to_order method. + * + * TODO: This method should ultimately be merged with `attach_intent_info_to_order` and then removed. + * * @param WC_Order $order The order. * @param string $intent_id The intent ID. * @param string $intent_status Intent status. @@ -780,10 +833,11 @@ public function get_fraud_meta_box_type_for_order( $order ) : string { * @param string $customer_id Customer ID. * @param string $charge_id Charge ID. * @param string $currency Currency code. + * @param string $payment_transaction_id The transaction ID of the linked charge. * * @throws Order_Not_Found_Exception */ - public function attach_intent_info_to_order( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency ) { + public function attach_intent_info_to_order__legacy( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency, $payment_transaction_id = null ) { // first, let's save all the metadata that needed for refunds, required for status change etc. $order->set_transaction_id( $intent_id ); $this->set_intent_id_for_order( $order, $intent_id ); @@ -792,6 +846,7 @@ public function attach_intent_info_to_order( $order, $intent_id, $intent_status, $this->set_intention_status_for_order( $order, $intent_status ); $this->set_customer_id_for_order( $order, $customer_id ); $this->set_wcpay_intent_currency_for_order( $order, $currency ); + $this->set_payment_transaction_id_for_order( $order, $payment_transaction_id ); $order->save(); } diff --git a/includes/subscriptions/class-wc-payments-invoice-service.php b/includes/subscriptions/class-wc-payments-invoice-service.php index c4d0cd38fb5..2e2b23663d2 100644 --- a/includes/subscriptions/class-wc-payments-invoice-service.php +++ b/includes/subscriptions/class-wc-payments-invoice-service.php @@ -299,15 +299,7 @@ public function get_and_attach_intent_info_to_order( $order, $intent_id ) { $charge = $intent_object->get_charge(); - $this->order_service->attach_intent_info_to_order( - $order, - $intent_id, - $intent_object->get_status(), - $intent_object->get_payment_method_id(), - $intent_object->get_customer_id(), - $charge ? $charge->get_id() : null, - $intent_object->get_currency() - ); + $this->order_service->attach_intent_info_to_order( $order, $intent_object ); } /** diff --git a/src/Internal/Service/OrderService.php b/src/Internal/Service/OrderService.php index 25e962cdadd..57265fcecc9 100644 --- a/src/Internal/Service/OrderService.php +++ b/src/Internal/Service/OrderService.php @@ -186,21 +186,25 @@ public function update_order_from_successful_intent( ) { $order = $this->get_order( $order_id ); - $charge = null; - $charge_id = null; + $charge = null; + $charge_id = null; + $payment_transaction_id = null; if ( $intent instanceof WC_Payments_API_Payment_Intention ) { - $charge = $intent->get_charge(); - $charge_id = $intent->get_charge()->get_id(); + $charge = $intent->get_charge(); + $charge_id = $intent->get_charge()->get_id(); + $payment_transaction = $charge ? $charge->get_balance_transaction() : null; + $payment_transaction_id = $payment_transaction['id'] ?? ''; } - $this->legacy_service->attach_intent_info_to_order( + $this->legacy_service->attach_intent_info_to_order__legacy( $order, $intent->get_id(), $intent->get_status(), $context->get_payment_method()->get_id(), $context->get_customer_id(), $charge_id, - $context->get_currency() + $context->get_currency(), + $payment_transaction_id, ); $this->legacy_service->attach_transaction_fee_to_order( $order, $charge ); @@ -253,7 +257,7 @@ public function update_order_from_intent_that_requires_action( ) { $order = $this->get_order( $order_id ); - $this->legacy_service->attach_intent_info_to_order( + $this->legacy_service->attach_intent_info_to_order__legacy( $order, $intent->get_id(), $intent->get_status(), diff --git a/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php b/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php index 3a76bcba3ea..3d44fc9bfa5 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php @@ -114,12 +114,7 @@ public function test_capture_terminal_payment_success() { ->method( 'attach_intent_info_to_order' ) ->with( $this->isInstanceOf( WC_Order::class ), - $this->mock_intent_id, - Intent_Status::REQUIRES_CAPTURE, - 'pm_mock', - 'cus_mock', - $this->mock_charge_id, - 'USD' + $mock_intent, ); $request = new WP_REST_Request( 'POST' ); @@ -171,12 +166,7 @@ public function test_capture_terminal_payment_succeeded_intent() { ->method( 'attach_intent_info_to_order' ) ->with( $this->isInstanceOf( WC_Order::class ), - $this->mock_intent_id, - Intent_Status::SUCCEEDED, - 'pm_mock', - 'cus_mock', - $this->mock_charge_id, - 'USD' + $mock_intent, ); $this->mock_gateway @@ -236,12 +226,7 @@ public function test_capture_terminal_payment_completed_order() { ->method( 'attach_intent_info_to_order' ) ->with( $this->isInstanceOf( WC_Order::class ), - $this->mock_intent_id, - Intent_Status::SUCCEEDED, - 'pm_mock', - 'cus_mock', - $this->mock_charge_id, - 'USD' + $mock_intent, ); $this->mock_gateway diff --git a/tests/unit/helpers/class-wc-helper-intention.php b/tests/unit/helpers/class-wc-helper-intention.php index 06130bb3b7e..3879db1f32d 100644 --- a/tests/unit/helpers/class-wc-helper-intention.php +++ b/tests/unit/helpers/class-wc-helper-intention.php @@ -38,7 +38,15 @@ public static function create_charge( $data = [] ) { 'amount_captured' => 5000, 'amount_refunded' => 0, 'application_fee_amount' => 0, - 'balance_transaction' => 'txn_mock', + 'balance_transaction' => [ + 'id' => 'txn_mock', + 'amount' => 5000, + 'available_on' => 1703808000, + 'created' => new DateTime( '2022-05-20 19:05:38' ), + 'currency' => 'usd', + 'exchange_rate' => null, + 'fee' => 82, + ], 'billing_details' => [], 'currency' => 'usd', 'dispute' => [], diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php index e788e467a14..8671ab35be0 100644 --- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php @@ -621,7 +621,7 @@ public function test_process_redirect_setup_intent_succeded() { $setup_intent = WC_Helper_Intention::create_setup_intention( [ - 'id' => 'pi_mock', + 'id' => $intent_id, 'client_secret' => $client_secret, 'status' => $intent_status, 'payment_method' => $payment_method_id, diff --git a/tests/unit/src/Internal/Service/OrderServiceTest.php b/tests/unit/src/Internal/Service/OrderServiceTest.php index 63f40cf074f..3faae6b6ccd 100644 --- a/tests/unit/src/Internal/Service/OrderServiceTest.php +++ b/tests/unit/src/Internal/Service/OrderServiceTest.php @@ -332,7 +332,7 @@ public function test_update_order_from_successful_intent( $intent ) { ->willReturn( $mock_charge ); } - // Prepare all parameters for `attach_intent_info_to_order`. + // Prepare all parameters for `attach_intent_info_to_order__legacy`. $intent->expects( $this->once() ) ->method( 'get_id' ) ->willReturn( $intent_id ); @@ -355,7 +355,7 @@ public function test_update_order_from_successful_intent( $intent ) { ->willReturn( 'prod' ); $this->mock_legacy_service->expects( $this->once() ) - ->method( 'attach_intent_info_to_order' ) + ->method( 'attach_intent_info_to_order__legacy' ) ->with( $mock_order, $intent_id, @@ -412,7 +412,7 @@ public function test_update_order_from_intent_that_requires_action() { $mock_intent->expects( $this->once() )->method( 'get_status' )->willReturn( $intent_status ); $this->mock_legacy_service->expects( $this->once() ) - ->method( 'attach_intent_info_to_order' ) + ->method( 'attach_intent_info_to_order__legacy' ) ->with( $mock_order, $intent_id, diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php index 1ce7132f420..b064ef08f90 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php @@ -304,7 +304,7 @@ public function test_intent_status_success() { $this->mock_order_service ->expects( $this->once() ) ->method( 'attach_intent_info_to_order' ) - ->with( $mock_order, $intent_id, $status, 'pm_mock', $customer_id, $charge_id, 'USD' ); + ->with( $mock_order, $intent ); $this->mock_order_service ->expects( $this->once() ) @@ -471,7 +471,7 @@ public function test_intent_status_requires_capture() { $this->mock_order_service ->expects( $this->once() ) ->method( 'attach_intent_info_to_order' ) - ->with( $mock_order, $intent_id, $status, 'pm_mock', $customer_id, $charge_id, 'USD' ); + ->with( $mock_order, $intent ); // Assert: The Order_Service is called correctly. $this->mock_order_service @@ -919,7 +919,7 @@ public function test_intent_status_requires_action() { $this->mock_order_service ->expects( $this->once() ) ->method( 'attach_intent_info_to_order' ) - ->with( $mock_order, $intent_id, $status, 'pm_mock', $customer_id, $charge_id, 'USD' ); + ->with( $mock_order, $intent ); $this->mock_order_service ->expects( $this->once() ) @@ -1035,7 +1035,7 @@ public function test_setup_intent_status_requires_action() { $this->mock_order_service ->expects( $this->once() ) ->method( 'attach_intent_info_to_order' ) - ->with( $mock_order, $intent_id, $status, 'pm_mock', $customer_id, '', 'USD' ); + ->with( $mock_order, $intent ); // Assert: Order status was not updated. $mock_order diff --git a/tests/unit/test-class-wc-payments-order-service.php b/tests/unit/test-class-wc-payments-order-service.php index 83ea7139ed9..b1d1f030ff0 100644 --- a/tests/unit/test-class-wc-payments-order-service.php +++ b/tests/unit/test-class-wc-payments-order-service.php @@ -1211,18 +1211,40 @@ public function test_get_fraud_meta_box_type() { $this->assertEquals( $fraud_meta_box_type_from_service, $fraud_meta_box_type ); } + public function test_set_payment_transaction_id_for_order() { + $transaction_id = 'txn_mock'; + $this->order_service->set_payment_transaction_id_for_order( $this->order, $transaction_id ); + $this->assertSame( $this->order->get_meta( '_wcpay_payment_transaction_id', true ), $transaction_id ); + } + public function test_attach_intent_info_to_order() { - $intent_id = 'pi_mock'; - $intent_status = 'succeeded'; - $payment_method = 'woocommerce_payments'; - $customer_id = 'cus_12345'; - $charge_id = 'ch_mock'; - $currency = 'USD'; - $this->order_service->attach_intent_info_to_order( $this->order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency ); + $intent_id = 'pi_mock'; + $intent = WC_Helper_Intention::create_intention( [ 'id' => $intent_id ] ); + $this->order_service->attach_intent_info_to_order( $this->order, $intent ); $this->assertEquals( $intent_id, $this->order->get_meta( '_intent_id', true ) ); } + public function test_attach_intent_info_to_order_after_successful_payment() { + $intent = WC_Helper_Intention::create_intention( + [ + 'id' => 'pi_mock', + 'status' => Intent_Status::SUCCEEDED, + ] + ); + $this->order_service->attach_intent_info_to_order( $this->order, $intent ); + + $another_intent = WC_Helper_Intention::create_intention( + [ + 'id' => 'pi_mock_2', + 'status' => Intent_Status::CANCELED, + ] + ); + $this->order_service->attach_intent_info_to_order( $this->order, $another_intent ); + + $this->assertEquals( Intent_Status::SUCCEEDED, $this->order->get_meta( '_intention_status', true ) ); + } + /** * Several methods use the private method get_order to get the order being worked on. If an order is not found * then an exception is thrown. This test attempt to confirm that exception gets thrown.