+ At least one risk filter needs to be enabled for advanced protection.
+
( {
dispatch: jest.fn( () => ( {
setIsMatching: jest.fn(),
createSuccessNotice: jest.fn(),
+ createErrorNotice: jest.fn(),
onLoad: jest.fn(),
} ) ),
registerStore: jest.fn(),
@@ -438,4 +439,45 @@ describe( 'Advanced fraud protection settings', () => {
expect( protectionLevelState.updateState.mock.calls.length ).toBe( 0 );
expect( protectionLevelState.updateState.mock.calls ).toEqual( [] );
} );
+ test( 'does not update protection level to advanced when no risk rules are enabled', async () => {
+ const protectionLevelState = {
+ state: 'standard',
+ updateState: jest.fn( ( level ) => {
+ protectionLevelState.state = level;
+ } ),
+ };
+ mockUseCurrentProtectionLevel.mockReturnValue( [
+ protectionLevelState.state,
+ protectionLevelState.updateState,
+ ] );
+ mockUseSettings.mockReturnValue( {
+ settings: {
+ advanced_fraud_protection_settings: defaultSettings,
+ },
+ isSaving: false,
+ saveSettings: jest.fn(),
+ isLoading: false,
+ } );
+ mockUseAdvancedFraudProtectionSettings.mockReturnValue( [
+ defaultSettings,
+ jest.fn(),
+ ] );
+ container = render(
+
+ );
+ const [ saveButton ] = await container.findAllByText( 'Save Changes' );
+ saveButton.click();
+ await waitFor( () => {
+ expect( mockUseSettings().saveSettings.mock.calls.length ).toBe(
+ 1
+ );
+ } );
+
+ expect( protectionLevelState.state ).toBe( 'basic' );
+ } );
} );
diff --git a/client/settings/fraud-protection/style.scss b/client/settings/fraud-protection/style.scss
index 70d75d9f0b5..1981d4b3559 100644
--- a/client/settings/fraud-protection/style.scss
+++ b/client/settings/fraud-protection/style.scss
@@ -43,7 +43,7 @@
}
&-header-breadcrumb {
margin-top: 0;
- margin-bottom: 24px;
+ margin-bottom: 8px;
@media screen and ( min-width: 961px ) {
margin-top: -16px;
}
@@ -261,6 +261,10 @@
);
cursor: pointer;
}
+ &-advanced-settings-notice {
+ margin-top: 0;
+ margin-bottom: 16px;
+ }
}
.components-modal__header {
diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php
index 093004d37b2..10068abaf83 100644
--- a/includes/admin/class-wc-rest-payments-orders-controller.php
+++ b/includes/admin/class-wc-rest-payments-orders-controller.php
@@ -47,6 +47,13 @@ class WC_REST_Payments_Orders_Controller extends WC_Payments_REST_Controller {
*/
private $order_service;
+ /**
+ * WC_Payments_Token instance for working with customer tokens
+ *
+ * @var WC_Payments_Token_Service
+ */
+ private $token_service;
+
/**
* WC_Payments_REST_Controller constructor.
*
@@ -54,12 +61,14 @@ class WC_REST_Payments_Orders_Controller extends WC_Payments_REST_Controller {
* @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway.
* @param WC_Payments_Customer_Service $customer_service Customer class instance.
* @param WC_Payments_Order_Service $order_service Order Service class instance.
+ * @param WC_Payments_Token_Service $token_service Token Service class instance.
*/
- public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service ) {
+ public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service, WC_Payments_Token_Service $token_service ) {
parent::__construct( $api_client );
$this->gateway = $gateway;
$this->customer_service = $customer_service;
$this->order_service = $order_service;
+ $this->token_service = $token_service;
}
/**
@@ -217,6 +226,30 @@ public function capture_terminal_payment( WP_REST_Request $request ) {
}
// Store receipt generation URL for mobile applications in order meta-data.
$order->add_meta_data( 'receipt_url', get_rest_url( null, sprintf( '%s/payments/readers/receipts/%s', $this->namespace, $intent->get_id() ) ) );
+
+ // Add payment method for future subscription payments.
+ $generated_card = $intent->get_charge()->get_payment_method_details()[ Payment_Method::CARD_PRESENT ]['generated_card'] ?? null;
+ // If we don't get a generated card, e.g. because a digital wallet was used, we can still return that the initial payment was successful.
+ // The subscription will not be activated and customers will need to provide a new payment method for renewals.
+ if ( $generated_card ) {
+ $has_subscriptions = function_exists( 'wcs_order_contains_subscription' ) &&
+ function_exists( 'wcs_get_subscriptions_for_order' ) &&
+ function_exists( 'wcs_is_manual_renewal_required' ) &&
+ wcs_order_contains_subscription( $order_id );
+ if ( $has_subscriptions ) {
+ $token = $this->token_service->add_payment_method_to_user( $generated_card, $order->get_user() );
+ $this->gateway->add_token_to_order( $order, $token );
+ foreach ( wcs_get_subscriptions_for_order( $order ) as $subscription ) {
+ $subscription->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID );
+ // Where the setting doesn't force manual renewals, we should turn them off, because we have an auto-renewal token now.
+ if ( ! wcs_is_manual_renewal_required() ) {
+ $subscription->set_requires_manual_renewal( false );
+ }
+ $subscription->save();
+ }
+ }
+ }
+
// Actualize order status.
$this->order_service->mark_terminal_payment_completed( $order, $intent_id, $result['status'] );
@@ -307,15 +340,14 @@ public function capture_authorization( WP_REST_Request $request ) {
/**
* Returns customer id from order. Create or update customer if needed.
- * Use-cases: It was used by older versions of our Mobile apps in their workflows.
- *
- * @deprecated 3.9.0
+ * Use-cases:
+ * - It was used by older versions of our mobile apps to add the customer details to Payment Intents.
+ * - It is used by the apps to set customer details on Payment Intents for an order containing subscriptions. Required for capturing renewal payments off session.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_customer( $request ) {
- wc_deprecated_function( __FUNCTION__, '3.9.0' );
try {
$order_id = $request['order_id'];
diff --git a/includes/admin/class-wc-rest-woopay-session-controller.php b/includes/admin/class-wc-rest-woopay-session-controller.php
index bc9caeb0763..e35f6e32f24 100644
--- a/includes/admin/class-wc-rest-woopay-session-controller.php
+++ b/includes/admin/class-wc-rest-woopay-session-controller.php
@@ -9,8 +9,6 @@
use WCPay\WooPay\WooPay_Session;
use Automattic\Jetpack\Connection\Rest_Authentication;
-use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
-use WCPay\Exceptions\Rest_Request_Exception;
use WCPay\Logger;
/**
@@ -56,13 +54,6 @@ public function register_routes() {
*/
public function get_session_data( WP_REST_Request $request ): WP_REST_Response {
try {
- $payload = $this->validated_cart_token_payload( $request->get_header( 'cart_token' ) );
- $user_id = (int) $payload->user_id ?? null;
-
- if ( is_int( $user_id ) && $user_id > 0 ) {
- wp_set_current_user( $user_id );
- }
-
// phpcs:ignore
/**
* @psalm-suppress UndefinedClass
@@ -70,10 +61,8 @@ public function get_session_data( WP_REST_Request $request ): WP_REST_Response {
$response = WooPay_Session::get_init_session_request( null, null, null, $request );
return rest_ensure_response( $response );
- } catch ( Rest_Request_Exception $e ) {
- $error_code = $e->getCode() === 400 ? 'rest_invalid_param' : 'wcpay_server_error';
- $error = new WP_Error( $error_code, $e->getMessage(), [ 'status' => $e->getCode() ] );
-
+ } catch ( Exception $e ) {
+ $error = new WP_Error( 'wcpay_server_error', $e->getMessage(), [ 'status' => 400 ] );
Logger::log( 'Error validating cart token from WooPay request: ' . $e->getMessage() );
return rest_convert_error_to_response( $error );
@@ -89,31 +78,6 @@ public function check_permission() {
return $this->is_request_from_woopay() && $this->has_valid_request_signature();
}
- /**
- * Validates the cart token and returns its payload.
- *
- * @param string|null $cart_token The cart token to validate.
- *
- * @return object The validated cart token.
- *
- * @throws Rest_Request_Exception If the cart token is invalid, missing, or cannot be validated.
- */
- public function validated_cart_token_payload( $cart_token ): object {
- if ( ! $cart_token ) {
- throw new Rest_Request_Exception( 'Missing cart token.', 400 );
- }
-
- if ( ! class_exists( JsonWebToken::class ) ) {
- throw new Rest_Request_Exception( 'Cannot validate cart token.', 500 );
- }
-
- if ( ! JsonWebToken::validate( $cart_token, '@' . wp_salt() ) ) {
- throw new Rest_Request_Exception( 'Invalid cart token.', 400 );
- }
-
- return JsonWebToken::get_parts( $cart_token )->payload;
- }
-
/**
* Returns true if the request that's currently being processed is signed with the blog token.
*
diff --git a/includes/class-wc-payments-token-service.php b/includes/class-wc-payments-token-service.php
index 37fa25ced0e..6a8f9e21d0a 100644
--- a/includes/class-wc-payments-token-service.php
+++ b/includes/class-wc-payments-token-service.php
@@ -81,6 +81,14 @@ public function add_token_to_user( $payment_method, $user ) {
$token->set_gateway_id( $gateway_id );
$token->set_email( $payment_method[ Payment_Method::LINK ]['email'] );
break;
+ case Payment_Method::CARD_PRESENT:
+ $token = new WC_Payment_Token_CC();
+ $token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID );
+ $token->set_expiry_month( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_month'] );
+ $token->set_expiry_year( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_year'] );
+ $token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD_PRESENT ]['brand'] ) );
+ $token->set_last4( $payment_method[ Payment_Method::CARD_PRESENT ]['last4'] );
+ break;
default:
$token = new WC_Payment_Token_CC();
$token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID );
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 72c90d3a635..cc6637c30a3 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -992,7 +992,7 @@ public static function init_rest_api() {
$conn_tokens_controller->register_routes();
include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-orders-controller.php';
- $orders_controller = new WC_REST_Payments_Orders_Controller( self::$api_client, self::get_gateway(), self::$customer_service, self::$order_service );
+ $orders_controller = new WC_REST_Payments_Orders_Controller( self::$api_client, self::get_gateway(), self::$customer_service, self::$order_service, self::$token_service );
$orders_controller->register_routes();
include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-fraud-outcomes-controller.php';
diff --git a/includes/core/server/request/class-add-account-tos-agreement.md b/includes/core/server/request/class-add-account-tos-agreement.md
index 5b97bee0993..b25dc68ede2 100644
--- a/includes/core/server/request/class-add-account-tos-agreement.md
+++ b/includes/core/server/request/class-add-account-tos-agreement.md
@@ -1,6 +1,6 @@
# `Add_Account_Tos_Agreement` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-cancel-intention.md b/includes/core/server/request/class-cancel-intention.md
index 0f22a295fae..9c4c1e449e5 100644
--- a/includes/core/server/request/class-cancel-intention.md
+++ b/includes/core/server/request/class-cancel-intention.md
@@ -1,6 +1,6 @@
# `Cancel_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-capture-intention.md b/includes/core/server/request/class-capture-intention.md
index 785b9012bd9..0b217e0cbe8 100644
--- a/includes/core/server/request/class-capture-intention.md
+++ b/includes/core/server/request/class-capture-intention.md
@@ -1,6 +1,6 @@
# `Capture_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-create-and-confirm-intention.md b/includes/core/server/request/class-create-and-confirm-intention.md
index e6e2c390f00..809c2af99a6 100644
--- a/includes/core/server/request/class-create-and-confirm-intention.md
+++ b/includes/core/server/request/class-create-and-confirm-intention.md
@@ -1,6 +1,6 @@
# `Create_and_Confirm_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-create-and-confirm-setup-intention.md b/includes/core/server/request/class-create-and-confirm-setup-intention.md
index e50bf1fd471..e1a4de75f80 100644
--- a/includes/core/server/request/class-create-and-confirm-setup-intention.md
+++ b/includes/core/server/request/class-create-and-confirm-setup-intention.md
@@ -1,6 +1,6 @@
# `Create_and_Confirm_Setup_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-create-intention.md b/includes/core/server/request/class-create-intention.md
index 0ac4f7c397b..fc7c16fbf60 100644
--- a/includes/core/server/request/class-create-intention.md
+++ b/includes/core/server/request/class-create-intention.md
@@ -1,6 +1,6 @@
# `Create_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-create-setup-intention.md b/includes/core/server/request/class-create-setup-intention.md
index 877ec791a65..a8aae0547ff 100644
--- a/includes/core/server/request/class-create-setup-intention.md
+++ b/includes/core/server/request/class-create-setup-intention.md
@@ -1,6 +1,6 @@
# `Create_Setup_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-get-account-capital-link.md b/includes/core/server/request/class-get-account-capital-link.md
index 2360ba3a0a9..861fd5f6c30 100644
--- a/includes/core/server/request/class-get-account-capital-link.md
+++ b/includes/core/server/request/class-get-account-capital-link.md
@@ -1,6 +1,6 @@
# `Get_Account_Capital_Link` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-get-account-login-data.md b/includes/core/server/request/class-get-account-login-data.md
index 3f3675207e4..d5e306138e7 100644
--- a/includes/core/server/request/class-get-account-login-data.md
+++ b/includes/core/server/request/class-get-account-login-data.md
@@ -1,6 +1,6 @@
# `Get_Account_Login_Data` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-get-account.md b/includes/core/server/request/class-get-account.md
index 48606f63de9..ef329de1011 100644
--- a/includes/core/server/request/class-get-account.md
+++ b/includes/core/server/request/class-get-account.md
@@ -1,6 +1,6 @@
# `Get_Account` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md).
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md).
## Description
diff --git a/includes/core/server/request/class-get-charge.md b/includes/core/server/request/class-get-charge.md
index bc9173a41e1..adecfb612e2 100644
--- a/includes/core/server/request/class-get-charge.md
+++ b/includes/core/server/request/class-get-charge.md
@@ -1,6 +1,6 @@
# `Get_Charge` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-get-intention.md b/includes/core/server/request/class-get-intention.md
index f7b71e8c091..90331d78c43 100644
--- a/includes/core/server/request/class-get-intention.md
+++ b/includes/core/server/request/class-get-intention.md
@@ -1,6 +1,6 @@
# `Get_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-get-reporting-payment-activity.md b/includes/core/server/request/class-get-reporting-payment-activity.md
new file mode 100644
index 00000000000..53f676f6d59
--- /dev/null
+++ b/includes/core/server/request/class-get-reporting-payment-activity.md
@@ -0,0 +1,38 @@
+# `Get_Reporting_Payment_Activity` request class
+
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
+
+## Description
+
+The `WCPay\Core\Server\Request\Get_Reporting_Payment_Activity` class is used to construct the request for retrieving payment activity.
+
+## Parameters
+
+| Parameter | Setter | Immutable | Required | Default value |
+|-------------|-------------------------------------------|:---------:|:--------:|:-------------:|
+| `date_start`| `set_date_start( string $date_start )` | No | Yes | - |
+| `date_end` | `set_date_end( string $date_end )` | No | Yes | - |
+| `timezone` | `set_timezone( string $timezone )` | No | Yes | - |
+
+The `date_start` and `date_end` parameters should be in the 'YYYY-MM-DDT00:00:00' format.
+The `timezone` parameter can be passed as an offset or as a [timezone name](https://www.php.net/manual/en/timezones.php).
+
+## Filter
+
+When using this request, provide the following filter and arguments:
+
+- Name: `wcpay_get_payment_activity`
+
+## Example:
+
+```php
+$request = Get_Reporting_Payment_Activity::create();
+$request->set_date_start( $date_start );
+$request->set_date_end( $date_end );
+$request->set_timezone( $timezone );
+$request->send();
+```
+
+## Exceptions
+
+- `Invalid_Request_Parameter_Exception` - Thrown when the provided date or timezone is not in expected format.
\ No newline at end of file
diff --git a/includes/core/server/request/class-get-reporting-payment-activity.php b/includes/core/server/request/class-get-reporting-payment-activity.php
index d9be6cf33eb..f8697a198e8 100644
--- a/includes/core/server/request/class-get-reporting-payment-activity.php
+++ b/includes/core/server/request/class-get-reporting-payment-activity.php
@@ -16,7 +16,6 @@
*/
class Get_Reporting_Payment_Activity extends Request {
-
const REQUIRED_PARAMS = [
'date_start',
'date_end',
@@ -50,32 +49,52 @@ public function get_method(): string {
/**
* Sets the start date for the payment activity data.
*
- * @param string|null $date_start The start date in the format 'YYYY-MM-DDT00:00:00' or null.
+ * @param string $date_start The start date in the format 'YYYY-MM-DDT00:00:00'.
* @return void
+ *
+ * @throws Invalid_Request_Parameter_Exception Exception if the date is not in valid format.
*/
- public function set_date_start( ?string $date_start ) {
- // TBD - validation.
+ public function set_date_start( string $date_start ) {
+ $this->validate_date( $date_start, 'Y-m-d\TH:i:s' );
$this->set_param( 'date_start', $date_start );
}
/**
* Sets the end date for the payment activity data.
*
- * @param string|null $date_end The end date in the format 'YYYY-MM-DDT00:00:00' or null.
+ * @param string $date_end The end date in the format 'YYYY-MM-DDT00:00:00'.
* @return void
+ *
+ * @throws Invalid_Request_Parameter_Exception Exception if the date is not in valid format.
*/
public function set_date_end( string $date_end ) {
- // TBD - validation.
+ $this->validate_date( $date_end, 'Y-m-d\TH:i:s' );
$this->set_param( 'date_end', $date_end );
}
/**
* Sets the timezone for the reporting data.
*
- * @param string|null $timezone The timezone to set or null.
+ * @param string $timezone The timezone to set.
* @return void
+ *
+ * @throws Invalid_Request_Parameter_Exception Exception if the timezone is not in valid format.
*/
- public function set_timezone( ?string $timezone ) {
- $this->set_param( 'timezone', $timezone ?? 'UTC' );
+ public function set_timezone( string $timezone ) {
+ try {
+ new \DateTimeZone( $timezone );
+ } catch ( \Exception $e ) {
+ throw new Invalid_Request_Parameter_Exception(
+ esc_html(
+ sprintf(
+ // Translators: %s is a provided timezone.
+ __( '%s is not a valid timezone.', 'woocommerce-payments' ),
+ $timezone,
+ )
+ ),
+ 'wcpay_core_invalid_request_parameter_invalid_timezone'
+ );
+ }
+ $this->set_param( 'timezone', $timezone );
}
}
diff --git a/includes/core/server/request/class-get-request.md b/includes/core/server/request/class-get-request.md
index b591cb98cba..b439ec2e612 100644
--- a/includes/core/server/request/class-get-request.md
+++ b/includes/core/server/request/class-get-request.md
@@ -1,7 +1,7 @@
# `Get_Request` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-get-setup-intention.md b/includes/core/server/request/class-get-setup-intention.md
index 5b42c8ad4fe..ab3ed78e07e 100644
--- a/includes/core/server/request/class-get-setup-intention.md
+++ b/includes/core/server/request/class-get-setup-intention.md
@@ -1,6 +1,6 @@
# `Get_Setup_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-list-authorizations.md b/includes/core/server/request/class-list-authorizations.md
index ba1b070ef65..5802476a3bd 100644
--- a/includes/core/server/request/class-list-authorizations.md
+++ b/includes/core/server/request/class-list-authorizations.md
@@ -1,6 +1,6 @@
# `List_Authorizations` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-list-charge-refunds.md b/includes/core/server/request/class-list-charge-refunds.md
index 485f808d4af..4d9b73873a7 100644
--- a/includes/core/server/request/class-list-charge-refunds.md
+++ b/includes/core/server/request/class-list-charge-refunds.md
@@ -1,6 +1,6 @@
# `List_Charge_Refunds` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-list-deposits.md b/includes/core/server/request/class-list-deposits.md
index 9e25eacc72c..e3560134f77 100644
--- a/includes/core/server/request/class-list-deposits.md
+++ b/includes/core/server/request/class-list-deposits.md
@@ -1,6 +1,6 @@
# `List_Deposits` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-list-disputes.md b/includes/core/server/request/class-list-disputes.md
index 83e720fda2a..2b35a520e04 100644
--- a/includes/core/server/request/class-list-disputes.md
+++ b/includes/core/server/request/class-list-disputes.md
@@ -1,6 +1,6 @@
# `List_Disputes` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-list-documents.md b/includes/core/server/request/class-list-documents.md
index 207a2eb00fd..17d06b911c3 100644
--- a/includes/core/server/request/class-list-documents.md
+++ b/includes/core/server/request/class-list-documents.md
@@ -1,6 +1,6 @@
# `List_Documents` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-list-fraud-outcome-transactions.md b/includes/core/server/request/class-list-fraud-outcome-transactions.md
index 7e7b48d49f0..2c24e506414 100644
--- a/includes/core/server/request/class-list-fraud-outcome-transactions.md
+++ b/includes/core/server/request/class-list-fraud-outcome-transactions.md
@@ -1,6 +1,6 @@
# `List_Fraud_Outcome_Transactions` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-list-transactions.md b/includes/core/server/request/class-list-transactions.md
index d791bcc322d..9c5bbbeaff2 100644
--- a/includes/core/server/request/class-list-transactions.md
+++ b/includes/core/server/request/class-list-transactions.md
@@ -1,6 +1,6 @@
# `List_Transactions` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-refund-charge.md b/includes/core/server/request/class-refund-charge.md
index 7ea32059739..36610499e43 100644
--- a/includes/core/server/request/class-refund-charge.md
+++ b/includes/core/server/request/class-refund-charge.md
@@ -1,6 +1,6 @@
# `Refund_Charge` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-update-account.md b/includes/core/server/request/class-update-account.md
index 759fed59df2..ac44ba13b62 100644
--- a/includes/core/server/request/class-update-account.md
+++ b/includes/core/server/request/class-update-account.md
@@ -1,6 +1,6 @@
# `Update_Account` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-update-intention.md b/includes/core/server/request/class-update-intention.md
index cdc37f87aca..2a28b99e342 100644
--- a/includes/core/server/request/class-update-intention.md
+++ b/includes/core/server/request/class-update-intention.md
@@ -1,6 +1,6 @@
# `Update_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-woopay-create-and-confirm-intention.md b/includes/core/server/request/class-woopay-create-and-confirm-intention.md
index 225cf21e01c..2b307abba5a 100644
--- a/includes/core/server/request/class-woopay-create-and-confirm-intention.md
+++ b/includes/core/server/request/class-woopay-create-and-confirm-intention.md
@@ -1,6 +1,6 @@
# `WooPay_Create_and_Confirm_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-woopay-create-and-confirm-setup-intention.md b/includes/core/server/request/class-woopay-create-and-confirm-setup-intention.md
index 04904419e1a..0e0ca45cee9 100644
--- a/includes/core/server/request/class-woopay-create-and-confirm-setup-intention.md
+++ b/includes/core/server/request/class-woopay-create-and-confirm-setup-intention.md
@@ -1,6 +1,6 @@
# `WooPay_Create_and_Confirm_Setup_Intention` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/core/server/request/class-woopay-create-intent.md b/includes/core/server/request/class-woopay-create-intent.md
index 0ae42ef421f..6e4d4314a44 100644
--- a/includes/core/server/request/class-woopay-create-intent.md
+++ b/includes/core/server/request/class-woopay-create-intent.md
@@ -1,6 +1,6 @@
# `WooPay_Create_Intent` request class
-[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../requests.md)
+[ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md)
## Description
diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php
index 63559005498..7e04124c6cf 100644
--- a/includes/woopay/class-woopay-session.php
+++ b/includes/woopay/class-woopay-session.php
@@ -47,6 +47,9 @@ class WooPay_Session {
'@^\/wc\/store(\/v[\d]+)?\/checkout\/(?P
[\d]+)@',
'@^\/wc\/store(\/v[\d]+)?\/checkout$@',
'@^\/wc\/store(\/v[\d]+)?\/order\/(?P[\d]+)@',
+ // The route below is not a Store API route. However, this REST endpoint is used by WooPay to indirectly reach the Store API.
+ // By adding it to this list, we're able to identify the user and load the correct session for this route.
+ '@^\/wc\/v3\/woopay\/session$@',
];
/**
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 17038230df6..b358e46babb 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -100,4 +100,11 @@
$stripe_billing_migrator
+
+
+ wcs_get_subscriptions_for_order( $order )
+ wcs_is_manual_renewal_required()
+ wcs_order_contains_subscription( $order_id )
+
+
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 e53b8383e8c..c0aded3df5e 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
@@ -44,6 +44,11 @@ class WC_REST_Payments_Orders_Controller_Test extends WCPAY_UnitTestCase {
*/
private $order_service;
+ /**
+ * @var WC_Payments_Token_Service|MockObject
+ */
+ private $mock_token_service;
+
/**
* @var string
*/
@@ -68,6 +73,7 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class );
$this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class );
+ $this->mock_token_service = $this->createMock( WC_Payments_Token_Service::class );
$this->order_service = $this->getMockBuilder( 'WC_Payments_Order_Service' )
->setConstructorArgs( [ $this->mock_api_client ] )
->setMethods( [ 'attach_intent_info_to_order' ] )
@@ -77,7 +83,8 @@ public function set_up() {
$this->mock_api_client,
$this->mock_gateway,
$this->mock_customer_service,
- $this->order_service
+ $this->order_service,
+ $this->mock_token_service
);
}
@@ -868,9 +875,6 @@ public function test_capture_authorization_not_found() {
$this->assertSame( 404, $data['status'] );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_invalid_order_id() {
$request = new WP_REST_Request( 'POST' );
$request->set_body_params(
@@ -887,9 +891,6 @@ public function test_create_customer_invalid_order_id() {
$this->assertEquals( 404, $data['status'] );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_guest_without_customer_id() {
$order = WC_Helper_Order::create_order( 0 );
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -956,9 +957,6 @@ function ( $argument ) {
);
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_guest_with_customer_id() {
$order = WC_Helper_Order::create_order( 0 );
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -1009,9 +1007,6 @@ function ( $argument ) {
$this->assertSame( 'cus_guest', $result_order->get_meta( '_stripe_customer_id' ) );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_non_guest_with_customer_id() {
$order = WC_Helper_Order::create_order();
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -1053,9 +1048,6 @@ public function test_create_customer_from_order_non_guest_with_customer_id() {
$this->assertSame( 'cus_exist', $result_order->get_meta( '_stripe_customer_id' ) );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_with_invalid_status() {
$order = WC_Helper_Order::create_order();
$order->set_status( Order_Status::COMPLETED );
@@ -1088,9 +1080,6 @@ public function test_create_customer_from_order_with_invalid_status() {
$this->assertEquals( 400, $data['status'] );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_non_guest_with_customer_id_from_order_meta() {
$order = WC_Helper_Order::create_order();
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -1133,9 +1122,6 @@ public function test_create_customer_from_order_non_guest_with_customer_id_from_
$this->assertSame( 'cus_exist', $result_order->get_meta( '_stripe_customer_id' ) );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_non_guest_without_customer_id() {
$order = WC_Helper_Order::create_order();
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -1737,4 +1723,268 @@ private function create_charge_object() {
return new WC_Payments_API_Charge( $this->mock_charge_id, 1500, $created );
}
+
+ public function test_capture_terminal_payment_with_subscription_product_sets_generated_card_on_user() {
+ $order = $this->create_mock_order();
+
+ $subscription = new WC_Subscription();
+ $subscription->set_parent( $order );
+ $this->mock_wcs_order_contains_subscription( true );
+ $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] );
+ $this->mock_wcs_is_manual_renewal_required( false );
+
+ $generated_card_id = 'pm_generatedCardId';
+
+ $mock_intent = WC_Helper_Intention::create_intention(
+ [
+ 'charge' => [
+ 'payment_method_details' => [
+ 'type' => 'card_present',
+ 'card_present' => [
+ 'generated_card' => $generated_card_id,
+ ],
+ ],
+ ],
+ 'metadata' => [
+ 'order_id' => $order->get_id(),
+ ],
+ 'status' => Intent_Status::REQUIRES_CAPTURE,
+ ]
+ );
+
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id );
+
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $mock_intent );
+
+ $this->mock_gateway
+ ->expects( $this->once() )
+ ->method( 'capture_charge' )
+ ->with( $this->isInstanceOf( WC_Order::class ) )
+ ->willReturn(
+ [
+ 'status' => Intent_Status::SUCCEEDED,
+ 'id' => $this->mock_intent_id,
+ ]
+ );
+
+ $this->order_service
+ ->expects( $this->once() )
+ ->method( 'attach_intent_info_to_order' )
+ ->with(
+ $this->isInstanceOf( WC_Order::class ),
+ $mock_intent,
+ );
+
+ $this->mock_token_service
+ ->expects( $this->once() )
+ ->method( 'add_payment_method_to_user' )
+ ->with(
+ $generated_card_id,
+ $this->isInstanceOf( WP_User::class )
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $order->get_id(),
+ 'payment_intent_id' => $this->mock_intent_id,
+ ]
+ );
+
+ $response = $this->controller->capture_terminal_payment( $request );
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEquals( 'woocommerce_payments', $subscription->get_payment_method() );
+ }
+
+ /**
+ * @dataProvider provider_capture_terminal_payment_with_subscription_product_sets_manual_renewal
+ */
+ public function test_capture_terminal_payment_with_subscription_product_sets_manual_renewal( bool $manual_renewal_required_setting, bool $initial_subscription_manual_renewal, bool $expected_subscription_manual_renewal ) {
+ $order = $this->create_mock_order();
+
+ $subscription = new WC_Subscription();
+ $subscription->set_parent( $order );
+ $subscription->set_requires_manual_renewal( $initial_subscription_manual_renewal );
+ $this->mock_wcs_order_contains_subscription( true );
+ $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] );
+ $this->mock_wcs_is_manual_renewal_required( $manual_renewal_required_setting );
+
+ $generated_card_id = 'pm_generatedCardId';
+
+ $mock_intent = WC_Helper_Intention::create_intention(
+ [
+ 'charge' => [
+ 'payment_method_details' => [
+ 'type' => 'card_present',
+ 'card_present' => [
+ 'generated_card' => $generated_card_id,
+ ],
+ ],
+ ],
+ 'metadata' => [
+ 'order_id' => $order->get_id(),
+ ],
+ 'status' => Intent_Status::REQUIRES_CAPTURE,
+ ]
+ );
+
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id );
+
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $mock_intent );
+
+ $this->mock_gateway
+ ->expects( $this->once() )
+ ->method( 'capture_charge' )
+ ->with( $this->isInstanceOf( WC_Order::class ) )
+ ->willReturn(
+ [
+ 'status' => Intent_Status::SUCCEEDED,
+ 'id' => $this->mock_intent_id,
+ ]
+ );
+
+ $this->order_service
+ ->expects( $this->once() )
+ ->method( 'attach_intent_info_to_order' )
+ ->with(
+ $this->isInstanceOf( WC_Order::class ),
+ $mock_intent,
+ );
+
+ $this->mock_token_service
+ ->expects( $this->once() )
+ ->method( 'add_payment_method_to_user' )
+ ->with(
+ $generated_card_id,
+ $this->isInstanceOf( WP_User::class )
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $order->get_id(),
+ 'payment_intent_id' => $this->mock_intent_id,
+ ]
+ );
+
+ $response = $this->controller->capture_terminal_payment( $request );
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEquals( $expected_subscription_manual_renewal, $subscription->is_manual() );
+ }
+
+ /**
+ * bool $manual_renewal_required_setting
+ * bool $initial_subscription_manual_renewal
+ * bool $expected_subscription_manual_renewal
+ */
+ public function provider_capture_terminal_payment_with_subscription_product_sets_manual_renewal(): array {
+ return [
+ [ true, true, true ],
+ [ false, true, false ],
+ [ true, false, false ], // even if manual_renewal_required, we won't set it to manual_renewal if it started as automatic.
+ [ false, false, false ],
+ ];
+ }
+
+ /**
+ * Cleanup after all tests.
+ */
+ public static function tear_down_after_class() {
+ WC_Subscriptions::set_wcs_order_contains_subscription( null );
+ WC_Subscriptions::set_wcs_get_subscriptions_for_order( null );
+ WC_Subscriptions::set_wcs_is_manual_renewal_required( null );
+ parent::tear_down_after_class();
+ }
+
+ private function mock_wcs_order_contains_subscription( $value ) {
+ WC_Subscriptions::set_wcs_order_contains_subscription(
+ function ( $order ) use ( $value ) {
+ return $value;
+ }
+ );
+ }
+
+ private function mock_wcs_get_subscriptions_for_order( $value ) {
+ WC_Subscriptions::set_wcs_get_subscriptions_for_order(
+ function ( $order ) use ( $value ) {
+ return $value;
+ }
+ );
+ }
+
+ private function mock_wcs_is_manual_renewal_required( $value ) {
+ WC_Subscriptions::set_wcs_is_manual_renewal_required(
+ function () use ( $value ) {
+ return $value;
+ }
+ );
+ }
+
+ public function test_capture_terminal_payment_with_subscription_product_returns_success_even_if_no_generated_card() {
+ $order = $this->create_mock_order();
+
+ $subscription = new WC_Subscription();
+ $subscription->set_parent( $order );
+ $this->mock_wcs_order_contains_subscription( true );
+ $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] );
+
+ $mock_intent = WC_Helper_Intention::create_intention(
+ [
+ 'charge' => [
+ 'payment_method_details' => [
+ 'type' => 'card_present',
+ 'card_present' => [],
+ ],
+ ],
+ 'metadata' => [
+ 'order_id' => $order->get_id(),
+ ],
+ 'status' => Intent_Status::REQUIRES_CAPTURE,
+ ]
+ );
+
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id );
+
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $mock_intent );
+
+ $this->mock_gateway
+ ->expects( $this->once() )
+ ->method( 'capture_charge' )
+ ->with( $this->isInstanceOf( WC_Order::class ) )
+ ->willReturn(
+ [
+ 'status' => Intent_Status::SUCCEEDED,
+ 'id' => $this->mock_intent_id,
+ ]
+ );
+
+ $this->order_service
+ ->expects( $this->once() )
+ ->method( 'attach_intent_info_to_order' )
+ ->with(
+ $this->isInstanceOf( WC_Order::class ),
+ $mock_intent,
+ );
+
+ $this->mock_token_service
+ ->expects( $this->never() )
+ ->method( 'add_payment_method_to_user' );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $order->get_id(),
+ 'payment_intent_id' => $this->mock_intent_id,
+ ]
+ );
+
+ $response = $this->controller->capture_terminal_payment( $request );
+ $this->assertSame( 200, $response->status );
+ }
}
diff --git a/tests/unit/helpers/class-wc-helper-subscriptions.php b/tests/unit/helpers/class-wc-helper-subscriptions.php
index 3d361a6446d..a2c99a77071 100644
--- a/tests/unit/helpers/class-wc-helper-subscriptions.php
+++ b/tests/unit/helpers/class-wc-helper-subscriptions.php
@@ -90,6 +90,13 @@ function wcs_get_orders_with_meta_query( $args ) {
return ( WC_Subscriptions::$wcs_get_orders_with_meta_query )( $args );
}
+function wcs_is_manual_renewal_required() {
+ if ( ! WC_Subscriptions::$wcs_is_manual_renewal_required ) {
+ return;
+ }
+ return ( WC_Subscriptions::$wcs_is_manual_renewal_required )();
+}
+
/**
* Class WC_Subscriptions.
*
@@ -187,6 +194,13 @@ class WC_Subscriptions {
*/
public static $wcs_order_contains_renewal = null;
+ /**
+ * wcs_is_manual_renewal_required mock.
+ *
+ * @var function
+ */
+ public static $wcs_is_manual_renewal_required = null;
+
public static function set_wcs_order_contains_subscription( $function ) {
self::$wcs_order_contains_subscription = $function;
}
@@ -234,4 +248,8 @@ public static function wcs_order_contains_renewal( $function ) {
public static function is_duplicate_site() {
return false;
}
+
+ public static function set_wcs_is_manual_renewal_required( $function ) {
+ self::$wcs_is_manual_renewal_required = $function;
+ }
}