Skip to content

Commit

Permalink
Add support to offline payment methods in checkout (#10219)
Browse files Browse the repository at this point in the history
  • Loading branch information
gpressutto5 authored Jan 23, 2025
1 parent 5a68b21 commit dd96d3f
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 3 deletions.
10 changes: 10 additions & 0 deletions includes/class-payment-information.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
exit; // Exit if accessed directly.
}

use WCPay\Constants\Payment_Method;
use WCPay\Constants\Payment_Type;
use WCPay\Constants\Payment_Initiated_By;
use WCPay\Constants\Payment_Capture_Type;
Expand Down Expand Up @@ -498,4 +499,13 @@ public function set_error( \WP_Error $error ) {
public function get_error() {
return $this->error;
}

/**
* Returns true if the payment method is an offline payment method, false otherwise.
*
* @return bool True if the payment method is an offline payment method, false otherwise.
*/
public function is_offline_payment_method(): bool {
return in_array( $this->payment_method_stripe_id, Payment_Method::OFFLINE_PAYMENT_METHODS, true );
}
}
5 changes: 3 additions & 2 deletions includes/class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$processing = [];
}

$is_offline_payment_method = $payment_information->is_offline_payment_method();
if ( ! empty( $intent ) ) {
if ( ! $intent->is_authorized() ) {
$intent_failed = true;
Expand Down Expand Up @@ -1860,15 +1861,15 @@ public function process_payment_for_order( $cart, $payment_information, $schedul

$this->order_service->attach_intent_info_to_order( $order, $intent );
$this->attach_exchange_info_to_order( $order, $charge_id );
if ( Intent_Status::SUCCEEDED === $status ) {
if ( Intent_Status::SUCCEEDED === $status || ( Intent_Status::REQUIRES_ACTION === $status && $is_offline_payment_method ) ) {
$this->duplicate_payment_prevention_service->remove_session_processing_order( $order->get_id() );
}
$this->order_service->update_order_status_from_intent( $order, $intent );
$this->order_service->attach_transaction_fee_to_order( $order, $charge );

$this->maybe_add_customer_notification_note( $order, $processing );

if ( isset( $response ) ) {
if ( isset( $response ) && ! $is_offline_payment_method ) {
return $response;
}

Expand Down
29 changes: 28 additions & 1 deletion includes/class-wc-payments-order-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use WCPay\Constants\Fraud_Meta_Box_Type;
use WCPay\Constants\Order_Status;
use WCPay\Constants\Intent_Status;
use WCPay\Constants\Payment_Method;
use WCPay\Exceptions\Order_Not_Found_Exception;
use WCPay\Fraud_Prevention\Models\Rule;
use WCPay\Logger;
Expand Down Expand Up @@ -180,7 +181,11 @@ public function update_order_status_from_intent( $order, $intent ) {
break;
case Intent_Status::REQUIRES_ACTION:
case Intent_Status::REQUIRES_PAYMENT_METHOD:
$this->mark_payment_started( $order, $intent_data );
if ( in_array( $intent->get_payment_method_type(), Payment_Method::OFFLINE_PAYMENT_METHODS, true ) ) {
$this->mark_payment_on_hold( $order, $intent_data );
} else {
$this->mark_payment_started( $order, $intent_data );
}
break;
default:
Logger::error( 'Uncaught payment intent status of ' . $intent_data['intent_status'] . ' passed for order id: ' . $order->get_id() );
Expand Down Expand Up @@ -1114,6 +1119,28 @@ private function mark_payment_authorized( $order, $intent_data ) {
$this->set_intention_status_for_order( $order, $intent_data['intent_status'] );
}

/**
* Updates an order to on-hold status, while adding a note with a link to the transaction.
*
* @param WC_Order $order Order object.
* @param array $intent_data The intent data associated with this order.
*
* @return void
*/
private function mark_payment_on_hold( $order, $intent_data ) {
$note = $this->generate_payment_started_note( $order, $intent_data['intent_id'] );
if ( $this->order_note_exists( $order, $note ) ) {
return;
}

$fraud_meta_box_type = $this->intent_has_card_payment_type( $intent_data ) ? Fraud_Meta_Box_Type::PAYMENT_STARTED : Fraud_Meta_Box_Type::NOT_CARD;
$this->set_fraud_meta_box_type_for_order( $order, $fraud_meta_box_type );

$this->update_order_status( $order, Order_Status::ON_HOLD );
$order->add_order_note( $note );
$this->set_intention_status_for_order( $order, $intent_data['intent_status'] );
}

/**
* Updates an order to processing/completed status, while adding a note with a link to the transaction.
*
Expand Down
4 changes: 4 additions & 0 deletions includes/constants/class-payment-method.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ class Payment_Method extends Base_Constant {
self::AFTERPAY,
self::KLARNA,
];

const OFFLINE_PAYMENT_METHODS = [
'offline_test_payment_method', // TODO: Remove this once we have a real offline payment method.
];
}
113 changes: 113 additions & 0 deletions tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,119 @@ public function test_intent_status_requires_action() {
);
}

/**
* Test processing offline payment with the status "requires_action".
* This is the status returned when the shopper needs to complete
* the payment offsite.
*/
public function test_intent_status_requires_action_offine_payment() {
// Arrange: Reusable data.
$intent_id = 'pi_mock';
$charge_id = 'ch_mock';
$customer_id = 'cus_mock';
$status = Intent_Status::REQUIRES_ACTION;
$secret = 'cs_mock';
$order_id = 123;
$total = 12.23;

// Arrange: Create an order to test with.
$mock_order = $this->createMock( 'WC_Order' );

// Arrange: Set a good return value for order ID.
$mock_order
->method( 'get_id' )
->willReturn( $order_id );

// Arrange: Set a good return value for order total.
$mock_order
->method( 'get_total' )
->willReturn( $total );

// Arrange: Set a WP_User object as a return value of order's get_user.
$mock_order
->method( 'get_user' )
->willReturn( wp_get_current_user() );

// Arrange: Set a good return value for customer ID.
$this->mock_customer_service->expects( $this->once() )
->method( 'create_customer_for_user' )
->willReturn( $customer_id );

// Arrange: Create a mock cart.
$mock_cart = $this->createMock( 'WC_Cart' );

// Arrange: Return a 'requires_action' response from create_and_confirm_intention().
$intent = WC_Helper_Intention::create_intention( [ 'status' => $status ] );

$request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class );

$request->expects( $this->once() )
->method( 'format_response' )
->willReturn( $intent );

// Assert: Order has correct charge id meta data.
// Assert: Order has correct intention status meta data.
// Assert: Order has correct intent ID.
// This test is a little brittle because we don't really care about the order
// in which the different calls are made, but it's not possible to write it
// otherwise for now.
// There's an issue open for that here:
// https://github.com/sebastianbergmann/phpunit/issues/4026.
$mock_order
->expects( $this->exactly( 2 ) )
->method( 'update_meta_data' )
->withConsecutive(
[ '_wcpay_mode', WC_Payments::mode()->is_test() ? 'test' : 'prod' ],
[ '_wcpay_multi_currency_stripe_exchange_rate', 0.86 ]
);

// Assert: The Order_Service is called correctly.
$this->mock_order_service
->expects( $this->once() )
->method( 'set_customer_id_for_order' )
->with( $mock_order, $customer_id );

$this->mock_order_service
->expects( $this->once() )
->method( 'set_payment_method_id_for_order' )
->with( $mock_order, 'pm_mock' );

$this->mock_order_service
->expects( $this->once() )
->method( 'attach_intent_info_to_order' )
->with( $mock_order, $intent );

$this->mock_order_service
->expects( $this->once() )
->method( 'update_order_status_from_intent' )
->with( $mock_order, $intent );

// Assert: empty_cart() was called (just like status success).
$mock_cart
->expects( $this->once() )
->method( 'empty_cart' );

$charge_request = $this->mock_wcpay_request( Get_Charge::class, 1, 'ch_mock' );

$charge_request->expects( $this->once() )
->method( 'format_response' )
->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] );

// Act: process payment.
$payment_information_mock = $this->getMockBuilder( 'WCPay\Payment_Information' )
->setConstructorArgs( [ 'pm_mock', $mock_order, null, null, null, null, null, '', 'card' ] )
->setMethods( [ 'is_offline_payment_method' ] )
->getMock();
$payment_information_mock->method( 'is_offline_payment_method' )
->willReturn( true );

$result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information_mock );

// Assert: Returning correct array.
$this->assertEquals( 'success', $result['result'] );
$this->assertEquals( $this->return_url, $result['redirect'] );
}

/**
* Test processing free order with the status "requires_action".
* This is the status returned when the saved card setup requires
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test-class-wc-payments-order-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use WCPay\Constants\Fraud_Meta_Box_Type;
use WCPay\Constants\Order_Status;
use WCPay\Constants\Intent_Status;
use WCPay\Constants\Payment_Method;
use WCPay\Fraud_Prevention\Models\Rule;

/**
Expand Down Expand Up @@ -476,6 +477,47 @@ public function mark_payment_started_provider() {
];
}

/**
* Tests if the order is marked with the payment on hold for offline payments.
* Public method update_order_status_from_intent calls private method mark_payment_on_hold.
*/
public function test_mark_payment_on_hold() {
// Arrange: Create intention with provided args.
$intent = WC_Helper_Intention::create_intention(
[
'status' => Intent_Status::REQUIRES_ACTION,
'payment_method_types' => [ 'offline_test_payment_method' ],
'payment_method_options' => [ Payment_Method::OFFLINE_PAYMENT_METHODS[0] => [] ],
]
);

// Act: Attempt to mark the payment on hold.
$this->order_service->update_order_status_from_intent( $this->order, $intent );

// Assert: Check to make sure the intent_status meta was set.
$this->assertEquals( $intent->get_status(), $this->order_service->get_intention_status_for_order( $this->order ) );

// Assert: Confirm that the fraud outcome status and fraud meta box type meta were not set/set correctly.
$this->assertEquals( false, $this->order_service->get_fraud_outcome_status_for_order( $this->order ) );
$this->assertEquals( Fraud_Meta_Box_Type::NOT_CARD, $this->order_service->get_fraud_meta_box_type_for_order( $this->order ) );

// Assert: Check that the order status was updated to on hold.
$this->assertTrue( $this->order->has_status( Order_Status::ON_HOLD ) );

// Assert: Check that the notes were updated.
$notes = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
$this->assertStringContainsString( 'started</strong> using WooPayments', $notes[0]->content );
$this->assertStringContainsString( 'Payments (<code>pi_mock</code>)', $notes[0]->content );

// Assert: Check that the order was unlocked.
$this->assertFalse( get_transient( 'wcpay_processing_intent_' . $this->order->get_id() ) );

// Assert: Applying the same data multiple times does not cause duplicate actions.
$this->order_service->update_order_status_from_intent( $this->order, $intent );
$notes_2 = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
$this->assertEquals( count( $notes ), count( $notes_2 ) );
}

/**
* Tests if mark_payment_started exits if the order status is not Peding.
* Public method update_order_status_from_intent calls private method mark_payment_started.
Expand Down

0 comments on commit dd96d3f

Please sign in to comment.