diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml
index 12439ae65b1..28aeab14f4b 100644
--- a/.github/workflows/compatibility.yml
+++ b/.github/workflows/compatibility.yml
@@ -64,7 +64,7 @@ jobs:
- name: "Generate matrix"
id: generate_matrix
run: |
- PHP_VERSIONS=$( echo "[\"$PHP_MIN_SUPPORTED_VERSION\", \"8.0\", \"8.1\"]" )
+ PHP_VERSIONS=$( echo "[\"7.4\", \"8.0\", \"8.1\"]" )
echo "matrix={\"woocommerce\":[\"beta\"],\"wordpress\":[\"latest\"],\"gutenberg\":[\"latest\"],\"php\":$PHP_VERSIONS}" >> $GITHUB_OUTPUT
# a dedicated job, as allowed to fail
diff --git a/assets/css/admin.css b/assets/css/admin.css
index 8b17d53e766..28ff0430c73 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -128,6 +128,10 @@
background-image: url( '../images/payment-methods/affirm-icon.svg' );
}
+.payment-method__brand--klarna {
+ background-image: url( '../images/payment-methods/klarna.svg' );
+}
+
.wc_gateways tr[data-gateway_id='woocommerce_payments'] .payment-method__icon {
border: 1px solid #ddd;
border-radius: 2px;
diff --git a/assets/images/payment-methods/klarna.svg b/assets/images/payment-methods/klarna.svg
new file mode 100644
index 00000000000..948f281a04b
--- /dev/null
+++ b/assets/images/payment-methods/klarna.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/changelog.txt b/changelog.txt
index 760c80618f3..655ddaebbba 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,83 @@
*** WooPayments Changelog ***
+= 6.6.0 - 2023-10-11 =
+* Add - Add a notice on the Settings page to request JCB capability for Japanese customers.
+* Add - Add current user data to the onboarding init request payload. This data is used for fraud prevention.
+* Add - Added API endpoint to fetch customer's saved payment methods.
+* Add - Added docs for cancel_authorization endpoint
+* Add - Added documentation for create payment intent API endpoint.
+* Add - Added documentation for payment methods API endpoint
+* Add - Add functionality to enable WooPay first party auth behind feature flag.
+* Add - Add helper function/method for raw currency amount conversion.
+* Add - Add Klarna payment method
+* Add - Add loading state to WooPay button
+* Add - Add payment intent creation endpoint
+* Add - Add the feature flag check for pay-for-order flow
+* Add - Add WC blocks spinner to the WooPay checkout styles.
+* Add - Behind a feature flag: dispute message added to transactions screen for disputes not needing a response.
+* Add - Display dispute information, recommended resolution steps, and actions directly on the transaction details screen to help merchants with dispute resolution.
+* Add - Display server error messages on Settings save
+* Add - Expand the data points added to the WooCommerce SSR to include all the main WooPayments features.
+* Add - Handle server-side feature flag for new UPE type enablement.
+* Add - Introduce the "Subscription Relationship" column under the Orders list admin page when HPOS is enabled.
+* Add - Show survey for merchants that disable WooPay.
+* Fix - Add Mix and Match Products support on WooPay.
+* Fix - Add multi-currency enablement check in WooPay session handling.
+* Fix - Comment: Behind a feature flag: Update documentation links (new/changed docs content) when notifying merchant that a dispute needs response.
+* Fix - Disable automatic currency switching and switcher widgets on pay_for_order page.
+* Fix - Ensure renewal orders paid via the Block Checkout are correctly linked to their subscription.
+* Fix - Ensure the order needs processing transient is deleted when a subscription order (eg renewal) is created. Fixes issues with renewal orders going straight to a completed status.
+* Fix - fix: save platform checkout info on blocks
+* Fix - Fix Apple Pay and Google Pay if card payments are disabled.
+* Fix - Fix error when disabling WCPay with core disabled.
+* Fix - Fix init WooPay and empty cart error
+* Fix - Fix modal header alignment on safari browser
+* Fix - Fix onboarding section on MultiCurrency settings page.
+* Fix - Fix WooPay express checkout button with product bundles on product page.
+* Fix - Hide tooltip related to Storefront theme in Multi-Currency settings when Storefront is not the active theme
+* Fix - Improved product details script with enhanced price calculation, and fallbacks for potential undefined values.
+* Fix - Improve escaping around attributes.
+* Fix - Load multi-currency class on setup page.
+* Fix - Missing styles on the Edit Subscription page when HPOS is enabled.
+* Fix - Only request WooPay session data once on blocks pages.
+* Fix - Payment method section missing for Affirm and Afterpay on transaction details page
+* Fix - Prevent charging completed or processing orders with a new payment intent ID
+* Fix - Prevent WooPay-related implementation to modify non-WooPay-specific webhooks by changing their data.
+* Fix - Prevent WooPay multiple redirect requests.
+* Fix - Redirect back to the connect page when attempting to access the new onboarding flow without a server connection.
+* Fix - Redirect back to the pay-for-order page when it's pay-for-order order
+* Fix - Resolved an issue that caused paying for failed/pending parent orders that include Product Add-ons to not calculate the correct total.
+* Fix - Speed up capturing terminal and authorized payments.
+* Fix - Store the correct subscription start date in postmeta and ordermeta when HPOS and data syncing is being used.
+* Fix - Tracking conditions
+* Fix - Virtual variable products no longer require shipping details when checking out with Apple Pay and Google Pay
+* Fix - When HPOS is enabled, deleting a customer will now delete their subscriptions.
+* Fix - When HPOS is enabled, make the orders_by_type_query filter box work in the WooCommerce orders screen.
+* Fix - WooPay save my info phone number fallback for virtual products
+* Update - Adapt the PO congratulations card copy for pending account status.
+* Update - Allow deferred intent creation UPE to support SEPA payments.
+* Update - Enhance design of bnpl payment methods status in settings screen
+* Update - Increase GBP transaction limit for Afterpay
+* Update - Only display the WCPay Subscriptions setting to existing users as part of deprecating this feature.
+* Update - Set WooPay First Party Authentication feature flag to default on.
+* Update - Store customer currencies as an option to avoid expensive calculation.
+* Update - Updated Transaction Details summary with added fee breakdown tooltip for disputed transactions.
+* Update - Update links that pointed to the dispute details screen to point to the transaction details screen
+* Update - Update Name Your Price compatibility to use new Compatibility methods.
+* Update - Update the content of modals that are displayed when deactivating the WooPayments or Woo Subscriptions plugins when the store has active Stripe Billing subscriptions.
+* Update - Update URL used to communicate with WooPay from the iFrame in the merchant site.
+* Dev - Added missing API docs links for payment intents and payment methods API endpoints
+* Dev - Capitalize the JCB label on transactions details page.
+* Dev - e2e tests for progressive onboarding
+* Dev - Extracting payment metadata and level 3 data generation into services.
+* Dev - Migrate away from hooking into actions in certain classes
+* Dev - Move fraud related service hooks out of class constructors and into new init_hooks methods.
+* Dev - Move hooks out of MultiCurrency constructor into own init_hooks method.
+* Dev - Refactored request class send() method
+* Dev - Refactor to move hook initialisation out of constructors.
+* Dev - This work is part of a UI improvements to increase disputes response that is behind a feature flag. A changelog entry will be added to represent the work as a whole.
+* Dev - Update subscriptions-core to 6.3.0.
+
= 6.5.1 - 2023-09-26 =
* Fix - fix incorrect payment method title for non-WooPayments gateways
diff --git a/client/additional-methods-setup/constants.js b/client/additional-methods-setup/constants.js
index 4f2222ea780..41747020d95 100644
--- a/client/additional-methods-setup/constants.js
+++ b/client/additional-methods-setup/constants.js
@@ -10,6 +10,7 @@ export const upeMethods = [
'affirm',
'afterpay_clearpay',
'jcb',
+ 'klarna',
];
export const upeCapabilityStatuses = {
diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js
index d53f7170b68..b7deafca8af 100644
--- a/client/checkout/api/index.js
+++ b/client/checkout/api/index.js
@@ -26,6 +26,7 @@ export default class WCPayAPI {
this.stripe = null;
this.stripePlatform = null;
this.request = request;
+ this.isWooPayRequesting = false;
}
createStripe( publishableKey, locale, accountId = '', betas = [] ) {
@@ -688,17 +689,21 @@ export default class WCPayAPI {
}
initWooPay( userEmail, woopayUserSession ) {
- const wcAjaxUrl = getConfig( 'wcAjaxUrl' );
- const nonce = getConfig( 'initWooPayNonce' );
-
- return this.request( buildAjaxURL( wcAjaxUrl, 'init_woopay' ), {
- _wpnonce: nonce,
- email: userEmail,
- user_session: woopayUserSession,
- order_id: getConfig( 'order_id' ),
- key: getConfig( 'key' ),
- billing_email: getConfig( 'billing_email' ),
- } );
+ if ( ! this.isWooPayRequesting ) {
+ this.isWooPayRequesting = true;
+ const wcAjaxUrl = getConfig( 'wcAjaxUrl' );
+ const nonce = getConfig( 'initWooPayNonce' );
+ return this.request( buildAjaxURL( wcAjaxUrl, 'init_woopay' ), {
+ _wpnonce: nonce,
+ email: userEmail,
+ user_session: woopayUserSession,
+ order_id: getConfig( 'order_id' ),
+ key: getConfig( 'key' ),
+ billing_email: getConfig( 'billing_email' ),
+ } ).finally( () => {
+ this.isWooPayRequesting = false;
+ } );
+ }
}
expressCheckoutAddToCart( productData ) {
diff --git a/client/checkout/api/test/index.test.js b/client/checkout/api/test/index.test.js
index 5a2711a4da0..3ed3cf22284 100644
--- a/client/checkout/api/test/index.test.js
+++ b/client/checkout/api/test/index.test.js
@@ -6,7 +6,9 @@ import request from 'wcpay/checkout/utils/request';
import { buildAjaxURL } from 'wcpay/payment-request/utils';
import { getConfig } from 'wcpay/utils/checkout';
-jest.mock( 'wcpay/checkout/utils/request', () => jest.fn() );
+jest.mock( 'wcpay/checkout/utils/request', () =>
+ jest.fn( () => Promise.resolve( {} ).finally( () => {} ) )
+);
jest.mock( 'wcpay/payment-request/utils', () => ( {
buildAjaxURL: jest.fn(),
} ) );
@@ -15,7 +17,7 @@ jest.mock( 'wcpay/utils/checkout', () => ( {
} ) );
describe( 'WCPayAPI', () => {
- test( 'initializes woopay using config params', () => {
+ test( 'does not initialize woopay if already requesting', async () => {
buildAjaxURL.mockReturnValue( 'https://example.org/' );
getConfig.mockImplementation( ( key ) => {
const mockProperties = {
@@ -28,7 +30,27 @@ describe( 'WCPayAPI', () => {
} );
const api = new WCPayAPI( {}, request );
- api.initWooPay( 'foo@bar.com', 'qwerty123' );
+ api.isWooPayRequesting = true;
+ await api.initWooPay( 'foo@bar.com', 'qwerty123' );
+
+ expect( request ).not.toHaveBeenCalled();
+ expect( api.isWooPayRequesting ).toBe( true );
+ } );
+
+ test( 'initializes woopay using config params', async () => {
+ buildAjaxURL.mockReturnValue( 'https://example.org/' );
+ getConfig.mockImplementation( ( key ) => {
+ const mockProperties = {
+ initWooPayNonce: 'foo',
+ order_id: 1,
+ key: 'testkey',
+ billing_email: 'test@example.com',
+ };
+ return mockProperties[ key ];
+ } );
+
+ const api = new WCPayAPI( {}, request );
+ await api.initWooPay( 'foo@bar.com', 'qwerty123' );
expect( request ).toHaveBeenLastCalledWith( 'https://example.org/', {
_wpnonce: 'foo',
@@ -38,5 +60,6 @@ describe( 'WCPayAPI', () => {
key: 'testkey',
billing_email: 'test@example.com',
} );
+ expect( api.isWooPayRequesting ).toBe( false );
} );
} );
diff --git a/client/checkout/blocks/upe-split.js b/client/checkout/blocks/upe-split.js
index ab2463aadc7..6166bdf907a 100644
--- a/client/checkout/blocks/upe-split.js
+++ b/client/checkout/blocks/upe-split.js
@@ -30,6 +30,7 @@ import {
PAYMENT_METHOD_NAME_SOFORT,
PAYMENT_METHOD_NAME_AFFIRM,
PAYMENT_METHOD_NAME_AFTERPAY,
+ PAYMENT_METHOD_NAME_KLARNA,
} from '../constants.js';
import { getSplitUPEFields } from './upe-split-fields';
import { getDeferredIntentCreationUPEFields } from './upe-deferred-intent-creation/payment-elements';
@@ -46,6 +47,7 @@ const upeMethods = {
sofort: PAYMENT_METHOD_NAME_SOFORT,
affirm: PAYMENT_METHOD_NAME_AFFIRM,
afterpay_clearpay: PAYMENT_METHOD_NAME_AFTERPAY,
+ klarna: PAYMENT_METHOD_NAME_KLARNA,
};
const enabledPaymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' );
diff --git a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js
index c174a861ca3..e80b8bb4cf8 100644
--- a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js
+++ b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js
@@ -8,7 +8,9 @@ import {
generateCheckoutEventNames,
getSelectedUPEGatewayPaymentMethod,
isLinkEnabled,
+ isPaymentMethodRestrictedToLocation,
isUsingSavedPaymentMethod,
+ togglePaymentMethodForCountry,
} from '../../utils/upe';
import {
processPayment,
@@ -109,8 +111,18 @@ jQuery( function ( $ ) {
) {
for ( const upeElement of $( '.wcpay-upe-element' ).toArray() ) {
await mountStripePaymentElement( api, upeElement );
+ restrictPaymentMethodToLocation( upeElement );
}
maybeEnableStripeLink( api );
}
}
+
+ function restrictPaymentMethodToLocation( upeElement ) {
+ if ( isPaymentMethodRestrictedToLocation( upeElement ) ) {
+ togglePaymentMethodForCountry( upeElement );
+ $( '#billing_country' ).on( 'change', function () {
+ togglePaymentMethodForCountry( upeElement );
+ } );
+ }
+ }
} );
diff --git a/client/checkout/classic/upe-split.js b/client/checkout/classic/upe-split.js
index 87edf7333ff..05ad9f1de49 100644
--- a/client/checkout/classic/upe-split.js
+++ b/client/checkout/classic/upe-split.js
@@ -21,6 +21,7 @@ import {
PAYMENT_METHOD_NAME_SOFORT,
PAYMENT_METHOD_NAME_AFFIRM,
PAYMENT_METHOD_NAME_AFTERPAY,
+ PAYMENT_METHOD_NAME_KLARNA,
SHORTCODE_SHIPPING_ADDRESS_FIELDS,
SHORTCODE_BILLING_ADDRESS_FIELDS,
} from '../constants';
@@ -657,6 +658,7 @@ jQuery( function ( $ ) {
PAYMENT_METHOD_NAME_SOFORT,
PAYMENT_METHOD_NAME_AFFIRM,
PAYMENT_METHOD_NAME_AFTERPAY,
+ PAYMENT_METHOD_NAME_KLARNA,
paymentMethodsConfig.card !== undefined && PAYMENT_METHOD_NAME_CARD,
].filter( Boolean );
const checkoutEvents = wcpayPaymentMethods
diff --git a/client/checkout/constants.js b/client/checkout/constants.js
index 5b6c5468905..4f48eae96fd 100644
--- a/client/checkout/constants.js
+++ b/client/checkout/constants.js
@@ -10,6 +10,7 @@ export const PAYMENT_METHOD_NAME_SOFORT = 'woocommerce_payments_sofort';
export const PAYMENT_METHOD_NAME_AFFIRM = 'woocommerce_payments_affirm';
export const PAYMENT_METHOD_NAME_AFTERPAY =
'woocommerce_payments_afterpay_clearpay';
+export const PAYMENT_METHOD_NAME_KLARNA = 'woocommerce_payments_klarna';
export const PAYMENT_METHOD_NAME_UPE = 'woocommerce_payments_upe';
export const PAYMENT_METHOD_NAME_PAYMENT_REQUEST =
'woocommerce_payments_payment_request';
@@ -30,6 +31,7 @@ export function getPaymentMethodsConstants() {
PAYMENT_METHOD_NAME_AFFIRM,
PAYMENT_METHOD_NAME_AFTERPAY,
PAYMENT_METHOD_NAME_CARD,
+ PAYMENT_METHOD_NAME_KLARNA,
];
}
diff --git a/client/checkout/utils/upe.js b/client/checkout/utils/upe.js
index da11bc2bfd7..71a673d3a2a 100644
--- a/client/checkout/utils/upe.js
+++ b/client/checkout/utils/upe.js
@@ -421,3 +421,35 @@ export const getShippingDetails = ( fields ) => {
return billingAsShippingAddress;
};
+
+/**
+ * Hides payment method if it has set specific countries in the PHP class.
+ *
+ * @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
+ * @return {boolean} Whether the payment method is restricted to selected billing country.
+ **/
+export const isPaymentMethodRestrictedToLocation = ( upeElement ) => {
+ const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' );
+ const paymentMethodType = upeElement.dataset.paymentMethodType;
+ return !! paymentMethodsConfig[ paymentMethodType ].countries.length;
+};
+
+/**
+ * @param {Object} upeElement The selector of the DOM element of particular payment method to mount the UPE element to.
+ **/
+export const togglePaymentMethodForCountry = ( upeElement ) => {
+ const paymentMethodsConfig = getUPEConfig( 'paymentMethodsConfig' );
+ const paymentMethodType = upeElement.dataset.paymentMethodType;
+ const supportedCountries =
+ paymentMethodsConfig[ paymentMethodType ].countries;
+
+ const billingCountry = document.getElementById( 'billing_country' ).value;
+ const upeContainer = document.querySelector(
+ '.payment_method_woocommerce_payments_' + paymentMethodType
+ );
+ if ( supportedCountries.includes( billingCountry ) ) {
+ upeContainer.style.display = 'block';
+ } else {
+ upeContainer.style.display = 'none';
+ }
+};
diff --git a/client/checkout/woopay/express-button/express-checkout-iframe.js b/client/checkout/woopay/express-button/express-checkout-iframe.js
index b021e9eab15..b6080610310 100644
--- a/client/checkout/woopay/express-button/express-checkout-iframe.js
+++ b/client/checkout/woopay/express-button/express-checkout-iframe.js
@@ -2,8 +2,13 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
import { getConfig } from 'utils/checkout';
import request from 'wcpay/checkout/utils/request';
+import { showErrorMessage } from 'wcpay/checkout/woopay/express-button/utils';
import { buildAjaxURL } from 'wcpay/payment-request/utils';
import {
getTargetElement,
@@ -129,52 +134,6 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => {
// Add the iframe to the wrapper.
iframeWrapper.insertBefore( iframe, null );
- const showErrorMessage = () => {
- // Set the notice text.
- const errorMessage = __(
- 'WooPay is unavailable at this time. Sorry for the inconvenience.',
- 'woocommerce-payments'
- );
-
- // Handle Blocks Cart and Checkout notices.
- if ( wcSettings.wcBlocksConfig && context !== 'product' ) {
- // This handles adding the error notice to the cart page.
- wp.data
- .dispatch( 'core/notices' )
- ?.createNotice( 'error', errorMessage, {
- context: `wc/${ context }`,
- } );
- } else {
- // We're either on a shortcode cart/checkout or single product page.
- fetch( getConfig( 'ajaxUrl' ), {
- method: 'POST',
- body: new URLSearchParams( {
- action: 'woopay_express_checkout_button_show_error_notice',
- _ajax_nonce: getConfig( 'woopayButtonNonce' ),
- context,
- message: errorMessage,
- } ),
- } )
- .then( ( response ) => response.json() )
- .then( ( response ) => {
- if ( response.success ) {
- // We need to manually add the notice to the page.
- const noticesWrapper = document.querySelector(
- '.woocommerce-notices-wrapper'
- );
- const wrapper = document.createElement( 'div' );
- wrapper.innerHTML = response.data.notice;
- noticesWrapper.insertBefore( wrapper, null );
-
- noticesWrapper.scrollIntoView( {
- behavior: 'smooth',
- block: 'center',
- } );
- }
- } );
- }
- };
-
const closeIframe = () => {
window.removeEventListener( 'resize', getWindowSize );
window.removeEventListener( 'resize', setPopoverPosition );
@@ -282,7 +241,12 @@ export const expressCheckoutIframe = async ( api, context, emailSelector ) => {
response.url
);
} else {
- showErrorMessage();
+ // Set the notice text.
+ const errorMessage = __(
+ 'WooPay is unavailable at this time. Sorry for the inconvenience.',
+ 'woocommerce-payments'
+ );
+ showErrorMessage( context, errorMessage );
closeIframe( false );
}
} );
diff --git a/client/checkout/woopay/express-button/index.js b/client/checkout/woopay/express-button/index.js
index 8db44bea906..bd2aeff02c2 100644
--- a/client/checkout/woopay/express-button/index.js
+++ b/client/checkout/woopay/express-button/index.js
@@ -13,7 +13,9 @@ import WCPayAPI from '../../api';
import request from '../../utils/request';
import '../../express-checkout-buttons.scss';
-const renderWooPayExpressCheckoutButton = () => {
+const oldWoopayContainers = [];
+
+const renderWooPayExpressCheckoutButton = ( listenForCartChanges = {} ) => {
// Create an API object, which will be used throughout the checkout.
const api = new WCPayAPI(
{
@@ -28,8 +30,17 @@ const renderWooPayExpressCheckoutButton = () => {
const woopayContainer = document.getElementById( 'wcpay-woopay-button' );
if ( woopayContainer ) {
+ while ( oldWoopayContainers.length > 0 ) {
+ // Ensure previous buttons are unmounted and cleaned up.
+ const oldWoopayContainer = oldWoopayContainers.pop();
+ ReactDOM.unmountComponentAtNode( oldWoopayContainer );
+ }
+
+ oldWoopayContainers.push( woopayContainer );
+
ReactDOM.render(
{
}
};
-window.addEventListener( 'load', renderWooPayExpressCheckoutButton );
+let listenForCartChanges = null;
+const renderWooPayExpressCheckoutButtonWithCallbacks = () => {
+ renderWooPayExpressCheckoutButton( listenForCartChanges );
+};
jQuery( ( $ ) => {
- $( document.body ).on( 'updated_cart_totals', () => {
- renderWooPayExpressCheckoutButton();
- } );
+ listenForCartChanges = {
+ start: () => {
+ $( document.body ).on(
+ 'updated_cart_totals',
+ renderWooPayExpressCheckoutButtonWithCallbacks
+ );
+ },
+ stop: () => {
+ $( document.body ).off(
+ 'updated_cart_totals',
+ renderWooPayExpressCheckoutButtonWithCallbacks
+ );
+ },
+ };
+
+ listenForCartChanges.start();
} );
+
+window.addEventListener(
+ 'load',
+ renderWooPayExpressCheckoutButtonWithCallbacks
+);
diff --git a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js
index 7b67433e342..457b1dabb4b 100644
--- a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js
+++ b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js
@@ -29,6 +29,13 @@ jest.mock( 'tracks', () => ( {
jest.mock( '../use-express-checkout-product-handler', () => jest.fn() );
+jest.spyOn( window, 'alert' ).mockImplementation( () => {} );
+
+global.fetch = jest.fn( () => Promise.resolve( { json: () => ( {} ) } ) );
+global.window.wc_add_to_cart_variation_params = {
+ i18n_make_a_selection_text: 'Mock text',
+};
+
describe( 'WoopayExpressCheckoutButton', () => {
const buttonSettings = {
type: 'default',
@@ -74,6 +81,9 @@ describe( 'WoopayExpressCheckoutButton', () => {
} );
test( 'call `expressCheckoutIframe` on button click when `isPreview` is false', () => {
+ getConfig.mockImplementation( ( v ) => {
+ return v === 'isWoopayFirstPartyAuthEnabled' ? false : 'foo';
+ } );
render(
{
} );
describe( 'Product Page', () => {
- test( 'should enable the button when add to cart button is enabled', () => {
- render(
-
- );
-
- const expressButton = screen.queryByRole( 'button', {
- name: 'WooPay',
+ test( 'should shown an alert when clicking the button when add to cart button is disabled', () => {
+ getConfig.mockImplementation( ( v ) => {
+ return v === 'isWoopayFirstPartyAuthEnabled' ? false : 'foo';
} );
- expect( expressButton ).toBeEnabled();
- } );
-
- test( 'should disable the button when add to cart button is disabled', () => {
useExpressCheckoutProductHandler.mockImplementation( () => ( {
addToCart: mockAddToCart,
isAddToCartDisabled: true,
@@ -152,10 +148,19 @@ describe( 'WoopayExpressCheckoutButton', () => {
const expressButton = screen.queryByRole( 'button', {
name: 'WooPay',
} );
- expect( expressButton ).toBeDisabled();
+
+ userEvent.click( expressButton );
+
+ expect( window.alert ).toBeCalledWith(
+ window.wc_add_to_cart_variation_params
+ .i18n_make_a_selection_text
+ );
} );
test( 'call `addToCart` and `expressCheckoutIframe` on express button click on product page', async () => {
+ getConfig.mockImplementation( ( v ) => {
+ return v === 'isWoopayFirstPartyAuthEnabled' ? false : 'foo';
+ } );
useExpressCheckoutProductHandler.mockImplementation( () => ( {
addToCart: mockAddToCart,
getProductData: jest.fn().mockReturnValue( {} ),
@@ -189,6 +194,9 @@ describe( 'WoopayExpressCheckoutButton', () => {
} );
test( 'do not call `addToCart` on express button click on product page when validation fails', async () => {
+ getConfig.mockImplementation( ( v ) => {
+ return v === 'isWoopayFirstPartyAuthEnabled' ? false : 'foo';
+ } );
useExpressCheckoutProductHandler.mockImplementation( () => ( {
addToCart: mockAddToCart,
getProductData: jest.fn().mockReturnValue( false ),
diff --git a/client/checkout/woopay/express-button/use-express-checkout-product-handler.js b/client/checkout/woopay/express-button/use-express-checkout-product-handler.js
index f9a5bb842b0..2e106c15925 100644
--- a/client/checkout/woopay/express-button/use-express-checkout-product-handler.js
+++ b/client/checkout/woopay/express-button/use-express-checkout-product-handler.js
@@ -64,24 +64,48 @@ const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => {
};
const getProductData = () => {
- let productId = document.querySelector( '.single_add_to_cart_button' )
+ const productId = document.querySelector( '.single_add_to_cart_button' )
.value;
+ // Check if product is a bundle product.
+ const bundleForm = document.querySelector( '.bundle_form' );
// Check if product is a variable product.
const variation = document.querySelector( '.single_variation_wrap' );
- if ( variation ) {
- productId = variation.querySelector( 'input[name="product_id"]' )
- .value;
- }
- const data = {
+ let data = {
product_id: productId,
- qty: document.querySelector( '.quantity .qty' ).value,
- attributes: document.querySelector( '.variations_form' )
- ? getAttributes()
- : [],
+ quantity: document.querySelector( '.quantity .qty' ).value,
};
+ if ( variation && ! bundleForm ) {
+ data.product_id = variation.querySelector(
+ 'input[name="product_id"]'
+ ).value;
+ data.attributes = document.querySelector( '.variations_form' )
+ ? getAttributes()
+ : [];
+ } else {
+ const formData = new FormData(
+ document.querySelector( 'form.cart' )
+ );
+
+ // Remove add-to-cart attribute to prevent redirection
+ // when "Redirect to the cart page after successful addition"
+ // option is enabled.
+ formData.delete( 'add-to-cart' );
+
+ const attributes = {};
+
+ for ( const fields of formData.entries() ) {
+ attributes[ fields[ 0 ] ] = fields[ 1 ];
+ }
+
+ data = {
+ ...data,
+ ...attributes,
+ };
+ }
+
const addOnForm = document.querySelector( 'form.cart' );
if ( addOnForm ) {
@@ -133,31 +157,80 @@ const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => {
addToCartButton.classList.contains( 'disabled' )
);
};
+
setIsAddToCartDisabled( getIsAddToCartDisabled() );
- const onVariationChange = () =>
- setIsAddToCartDisabled( getIsAddToCartDisabled() );
+ const enableAddToCartButton = () => {
+ setIsAddToCartDisabled( false );
+ };
- const variationList = document.querySelector( '.variations_form' );
+ const disableAddToCartButton = () => {
+ setIsAddToCartDisabled( true );
+ };
- if ( variationList ) {
- variationList.addEventListener( 'change', onVariationChange );
+ const bundleForm = document.querySelector( '.bundle_form' );
+ const mixAndMatchForm = document.querySelector( '.mnm_form' );
+ const variationForm = document.querySelector( '.variations_form' );
+
+ if ( bundleForm ) {
+ // eslint-disable-next-line no-undef
+ jQuery( bundleForm )
+ .on( 'woocommerce-product-bundle-show', enableAddToCartButton )
+ .on(
+ 'woocommerce-product-bundle-hide',
+ disableAddToCartButton
+ );
+ } else if ( mixAndMatchForm ) {
+ // eslint-disable-next-line no-undef
+ jQuery( mixAndMatchForm )
+ .on(
+ 'wc-mnm-display-add-to-cart-button',
+ enableAddToCartButton
+ )
+ .on( 'wc-mnm-hide-add-to-cart-button', disableAddToCartButton );
+ } else if ( variationForm ) {
+ // eslint-disable-next-line no-undef
+ jQuery( variationForm )
+ .on( 'show_variation', enableAddToCartButton )
+ .on( 'hide_variation', disableAddToCartButton );
}
return () => {
- if ( variationList ) {
- variationList.removeEventListener(
- 'change',
- onVariationChange
- );
+ if ( bundleForm ) {
+ // eslint-disable-next-line no-undef
+ jQuery( bundleForm )
+ .off(
+ 'woocommerce-product-bundle-show',
+ enableAddToCartButton
+ )
+ .off(
+ 'woocommerce-product-bundle-hide',
+ disableAddToCartButton
+ );
+ } else if ( mixAndMatchForm ) {
+ // eslint-disable-next-line no-undef
+ jQuery( mixAndMatchForm )
+ .off(
+ 'wc-mnm-display-add-to-cart-button',
+ enableAddToCartButton
+ )
+ .off(
+ 'wc-mnm-hide-add-to-cart-button',
+ disableAddToCartButton
+ );
+ } else if ( variationForm ) {
+ // eslint-disable-next-line no-undef
+ jQuery( variationForm )
+ .off( 'show_variation', enableAddToCartButton )
+ .off( 'hide_variation', disableAddToCartButton );
}
};
}, [ isProductPage, setIsAddToCartDisabled ] );
return {
- addToCart: addToCart,
- getProductData: getProductData,
- isAddToCartDisabled: isAddToCartDisabled,
+ addToCart,
+ getProductData,
+ isAddToCartDisabled,
};
};
diff --git a/client/checkout/woopay/express-button/utils.js b/client/checkout/woopay/express-button/utils.js
new file mode 100644
index 00000000000..a4449d9d782
--- /dev/null
+++ b/client/checkout/woopay/express-button/utils.js
@@ -0,0 +1,50 @@
+/**
+ * Internal dependencies
+ */
+import { getConfig } from 'wcpay/utils/checkout';
+
+/**
+ * Show an error message to the user, from the WooPay express checkout button.
+ *
+ * @param {string} context The context for where the button is being displayed.
+ * @param {string} errorMessage The error message to display.
+ */
+export const showErrorMessage = ( context, errorMessage ) => {
+ // Handle Blocks Cart and Checkout notices.
+ if ( wcSettings.wcBlocksConfig && context !== 'product' ) {
+ // This handles adding the error notice to the cart page.
+ wp.data
+ .dispatch( 'core/notices' )
+ ?.createNotice( 'error', errorMessage, {
+ context: `wc/${ context }`,
+ } );
+ } else {
+ // We're either on a shortcode cart/checkout or single product page.
+ fetch( getConfig( 'ajaxUrl' ), {
+ method: 'POST',
+ body: new URLSearchParams( {
+ action: 'woopay_express_checkout_button_show_error_notice',
+ _ajax_nonce: getConfig( 'woopayButtonNonce' ),
+ context,
+ message: errorMessage,
+ } ),
+ } )
+ .then( ( response ) => response.json() )
+ .then( ( response ) => {
+ if ( response.success ) {
+ // We need to manually add the notice to the page.
+ const noticesWrapper = document.querySelector(
+ '.woocommerce-notices-wrapper'
+ );
+ const wrapper = document.createElement( 'div' );
+ wrapper.innerHTML = response.data.notice;
+ noticesWrapper.insertBefore( wrapper, null );
+
+ noticesWrapper.scrollIntoView( {
+ behavior: 'smooth',
+ block: 'center',
+ } );
+ }
+ } );
+ }
+};
diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js
index 76477a4e0b1..ea53795faf4 100644
--- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js
+++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js
@@ -2,7 +2,8 @@
* External dependencies
*/
import { sprintf, __ } from '@wordpress/i18n';
-import React, { useEffect, useState, useRef } from 'react';
+import React, { useCallback, useEffect, useState, useRef } from 'react';
+import classNames from 'classnames';
/**
* Internal dependencies
@@ -12,8 +13,15 @@ import WoopayIconLight from './woopay-icon-light';
import { expressCheckoutIframe } from './express-checkout-iframe';
import useExpressCheckoutProductHandler from './use-express-checkout-product-handler';
import wcpayTracks from 'tracks';
+import { getConfig } from 'wcpay/utils/checkout';
+import request from 'wcpay/checkout/utils/request';
+import { showErrorMessage } from 'wcpay/checkout/woopay/express-button/utils';
+import { buildAjaxURL } from 'wcpay/payment-request/utils';
+
+const BUTTON_WIDTH_THRESHOLD = 140;
export const WoopayExpressCheckoutButton = ( {
+ listenForCartChanges = {},
isPreview = false,
buttonSettings,
api,
@@ -24,8 +32,12 @@ export const WoopayExpressCheckoutButton = ( {
narrow: 'narrow',
wide: 'wide',
};
- const buttonRef = useRef();
+ const sessionDataPromiseRef = useRef( null );
+ const initWoopayRef = useRef( null );
+ const buttonRef = useRef( null );
+ const isLoadingRef = useRef( false );
const { type: buttonType, height, size, theme, context } = buttonSettings;
+ const [ isLoading, setIsLoading ] = useState( false );
const [ buttonWidthType, setButtonWidthType ] = useState(
buttonWidthTypes.wide
);
@@ -45,6 +57,8 @@ export const WoopayExpressCheckoutButton = ( {
getProductData,
isAddToCartDisabled,
} = useExpressCheckoutProductHandler( api, isProductPage );
+ const getProductDataRef = useRef( getProductData );
+ const addToCartRef = useRef( addToCart );
useEffect( () => {
if ( ! buttonRef.current ) {
@@ -52,7 +66,7 @@ export const WoopayExpressCheckoutButton = ( {
}
const buttonWidth = buttonRef.current.getBoundingClientRect().width;
- const isButtonWide = buttonWidth > 140;
+ const isButtonWide = buttonWidth > BUTTON_WIDTH_THRESHOLD;
setButtonWidthType(
isButtonWide ? buttonWidthTypes.wide : buttonWidthTypes.narrow
);
@@ -69,52 +83,292 @@ export const WoopayExpressCheckoutButton = ( {
}
}, [ isPreview, context ] );
- const initWooPay = ( e ) => {
- e.preventDefault();
+ const defaultOnClick = useCallback(
+ ( e ) => {
+ e.preventDefault();
+
+ if ( isPreview ) {
+ return; // eslint-disable-line no-useless-return
+ }
+
+ wcpayTracks.recordUserEvent(
+ wcpayTracks.events.WOOPAY_BUTTON_CLICK,
+ {
+ source: context,
+ }
+ );
+
+ if ( isProductPage ) {
+ if ( isAddToCartDisabled ) {
+ alert(
+ window.wc_add_to_cart_variation_params
+ ?.i18n_make_a_selection_text ||
+ __(
+ 'Please select all required options to continue.',
+ 'woocommerce-payments'
+ )
+ );
+ return;
+ }
+
+ const productData = getProductDataRef.current();
+ if ( ! productData ) {
+ return;
+ }
+
+ addToCartRef
+ .current( productData )
+ .then( ( res ) => {
+ if ( res.error ) {
+ if ( res.submit ) {
+ // Some extensions needs to submit the form
+ // to show error messages.
+ document.querySelector( 'form.cart' ).submit();
+ }
+ return;
+ }
- if ( isPreview ) {
- return; // eslint-disable-line no-useless-return
+ expressCheckoutIframe( api, context, emailSelector );
+ } )
+ .catch( () => {
+ // handle error.
+ } );
+ } else {
+ expressCheckoutIframe( api, context, emailSelector );
+ }
+ },
+ [
+ api,
+ context,
+ emailSelector,
+ isAddToCartDisabled,
+ isPreview,
+ isProductPage,
+ ]
+ );
+
+ const newIframe = useCallback( () => {
+ if ( ! getConfig( 'isWoopayFirstPartyAuthEnabled' ) ) {
+ return;
}
- wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_BUTTON_CLICK, {
- source: context,
+ const getWoopayOtpUrl = () => {
+ const tracksUserId = JSON.stringify(
+ getConfig( 'tracksUserIdentity' )
+ );
+
+ const urlParams = new URLSearchParams();
+ urlParams.append( 'testMode', getConfig( 'testMode' ) );
+ urlParams.append( 'source_url', window.location.href );
+ urlParams.append( 'tracksUserIdentity', tracksUserId );
+
+ return (
+ getConfig( 'woopayHost' ) + '/connect/?' + urlParams.toString()
+ );
+ };
+
+ const iframe = document.createElement( 'iframe' );
+ iframe.src = getWoopayOtpUrl();
+ iframe.height = 0;
+ iframe.style.visibility = 'hidden';
+ iframe.style.position = 'absolute';
+ iframe.style.top = '0';
+
+ iframe.addEventListener( 'load', () => {
+ // Change button's onClick handle to use express checkout flow.
+ initWoopayRef.current = ( e ) => {
+ e.preventDefault();
+
+ if ( isPreview || isLoadingRef.current ) {
+ return;
+ }
+
+ // Set isLoadingRef to true to prevent multiple clicks.
+ isLoadingRef.current = true;
+ setIsLoading( true );
+
+ wcpayTracks.recordUserEvent(
+ wcpayTracks.events.WOOPAY_BUTTON_CLICK,
+ {
+ source: context,
+ }
+ );
+
+ if ( isProductPage ) {
+ const productData = getProductDataRef.current();
+
+ if ( ! productData ) {
+ return;
+ }
+
+ if ( listenForCartChanges.stop ) {
+ // Temporarily stop listening for cart changes to prevent
+ // rendering a new button + iFrame when the cart is updated.
+ listenForCartChanges.stop();
+ }
+
+ addToCartRef.current( productData ).then( () => {
+ if ( listenForCartChanges.start ) {
+ // Start listening for cart changes, again.
+ listenForCartChanges.start();
+ }
+ request(
+ buildAjaxURL(
+ getConfig( 'wcAjaxUrl' ),
+ 'get_woopay_session'
+ ),
+ {
+ _ajax_nonce: getConfig( 'woopaySessionNonce' ),
+ }
+ )
+ .then( ( response ) => {
+ iframe.contentWindow.postMessage(
+ {
+ action: 'setPreemptiveSessionData',
+ value: response,
+ },
+ getConfig( 'woopayHost' )
+ );
+ } )
+ .catch( () => {
+ const errorMessage = __(
+ 'Something went wrong. Please try again.',
+ 'woocommerce-payments'
+ );
+ showErrorMessage( context, errorMessage );
+ isLoadingRef.current = false;
+ setIsLoading( false );
+ } );
+ } );
+ } else {
+ // Non-product pages already have pre-fetched session data.
+ sessionDataPromiseRef.current
+ ?.then( ( response ) => {
+ iframe.contentWindow.postMessage(
+ {
+ action: 'setPreemptiveSessionData',
+ value: response,
+ },
+ getConfig( 'woopayHost' )
+ );
+ } )
+ .catch( () => {
+ const errorMessage = __(
+ 'Something went wrong. Please try again.',
+ 'woocommerce-payments'
+ );
+ showErrorMessage( context, errorMessage );
+ isLoadingRef.current = false;
+ setIsLoading( false );
+ } );
+ }
+ };
} );
- if ( isProductPage ) {
- const productData = getProductData();
+ return iframe;
+ }, [ isProductPage, context, isPreview, listenForCartChanges ] );
+
+ useEffect( () => {
+ if ( isPreview || ! getConfig( 'isWoopayFirstPartyAuthEnabled' ) ) {
+ return;
+ }
+
+ if ( ! isProductPage ) {
+ // Start to pre-fetch session data for non-product pages.
+ sessionDataPromiseRef.current = request(
+ buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_session' ),
+ {
+ _ajax_nonce: getConfig( 'woopaySessionNonce' ),
+ }
+ ).then( ( response ) => response );
+ }
+
+ buttonRef.current.parentElement.style.position = 'relative';
+ buttonRef.current.parentElement.appendChild( newIframe() );
- if ( ! productData ) {
+ const onMessage = ( event ) => {
+ const isFromWoopayHost = getConfig( 'woopayHost' ).startsWith(
+ event.origin
+ );
+ const isSessionDataSuccess =
+ event.data.action === 'set_preemptive_session_data_success';
+ const isSessionDataError =
+ event.data.action === 'set_preemptive_session_data_error';
+ const isSessionDataResponse =
+ isSessionDataSuccess || isSessionDataError;
+ if ( ! isFromWoopayHost || ! isSessionDataResponse ) {
return;
}
- addToCart( productData )
- .then( () => {
- expressCheckoutIframe( api, context, emailSelector );
- } )
- .catch( () => {
- // handle error.
- } );
- } else {
- expressCheckoutIframe( api, context, emailSelector );
- }
- };
+ if ( isSessionDataSuccess ) {
+ window.location.href = event.data.value.redirect_url;
+ } else if ( isSessionDataError ) {
+ const errorMessage = __(
+ 'WooPay is unavailable at this time. Please try again.',
+ 'woocommerce-payments'
+ );
+ showErrorMessage( context, errorMessage );
+
+ // Set button's default onClick handle to use modal checkout flow.
+ initWoopayRef.current = defaultOnClick;
+ isLoadingRef.current = false;
+ setIsLoading( false );
+ }
+ };
+
+ window.addEventListener( 'message', onMessage );
+
+ return () => {
+ window.removeEventListener( 'message', onMessage );
+ };
+ // Note: Any changes to this dependency array may cause a duplicate iframe to be appended.
+ }, [ context, defaultOnClick, isPreview, isProductPage, newIframe ] );
+
+ useEffect( () => {
+ // Set button's default onClick handle to use modal checkout flow.
+ initWoopayRef.current = defaultOnClick;
+ }, [ defaultOnClick ] );
+
+ useEffect( () => {
+ const handlePageShow = ( event ) => {
+ // Re-enable the button after navigating back/forward to the page if bfcache is used.
+ if ( event?.persisted ) {
+ isLoadingRef.current = false;
+ setIsLoading( false );
+ }
+ };
+
+ window.addEventListener( 'pageshow', handlePageShow );
+
+ return () => {
+ window.removeEventListener( 'pageshow', handlePageShow );
+ };
+ }, [] );
return (
initWoopayRef.current( e ) }
+ className={ classNames( 'woopay-express-button', {
+ 'is-loading': isLoading,
+ } ) }
data-type={ buttonType }
data-size={ size }
data-theme={ theme }
data-width-type={ buttonWidthType }
style={ { height: `${ height }px` } }
+ disabled={ isLoading }
>
- { text }
-
+ { isLoading ? (
+
+ ) : (
+ <>
+ { text }
+
+ >
+ ) }
);
};
diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-payment-method.js b/client/checkout/woopay/express-button/woopay-express-checkout-payment-method.js
index 94637e94988..228032bbf6d 100644
--- a/client/checkout/woopay/express-button/woopay-express-checkout-payment-method.js
+++ b/client/checkout/woopay/express-button/woopay-express-checkout-payment-method.js
@@ -1,3 +1,9 @@
+/**
+ * External dependencies
+ */
+import { useCallback } from 'react';
+import ReactDOM from 'react-dom';
+
/**
* Internal dependencies
*/
@@ -18,15 +24,27 @@ const api = new WCPayAPI(
request
);
+const WooPayExpressCheckoutButtonContainer = () => {
+ const onRefChange = useCallback( ( node ) => {
+ if ( node ) {
+ const root = ReactDOM.createRoot( node );
+
+ root.render(
+
+ );
+ }
+ }, [] );
+
+ return ;
+};
+
const wooPayExpressCheckoutPaymentMethod = () => ( {
name: PAYMENT_METHOD_NAME_WOOPAY_EXPRESS_CHECKOUT,
- content: (
-
- ),
+ content: ,
edit: (
{
+ name?: string;
className?: string;
label: string;
describedBy?: string;
@@ -81,6 +82,7 @@ const stateReducer = (
};
function CustomSelectControl< ItemType extends Item >( {
+ name,
className,
label,
describedBy,
@@ -168,6 +170,7 @@ function CustomSelectControl< ItemType extends Item >( {
'components-custom-select-control__button',
{ placeholder: ! itemString }
),
+ name,
} ) }
>
diff --git a/client/components/dispute-status-chip/index.tsx b/client/components/dispute-status-chip/index.tsx
index ccb9e11a553..393089a8a78 100644
--- a/client/components/dispute-status-chip/index.tsx
+++ b/client/components/dispute-status-chip/index.tsx
@@ -4,6 +4,7 @@
* External dependencies
*/
import React from 'react';
+import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
@@ -21,10 +22,24 @@ import type {
interface Props {
status: DisputeStatus | string;
dueBy?: CachedDispute[ 'due_by' ] | EvidenceDetails[ 'due_by' ];
+ prefixDisputeType?: boolean;
}
-const DisputeStatusChip: React.FC< Props > = ( { status, dueBy } ) => {
+const DisputeStatusChip: React.FC< Props > = ( {
+ status,
+ dueBy,
+ prefixDisputeType,
+} ) => {
const mapping = displayStatus[ status ] || {};
- const message = mapping.message || formatStringValue( status );
+ let message = mapping.message || formatStringValue( status );
+
+ // Statuses starting with warning_ are Inquiries and these are already prefaced with "Inquiry: "
+ if ( prefixDisputeType && ! status.startsWith( 'warning' ) ) {
+ message = sprintf(
+ /** translators: %s is the status of the Dispute. */
+ __( 'Disputed: %s', 'woocommerce-payments' ),
+ message
+ );
+ }
const needsResponse = isAwaitingResponse( status );
const isUrgent =
diff --git a/client/components/grouped-select-control/index.tsx b/client/components/grouped-select-control/index.tsx
index 013605ce609..a8b75dfea0c 100644
--- a/client/components/grouped-select-control/index.tsx
+++ b/client/components/grouped-select-control/index.tsx
@@ -27,11 +27,13 @@ export interface GroupedSelectControlProps< ItemType > {
value?: ItemType | null;
placeholder?: string;
searchable?: boolean;
+ name?: string;
className?: string;
onChange?: ( changes: Partial< UseSelectState< ItemType > > ) => void;
}
const GroupedSelectControl = < ItemType extends ListItem >( {
+ name,
className,
label,
options: listItems,
@@ -176,6 +178,7 @@ const GroupedSelectControl = < ItemType extends ListItem >( {
'components-text-control__input wcpay-component-grouped-select-control__button',
{ placeholder }
),
+ name,
} ) }
>
diff --git a/client/components/inline-notice/index.tsx b/client/components/inline-notice/index.tsx
index d0c59812e96..bb230170ab9 100644
--- a/client/components/inline-notice/index.tsx
+++ b/client/components/inline-notice/index.tsx
@@ -7,6 +7,7 @@ import classNames from 'classnames';
import CheckmarkIcon from 'gridicons/dist/checkmark';
import NoticeOutlineIcon from 'gridicons/dist/notice-outline';
import InfoOutlineIcon from 'gridicons/dist/info-outline';
+import { Action } from 'wcpay/types/notices';
/**
* Internal dependencies.
@@ -21,6 +22,8 @@ interface InlineNoticeProps extends Notice.Props {
* @default undefined
*/
icon?: boolean | JSX.Element;
+
+ actions?: readonly Action[] | undefined;
}
/**
@@ -72,6 +75,8 @@ function InlineNotice( props: InlineNoticeProps ): JSX.Element {
key={ index }
className={ actionClass }
onClick={ action.onClick }
+ isBusy={ action.isBusy ?? false }
+ disabled={ action.disabled ?? false }
>
{ action.label }
diff --git a/client/components/loadable-checkbox/index.js b/client/components/loadable-checkbox/index.js
index 31ac7c60d70..8df3c49f8fe 100644
--- a/client/components/loadable-checkbox/index.js
+++ b/client/components/loadable-checkbox/index.js
@@ -27,6 +27,7 @@ const LoadableCheckboxControl = ( {
setupTooltip = '',
delayMsOnCheck = 0,
delayMsOnUncheck = 0,
+ needsAttention = false,
} ) => {
const [ isLoading, setLoading ] = useState( false );
const [ checkedState, setCheckedState ] = useState( checked );
@@ -87,24 +88,15 @@ const LoadableCheckboxControl = ( {
) }
{ ( isManualCaptureEnabled && ! isAllowingManualCapture ) ||
- isSetupRequired ? (
+ isSetupRequired ||
+ needsAttention ? (
diff --git a/client/multi-currency-setup/tasks/add-currencies-task/test/index.test.js b/client/multi-currency-setup/tasks/add-currencies-task/test/index.test.js
index 094d38fae08..f58beb09ced 100644
--- a/client/multi-currency-setup/tasks/add-currencies-task/test/index.test.js
+++ b/client/multi-currency-setup/tasks/add-currencies-task/test/index.test.js
@@ -420,10 +420,8 @@ describe( 'Multi-Currency enabled currencies list', () => {
).not.toBeInTheDocument();
expect(
- screen.getByRole( 'button', {
- name: /Add ([a-z0-9]+ )?currenc(y|ies)/i,
- } )
- ).toBeDisabled();
+ screen.getByRole( 'button', { name: 'Continue' } )
+ ).not.toBeDisabled();
// Reset mock currencies to original state.
useEnabledCurrencies.mockReturnValue( {
diff --git a/client/multi-currency-setup/tasks/store-settings-task/index.js b/client/multi-currency-setup/tasks/store-settings-task/index.js
index dbfc7381c08..7e20fc0f3e4 100644
--- a/client/multi-currency-setup/tasks/store-settings-task/index.js
+++ b/client/multi-currency-setup/tasks/store-settings-task/index.js
@@ -15,10 +15,15 @@ import WizardTaskItem from '../../wizard/task-item';
import PreviewModal from '../../../multi-currency/preview-modal';
import './index.scss';
-import { useStoreSettings } from 'wcpay/data';
+import { useStoreSettings, useSettings, useMultiCurrency } from 'wcpay/data';
const StoreSettingsTask = () => {
const { storeSettings, submitStoreSettingsUpdate } = useStoreSettings();
+ const { saveSettings, isSaving } = useSettings();
+ const [
+ isMultiCurrencyEnabled,
+ updateIsMultiCurrencyEnabled,
+ ] = useMultiCurrency();
const [ isPending, setPending ] = useState( false );
@@ -65,10 +70,18 @@ const StoreSettingsTask = () => {
const handleContinueClick = () => {
setPending( true );
+
+ if ( ! isMultiCurrencyEnabled ) {
+ updateIsMultiCurrencyEnabled( true );
+ saveSettings();
+ }
+
submitStoreSettingsUpdate(
isAutomaticSwitchEnabledValue,
- isStorefrontSwitcherEnabledValue
+ isStorefrontSwitcherEnabledValue,
+ ! isMultiCurrencyEnabled
);
+
setPending( false );
setCompleted( true, 'setup-complete' );
};
@@ -127,31 +140,30 @@ const StoreSettingsTask = () => {
'woocommerce-payments'
) }
/>
-
+
+ { __(
+ 'A currency switcher is also available in your widgets.',
+ 'woocommerce-payments'
+ ) }
+
>
) : null }
-
- { __(
- 'A currency switcher is also available in your widgets.',
- 'woocommerce-payments'
- ) }
-
{ __( 'Continue', 'woocommerce-payments' ) }
{ __( 'Preview', 'woocommerce-payments' ) }
diff --git a/client/multi-currency-setup/tasks/store-settings-task/test/__snapshots__/index.test.js.snap b/client/multi-currency-setup/tasks/store-settings-task/test/__snapshots__/index.test.js.snap
index 835869834ca..ef97ef8d897 100644
--- a/client/multi-currency-setup/tasks/store-settings-task/test/__snapshots__/index.test.js.snap
+++ b/client/multi-currency-setup/tasks/store-settings-task/test/__snapshots__/index.test.js.snap
@@ -121,9 +121,8 @@ exports[`Multi-Currency store settings store settings task renders correctly: sn
-
A currency switcher is also available in your widgets.
diff --git a/client/multi-currency-setup/tasks/store-settings-task/test/index.test.js b/client/multi-currency-setup/tasks/store-settings-task/test/index.test.js
index 0fd0310fe52..e81b7b08f99 100644
--- a/client/multi-currency-setup/tasks/store-settings-task/test/index.test.js
+++ b/client/multi-currency-setup/tasks/store-settings-task/test/index.test.js
@@ -8,12 +8,19 @@ import { render, screen, fireEvent } from '@testing-library/react';
* Internal dependencies
*/
import WizardTaskContext from '../../../../additional-methods-setup/wizard/task/context';
-import { useCurrencies, useStoreSettings } from 'wcpay/data';
+import {
+ useCurrencies,
+ useStoreSettings,
+ useSettings,
+ useMultiCurrency,
+} from 'wcpay/data';
import StoreSettingsTask from '..';
jest.mock( 'wcpay/data', () => ( {
useStoreSettings: jest.fn(),
useCurrencies: jest.fn(),
+ useSettings: jest.fn(),
+ useMultiCurrency: jest.fn(),
} ) );
const changeableSettings = [
@@ -43,6 +50,11 @@ useStoreSettings.mockReturnValue( {
submitStoreSettingsUpdate: jest.fn(),
} );
+useSettings.mockReturnValue( {
+ saveSettings: jest.fn().mockResolvedValue( {} ),
+ isSaving: false,
+} );
+
const setCompletedMock = jest.fn();
const createContainer = () => {
@@ -61,6 +73,10 @@ describe( 'Multi-Currency store settings', () => {
jest.clearAllMocks();
} );
+ beforeEach( () => {
+ useMultiCurrency.mockReturnValue( [ true, jest.fn() ] );
+ } );
+
test( 'store settings task renders correctly', () => {
const container = createContainer();
expect( container ).toMatchSnapshot(
@@ -83,6 +99,40 @@ describe( 'Multi-Currency store settings', () => {
} );
} );
+ test( 'multi-currency is enabled if it was previously disabled', async () => {
+ useMultiCurrency.mockReturnValue( [ false, jest.fn() ] );
+
+ createContainer();
+ const { submitStoreSettingsUpdate } = useStoreSettings();
+ const { saveSettings } = useSettings();
+ const [ , updateIsMultiCurrencyEnabled ] = useMultiCurrency();
+
+ fireEvent.click(
+ screen.getByRole( 'button', {
+ name: /Continue/,
+ } )
+ );
+
+ expect( saveSettings ).toBeCalled();
+ expect( updateIsMultiCurrencyEnabled ).toBeCalledWith( true );
+ expect( submitStoreSettingsUpdate ).toBeCalledWith(
+ false,
+ false,
+ true
+ );
+
+ changeableSettings.forEach( ( setting ) => {
+ fireEvent.click( screen.getByTestId( setting ) );
+ expect( screen.getByTestId( setting ) ).toBeChecked();
+ } );
+ fireEvent.click(
+ screen.getByRole( 'button', {
+ name: /Continue/,
+ } )
+ );
+ expect( submitStoreSettingsUpdate ).toBeCalledWith( true, true, true );
+ } );
+
test( 'store settings are saved with continue button click', () => {
createContainer();
const { submitStoreSettingsUpdate } = useStoreSettings();
@@ -91,7 +141,11 @@ describe( 'Multi-Currency store settings', () => {
name: /Continue/,
} )
);
- expect( submitStoreSettingsUpdate ).toBeCalledWith( false, false );
+ expect( submitStoreSettingsUpdate ).toBeCalledWith(
+ false,
+ false,
+ false
+ );
changeableSettings.forEach( ( setting ) => {
fireEvent.click( screen.getByTestId( setting ) );
@@ -102,7 +156,7 @@ describe( 'Multi-Currency store settings', () => {
name: /Continue/,
} )
);
- expect( submitStoreSettingsUpdate ).toBeCalledWith( true, true );
+ expect( submitStoreSettingsUpdate ).toBeCalledWith( true, true, false );
} );
test( 'store settings preview should open a modal with an iframe', () => {
diff --git a/client/multi-currency/multi-currency-settings/store-settings/index.js b/client/multi-currency/multi-currency-settings/store-settings/index.js
index ccb8b8f4818..eddeb570ccf 100644
--- a/client/multi-currency/multi-currency-settings/store-settings/index.js
+++ b/client/multi-currency/multi-currency-settings/store-settings/index.js
@@ -139,35 +139,43 @@ const StoreSettings = () => {
) }
{ storeSettings.site_theme === 'Storefront' ? (
-
- ) : null }
-
- { createInterpolateElement(
- sprintf(
- /* translators: %s: url to the widgets page */
- __(
- 'A currency switcher is also available in your widgets. ' +
- 'Configure now ',
+ <>
+ ,
- }
- ) }
-
+ ) }
+ />
+
+ { createInterpolateElement(
+ sprintf(
+ /* translators: %s: url to the widgets page */
+ __(
+ 'A currency switcher is also available in your widgets. ' +
+ '
Configure now ',
+ 'woocommerce-payments'
+ ),
+ 'widgets.php'
+ ),
+ {
+ linkToWidgets: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
+
+ ),
+ }
+ ) }
+
+ >
+ ) : null }
{
name: keyof OnboardingFields;
}
-export const OnboardingTextField: React.FC< OnboardingTextFieldProps > = ( {
- name,
- ...rest
-} ) => {
+export const OnboardingTextField: React.FC< OnboardingTextFieldProps > = (
+ props
+) => {
+ const { name } = props;
const { data, setData, touched } = useOnboardingContext();
const { validate, error } = useValidation( name );
const inputRef = React.useRef< HTMLInputElement >( null );
@@ -85,7 +85,7 @@ export const OnboardingTextField: React.FC< OnboardingTextFieldProps > = ( {
if ( event.key === 'Enter' ) validate();
} }
error={ error() }
- { ...rest }
+ { ...props }
/>
);
};
@@ -95,10 +95,10 @@ interface OnboardingPhoneNumberFieldProps
name: keyof OnboardingFields;
}
-export const OnboardingPhoneNumberField: React.FC< OnboardingPhoneNumberFieldProps > = ( {
- name,
- ...rest
-} ) => {
+export const OnboardingPhoneNumberField: React.FC< OnboardingPhoneNumberFieldProps > = (
+ props
+) => {
+ const { name } = props;
const { data, setData, temp, setTemp, touched } = useOnboardingContext();
const { validate, error } = useValidation( name );
@@ -117,7 +117,7 @@ export const OnboardingPhoneNumberField: React.FC< OnboardingPhoneNumberFieldPro
onKeyDown={ ( event: React.KeyboardEvent< HTMLInputElement > ) => {
if ( event.key === 'Enter' ) validate();
} }
- { ...rest }
+ { ...props }
/>
);
};
@@ -129,10 +129,10 @@ interface OnboardingSelectFieldProps< ItemType >
}
export const OnboardingSelectField = < ItemType extends SelectItem >( {
- name,
onChange,
...rest
}: OnboardingSelectFieldProps< ItemType > ): JSX.Element => {
+ const { name } = rest;
const { data, setData } = useOnboardingContext();
const { validate, error } = useValidation( name );
@@ -169,10 +169,10 @@ interface OnboardingGroupedSelectFieldProps< ItemType >
export const OnboardingGroupedSelectField = <
ListItemType extends GroupedSelectItem
>( {
- name,
onChange,
...rest
}: OnboardingGroupedSelectFieldProps< ListItemType > ): JSX.Element => {
+ const { name } = rest;
const { data, setData } = useOnboardingContext();
const { validate, error } = useValidation( name );
diff --git a/client/order/index.js b/client/order/index.js
index 35254d5a6da..02493a65431 100644
--- a/client/order/index.js
+++ b/client/order/index.js
@@ -158,7 +158,10 @@ const DisputeNotice = ( { chargeId } ) => {
// Disable the refund button.
refundButton.disabled = true;
- const disputeDetailsLink = getDetailsURL( dispute.id, 'disputes' );
+ const disputeDetailsLink = getDetailsURL(
+ chargeId,
+ 'transactions'
+ );
let tooltipText = '';
@@ -315,8 +318,8 @@ const DisputeNotice = ( { chargeId } ) => {
}
);
window.location = getDetailsURL(
- dispute.id,
- 'disputes'
+ chargeId,
+ 'transactions'
);
},
},
diff --git a/client/overview/connection-sucess-notice.tsx b/client/overview/connection-sucess-notice.tsx
index d7826a7595b..5a0e4c0af16 100644
--- a/client/overview/connection-sucess-notice.tsx
+++ b/client/overview/connection-sucess-notice.tsx
@@ -15,7 +15,11 @@ const ConnectionSuccessNotice: React.FC = () => {
const {
accountStatus: {
- progressiveOnboarding: { isComplete, isEnabled },
+ progressiveOnboarding: {
+ isEnabled: isPoEnabled,
+ isComplete: isPoComplete,
+ },
+ status: accountStatus,
},
onboardingTestMode,
} = wcpaySettings;
@@ -41,9 +45,9 @@ const ConnectionSuccessNotice: React.FC = () => {
{ /* Show dismiss button only at the end of Progressive Onboarding //
or at the end of the full KYC flow. */ }
- { ! ( isEnabled && ! isComplete ) && }
+ { ! ( isPoEnabled && ! isPoComplete ) && }
- { isEnabled && ! isComplete ? (
+ { isPoEnabled && ! isPoComplete ? (
<>
{ __(
@@ -60,12 +64,21 @@ const ConnectionSuccessNotice: React.FC = () => {
>
) : (
<>
-
- { __(
- 'Congratulations! Your store has been verified.',
- 'woocommerce-payments'
- ) }
-
+ { accountStatus !== 'complete' ? (
+
+ { __(
+ 'Congratulations! Your store is being verified.',
+ 'woocommerce-payments'
+ ) }
+
+ ) : (
+
+ { __(
+ 'Congratulations! Your store has been verified.',
+ 'woocommerce-payments'
+ ) }
+
+ ) }
>
) }
diff --git a/client/overview/task-list/tasks/dispute-task.tsx b/client/overview/task-list/tasks/dispute-task.tsx
index 9df718633f6..ec2d5bd0151 100644
--- a/client/overview/task-list/tasks/dispute-task.tsx
+++ b/client/overview/task-list/tasks/dispute-task.tsx
@@ -56,13 +56,13 @@ export const getDisputeResolutionTask = (
} );
const history = getHistory();
if ( activeDisputeCount === 1 ) {
- // Redirect to the dispute details page if there is only one dispute.
- const disputeId = activeDisputes[ 0 ].dispute_id;
+ // Redirect to the transaction details page if there is only one dispute.
+ const chargeId = activeDisputes[ 0 ].charge_id;
history.push(
getAdminUrl( {
page: 'wc-admin',
- path: '/payments/disputes/details',
- id: disputeId,
+ path: '/payments/transactions/details',
+ id: chargeId,
} )
);
} else {
diff --git a/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx b/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx
new file mode 100644
index 00000000000..fe3aa20c942
--- /dev/null
+++ b/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx
@@ -0,0 +1,373 @@
+/** @format **/
+
+/**
+ * External dependencies
+ */
+import React, { useState, useContext } from 'react';
+import moment from 'moment';
+import { __, sprintf } from '@wordpress/i18n';
+import { backup, edit, lock, arrowRight } from '@wordpress/icons';
+import { useDispatch } from '@wordpress/data';
+import { createInterpolateElement } from '@wordpress/element';
+import { Link } from '@woocommerce/components';
+import {
+ Button,
+ Card,
+ CardBody,
+ Flex,
+ FlexItem,
+ Icon,
+ Modal,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import type { Dispute } from 'wcpay/types/disputes';
+import type { ChargeBillingDetails } from 'wcpay/types/charges';
+import wcpayTracks from 'tracks';
+import { useDisputeAccept } from 'wcpay/data';
+import { getDisputeFeeFormatted, isInquiry } from 'wcpay/disputes/utils';
+import { getAdminUrl } from 'wcpay/utils';
+import DisputeNotice from './dispute-notice';
+import IssuerEvidenceList from './evidence-list';
+import DisputeSummaryRow from './dispute-summary-row';
+import { DisputeSteps, InquirySteps } from './dispute-steps';
+import InlineNotice from 'components/inline-notice';
+import WCPaySettingsContext from 'wcpay/settings/wcpay-settings-context';
+import './style.scss';
+
+interface Props {
+ dispute: Dispute;
+ customer: ChargeBillingDetails | null;
+ chargeCreated: number;
+ orderUrl: string | undefined;
+}
+
+/**
+ * The lines of text to display in the modal to confirm acceptance / refunding of the dispute / inquiry.
+ */
+interface ModalLineItem {
+ icon: JSX.Element;
+ description: string | JSX.Element;
+}
+
+interface AcceptDisputeProps {
+ /**
+ * The label for the button that opens the modal.
+ */
+ acceptButtonLabel: string;
+ /**
+ * The event to track when the button that opens the modal is clicked.
+ */
+ acceptButtonTracksEvent: string;
+ /**
+ * The title of the modal.
+ */
+ modalTitle: string;
+ /**
+ * The lines of text to display in the modal.
+ */
+ modalLines: ModalLineItem[];
+ /**
+ * The label for the primary button in the modal to Accept / Refund the dispute / inquiry.
+ */
+ modalButtonLabel: string;
+ /**
+ * The event to track when the primary button in the modal is clicked.
+ */
+ modalButtonTracksEvent: string;
+}
+
+/**
+ * Disputes and Inquiries have different text for buttons and the modal.
+ * They also have different icons and tracks events. This function returns the correct props.
+ *
+ * @param dispute
+ */
+function getAcceptDisputeProps( dispute: Dispute ): AcceptDisputeProps {
+ if ( isInquiry( dispute ) ) {
+ return {
+ acceptButtonLabel: __( 'Issue refund', 'woocommerce-payments' ),
+ acceptButtonTracksEvent:
+ wcpayTracks.events.DISPUTE_INQUIRY_REFUND_MODAL_VIEW,
+ modalTitle: __( 'Issue a refund?', 'woocommerce-payments' ),
+ modalLines: [
+ {
+ icon: ,
+ description: __(
+ 'Issuing a refund will close the inquiry, returning the amount in question back to the cardholder. No additional fees apply.',
+ 'woocommerce-payments'
+ ),
+ },
+ {
+ icon: ,
+ description: __(
+ 'You will be taken to the order, where you must complete the refund process manually.',
+ 'woocommerce-payments'
+ ),
+ },
+ ],
+ modalButtonLabel: __(
+ 'View order to issue refund',
+ 'woocommerce-payments'
+ ),
+ modalButtonTracksEvent:
+ wcpayTracks.events.DISPUTE_INQUIRY_REFUND_CLICK,
+ };
+ }
+
+ return {
+ acceptButtonLabel: __( 'Accept dispute', 'woocommerce-payments' ),
+ acceptButtonTracksEvent: wcpayTracks.events.DISPUTE_ACCEPT_MODAL_VIEW,
+ modalTitle: __( 'Accept the dispute?', 'woocommerce-payments' ),
+ modalLines: [
+ {
+ icon: ,
+ description: createInterpolateElement(
+ sprintf(
+ /* translators: %s: dispute fee, : emphasis HTML element. */
+ __(
+ 'Accepting the dispute marks it as Lost . The disputed amount and the %s dispute fee will not be returned to you.',
+ 'woocommerce-payments'
+ ),
+ getDisputeFeeFormatted( dispute, true ) ?? '-'
+ ),
+ {
+ em: ,
+ }
+ ),
+ },
+ {
+ icon: ,
+ description: __(
+ 'This action is final and cannot be undone.',
+ 'woocommerce-payments'
+ ),
+ },
+ ],
+ modalButtonLabel: __( 'Accept dispute', 'woocommerce-payments' ),
+ modalButtonTracksEvent: wcpayTracks.events.DISPUTE_ACCEPT_CLICK,
+ };
+}
+
+const DisputeAwaitingResponseDetails: React.FC< Props > = ( {
+ dispute,
+ customer,
+ chargeCreated,
+ orderUrl,
+} ) => {
+ const { doAccept, isLoading } = useDisputeAccept( dispute );
+ const [ isModalOpen, setModalOpen ] = useState( false );
+
+ const now = moment();
+ const dueBy = moment.unix( dispute.evidence_details?.due_by ?? 0 );
+ const countdownDays = Math.floor( dueBy.diff( now, 'days', true ) );
+ const hasStagedEvidence = dispute.evidence_details?.has_evidence;
+ const { createErrorNotice } = useDispatch( 'core/notices' );
+
+ const {
+ featureFlags: { isDisputeIssuerEvidenceEnabled },
+ } = useContext( WCPaySettingsContext );
+
+ const onModalClose = () => {
+ setModalOpen( false );
+ };
+
+ const viewOrder = () => {
+ if ( orderUrl ) {
+ window.location.href = orderUrl;
+ return;
+ }
+
+ createErrorNotice(
+ __(
+ 'Unable to view order. Order not found.',
+ 'woocommerce-payments'
+ )
+ );
+ };
+
+ const disputeAcceptAction = getAcceptDisputeProps( dispute );
+
+ const challengeButtonDefaultText = isInquiry( dispute )
+ ? __( 'Submit evidence', 'woocommerce-payments' )
+ : __( 'Challenge dispute', 'woocommerce-payments' );
+
+ return (
+
+
+
+
+ { hasStagedEvidence && (
+
+ { __(
+ `You initiated a challenge to this dispute. Click 'Continue with challenge' to proceed with your draft response.`,
+ 'woocommerce-payments'
+ ) }
+
+ ) }
+
+
+
+ { isInquiry( dispute ) ? (
+
+ ) : (
+
+ ) }
+
+ { isDisputeIssuerEvidenceEnabled && (
+
+ ) }
+
+ { /* Dispute Actions */ }
+ {
+
+
+
{
+ wcpayTracks.recordEvent(
+ wcpayTracks.events
+ .DISPUTE_CHALLENGE_CLICKED,
+ {
+ dispute_status: dispute.status,
+ on_page: 'transaction_details',
+ }
+ );
+ } }
+ >
+ { hasStagedEvidence
+ ? __(
+ 'Continue with challenge',
+ 'woocommerce-payments'
+ )
+ : challengeButtonDefaultText }
+
+
+
+
{
+ wcpayTracks.recordEvent(
+ disputeAcceptAction.acceptButtonTracksEvent,
+ {
+ dispute_status: dispute.status,
+ on_page: 'transaction_details',
+ }
+ );
+ setModalOpen( true );
+ } }
+ >
+ { disputeAcceptAction.acceptButtonLabel }
+
+
+ { /** Accept dispute modal */ }
+ { isModalOpen && (
+
+
+
+ { __(
+ 'Before proceeding, please take note of the following:',
+ 'woocommerce-payments'
+ ) }
+
+
+
+ { disputeAcceptAction.modalLines.map(
+ ( line, key ) => (
+
+
+ { line.icon }
+
+
+ { line.description }
+
+
+ )
+ ) }
+
+
+
+ { __(
+ 'Cancel',
+ 'woocommerce-payments'
+ ) }
+
+ {
+ wcpayTracks.recordEvent(
+ disputeAcceptAction.modalButtonTracksEvent,
+ {
+ dispute_status:
+ dispute.status,
+ on_page:
+ 'transaction_details',
+ }
+ );
+ setModalOpen( false );
+ /**
+ * Handle the primary modal action.
+ * If it's an inquiry, redirect to the order page; otherwise, continue with the default dispute acceptance.
+ */
+ if ( isInquiry( dispute ) ) {
+ viewOrder();
+ } else {
+ doAccept();
+ }
+ } }
+ >
+ {
+ disputeAcceptAction.modalButtonLabel
+ }
+
+
+
+ ) }
+
+ }
+
+
+
+ );
+};
+
+export default DisputeAwaitingResponseDetails;
diff --git a/client/payment-details/dispute-details/dispute-due-by-date.tsx b/client/payment-details/dispute-details/dispute-due-by-date.tsx
new file mode 100644
index 00000000000..00c74ee2745
--- /dev/null
+++ b/client/payment-details/dispute-details/dispute-due-by-date.tsx
@@ -0,0 +1,52 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { dateI18n } from '@wordpress/date';
+import { __, _n, sprintf } from '@wordpress/i18n';
+import classNames from 'classnames';
+import moment from 'moment';
+
+const DisputeDueByDate: React.FC< {
+ dueBy: number;
+} > = ( { dueBy } ) => {
+ const daysRemaining = Math.floor(
+ moment.unix( dueBy ).diff( moment(), 'days', true )
+ );
+ const respondByDate = dateI18n(
+ 'M j, Y, g:ia',
+ moment( dueBy * 1000 ).toISOString()
+ );
+ return (
+
+ { respondByDate }
+ 2,
+ } ) }
+ >
+ { daysRemaining > 0 &&
+ sprintf(
+ // Translators: %d is the number of days left to respond to the dispute.
+ _n(
+ '(%d day left to respond)',
+ '(%d days left to respond)',
+ daysRemaining,
+ 'woocommerce-payments'
+ ),
+ daysRemaining
+ ) }
+
+ { daysRemaining === 0 &&
+ __( '(Last day today)', 'woocommerce-payments' ) }
+ { daysRemaining < 0 &&
+ __( '(Past due)', 'woocommerce-payments' ) }
+
+
+ );
+};
+
+export default DisputeDueByDate;
diff --git a/client/payment-details/dispute-details/dispute-notice.tsx b/client/payment-details/dispute-details/dispute-notice.tsx
index 44c3286704b..664bfd7b897 100644
--- a/client/payment-details/dispute-details/dispute-notice.tsx
+++ b/client/payment-details/dispute-details/dispute-notice.tsx
@@ -5,6 +5,7 @@
*/
import React from 'react';
import { __, sprintf } from '@wordpress/i18n';
+import { ExternalLink } from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
/**
@@ -18,52 +19,55 @@ import { isInquiry } from 'wcpay/disputes/utils';
interface DisputeNoticeProps {
dispute: Dispute;
- urgent: boolean;
+ isUrgent: boolean;
}
const DisputeNotice: React.FC< DisputeNoticeProps > = ( {
dispute,
- urgent,
+ isUrgent,
} ) => {
- const clientClaim =
+ const shopperDisputeReason =
reasons[ dispute.reason ]?.claim ??
__(
'The cardholder claims this is an unrecognized charge.',
'woocommerce-payments'
);
- const noticeText = isInquiry( dispute )
- ? /* translators: link to dispute documentation. %s is the clients claim for the dispute, eg "The cardholder claims this is an unrecognized charge." */
- __(
- // eslint-disable-next-line max-len
- '%s You can challenge their claim if you believe it’s invalid. Not responding will result in an automatic loss. Learn more ',
- 'woocommerce-payments'
- )
- : /* translators: link to dispute documentation. %s is the clients claim for the dispute, eg "The cardholder claims this is an unrecognized charge." */
- __(
- // eslint-disable-next-line max-len
- '%s Challenge the dispute if you believe the claim is invalid, or accept to forfeit the funds and pay the dispute fee. Non-response will result in an automatic loss. Learn more about responding to disputes ',
- 'woocommerce-payments'
- );
+ /* translators: link to dispute documentation. %s is the clients claim for the dispute, eg "The cardholder claims this is an unrecognized charge." */
+ let noticeText = __(
+ '%s Challenge the dispute if you believe the claim is invalid, ' +
+ 'or accept to forfeit the funds and pay the dispute fee. ' +
+ 'Non-response will result in an automatic loss. Learn more about responding to disputes ',
+ 'woocommerce-payments'
+ );
+ let learnMoreDocsUrl =
+ 'https://woocommerce.com/document/woopayments/fraud-and-disputes/managing-disputes/#responding';
+
+ if ( isInquiry( dispute ) ) {
+ /* translators: link to dispute inquiry documentation. %s is the clients claim for the dispute, eg "The cardholder claims this is an unrecognized charge." */
+ noticeText = __(
+ '%s You can challenge their claim if you believe it’s invalid. ' +
+ 'Not responding will result in an automatic loss. Learn more about payment inquiries ',
+ 'woocommerce-payments'
+ );
+ learnMoreDocsUrl =
+ 'https://woocommerce.com/document/woopayments/fraud-and-disputes/managing-disputes/#inquiries';
+ }
return (
- { createInterpolateElement( sprintf( noticeText, clientClaim ), {
- a: (
- // eslint-disable-next-line jsx-a11y/anchor-has-content
-
- ),
- strong: ,
- } ) }
+ { createInterpolateElement(
+ sprintf( noticeText, shopperDisputeReason ),
+ {
+ a: ,
+ strong: ,
+ }
+ ) }
);
};
diff --git a/client/payment-details/dispute-details/dispute-resolution-footer.tsx b/client/payment-details/dispute-details/dispute-resolution-footer.tsx
new file mode 100644
index 00000000000..92d7e0a987c
--- /dev/null
+++ b/client/payment-details/dispute-details/dispute-resolution-footer.tsx
@@ -0,0 +1,449 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import moment from 'moment';
+import { dateI18n } from '@wordpress/date';
+import { __, sprintf } from '@wordpress/i18n';
+import { Link } from '@woocommerce/components';
+import { createInterpolateElement } from '@wordpress/element';
+import { Button, CardFooter, Flex, FlexItem } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import type { Dispute } from 'wcpay/types/disputes';
+import wcpayTracks from 'tracks';
+import { getAdminUrl } from 'wcpay/utils';
+import { getDisputeFeeFormatted } from 'wcpay/disputes/utils';
+import './style.scss';
+
+const DisputeUnderReviewFooter: React.FC< {
+ dispute: Pick< Dispute, 'id' | 'metadata' | 'status' >;
+} > = ( { dispute } ) => {
+ const submissionDateFormatted = dispute.metadata.__evidence_submitted_at
+ ? dateI18n(
+ 'M j, Y',
+ moment
+ .unix(
+ parseInt( dispute.metadata.__evidence_submitted_at, 10 )
+ )
+ .toISOString()
+ )
+ : '-';
+
+ return (
+
+
+
+ { createInterpolateElement(
+ sprintf(
+ /* Translators: %s - formatted date, - link to documentation page */
+ __(
+ 'You submitted evidence for this dispute on %s. The cardholder’s bank is reviewing the case, which can take 60 days or more. You will be alerted when they make their final decision. Learn more about the dispute process .',
+ 'woocommerce-payments'
+ ),
+ submissionDateFormatted
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content -- Link content is provided by createInterpolateElement
+
+ ),
+ }
+ ) }
+
+
+
+ {
+ wcpayTracks.recordEvent(
+ wcpayTracks.events
+ .PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED,
+ {
+ dispute_status: dispute.status,
+ on_page: 'transaction_details',
+ }
+ );
+ } }
+ >
+ { __(
+ 'View submitted evidence',
+ 'woocommerce-payments'
+ ) }
+
+
+
+
+
+ );
+};
+
+const DisputeWonFooter: React.FC< {
+ dispute: Pick< Dispute, 'id' | 'metadata' | 'status' >;
+} > = ( { dispute } ) => {
+ const closedDateFormatted = dispute.metadata.__dispute_closed_at
+ ? dateI18n(
+ 'M j, Y',
+ moment
+ .unix(
+ parseInt( dispute.metadata.__dispute_closed_at, 10 )
+ )
+ .toISOString()
+ )
+ : '-';
+
+ return (
+
+
+
+ { createInterpolateElement(
+ sprintf(
+ /* Translators: %s - formatted date, - link to documentation page */
+ __(
+ 'Good news! You won this dispute on %s. The disputed amount and the dispute fee have been credited back to your account. Learn more about preventing disputes .',
+ 'woocommerce-payments'
+ ),
+ closedDateFormatted
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content -- Link content is provided by createInterpolateElement
+
+ ),
+ }
+ ) }
+
+
+
+ {
+ wcpayTracks.recordEvent(
+ wcpayTracks.events
+ .PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED,
+ {
+ dispute_status: dispute.status,
+ on_page: 'transaction_details',
+ }
+ );
+ } }
+ >
+ { __(
+ 'View dispute details',
+ 'woocommerce-payments'
+ ) }
+
+
+
+
+
+ );
+};
+
+const DisputeLostFooter: React.FC< {
+ dispute: Pick<
+ Dispute,
+ 'id' | 'metadata' | 'status' | 'balance_transactions'
+ >;
+} > = ( { dispute } ) => {
+ const isSubmitted = !! dispute.metadata.__evidence_submitted_at;
+ const isAccepted = dispute.metadata.__closed_by_merchant === '1';
+ const disputeFeeFormatted = getDisputeFeeFormatted( dispute, true ) ?? '-';
+
+ const closedDateFormatted = dispute.metadata.__dispute_closed_at
+ ? dateI18n(
+ 'M j, Y',
+ moment
+ .unix(
+ parseInt( dispute.metadata.__dispute_closed_at, 10 )
+ )
+ .toISOString()
+ )
+ : '-';
+
+ let messagePrefix = sprintf(
+ /* Translators: %1$s - formatted date */
+ __(
+ 'This dispute was lost on %1$s due to non-response.',
+ 'woocommerce-payments'
+ ),
+ closedDateFormatted
+ );
+
+ if ( isAccepted ) {
+ messagePrefix = sprintf(
+ /* Translators: %1$s - formatted date */
+ __(
+ 'This dispute was accepted and lost on %1$s.',
+ 'woocommerce-payments'
+ ),
+ closedDateFormatted
+ );
+ }
+
+ if ( isSubmitted ) {
+ messagePrefix = sprintf(
+ /* Translators: %1$s - formatted date */
+ __( 'This dispute was lost on %1$s.', 'woocommerce-payments' ),
+ closedDateFormatted
+ );
+ }
+
+ return (
+
+
+
+ { messagePrefix }{ ' ' }
+ { createInterpolateElement(
+ sprintf(
+ /* Translators: %1$s – the formatted dispute fee amount, - link to documentation page */
+ __(
+ 'The %1$s fee has been deducted from your account, and the disputed amount returned to the cardholder. Learn more about preventing disputes .',
+ 'woocommerce-payments'
+ ),
+ disputeFeeFormatted
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content -- Link content is provided by createInterpolateElement
+
+ ),
+ }
+ ) }
+
+
+ { isSubmitted && (
+
+
+ {
+ wcpayTracks.recordEvent(
+ wcpayTracks.events
+ .PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED,
+ {
+ dispute_status: dispute.status,
+ on_page: 'transaction_details',
+ }
+ );
+ } }
+ >
+ { __(
+ 'View dispute details',
+ 'woocommerce-payments'
+ ) }
+
+
+
+ ) }
+
+
+ );
+};
+
+const InquiryUnderReviewFooter: React.FC< {
+ dispute: Pick< Dispute, 'id' | 'metadata' | 'status' >;
+} > = ( { dispute } ) => {
+ const submissionDateFormatted = dispute.metadata.__evidence_submitted_at
+ ? dateI18n(
+ 'M j, Y',
+ moment
+ .unix(
+ parseInt( dispute.metadata.__evidence_submitted_at, 10 )
+ )
+ .toISOString()
+ )
+ : '-';
+
+ return (
+
+
+
+ { createInterpolateElement(
+ sprintf(
+ /* Translators: %s - formatted date, - link to documentation page */
+ __(
+ 'You submitted evidence for this inquiry on %s. The cardholder’s bank is reviewing the case, which can take 120 days or more. You will be alerted when they make their final decision. Learn more .',
+ 'woocommerce-payments'
+ ),
+ submissionDateFormatted
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content -- Link content is provided by createInterpolateElement
+
+ ),
+ }
+ ) }
+
+
+
+ {
+ wcpayTracks.recordEvent(
+ wcpayTracks.events
+ .PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED,
+ {
+ dispute_status: dispute.status,
+ on_page: 'transaction_details',
+ }
+ );
+ } }
+ >
+ { __(
+ 'View submitted evidence',
+ 'woocommerce-payments'
+ ) }
+
+
+
+
+
+ );
+};
+
+const InquiryClosedFooter: React.FC< {
+ dispute: Pick< Dispute, 'id' | 'metadata' | 'status' >;
+} > = ( { dispute } ) => {
+ const isSubmitted = !! dispute.metadata.__evidence_submitted_at;
+ const closedDateFormatted = dispute.metadata.__dispute_closed_at
+ ? dateI18n(
+ 'M j, Y',
+ moment
+ .unix(
+ parseInt( dispute.metadata.__dispute_closed_at, 10 )
+ )
+ .toISOString()
+ )
+ : '-';
+
+ return (
+
+
+
+ { createInterpolateElement(
+ sprintf(
+ /* Translators: %s - formatted date, - link to documentation page */
+ __(
+ 'This inquiry was closed on %s. Learn more about preventing disputes .',
+ 'woocommerce-payments'
+ ),
+ closedDateFormatted
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content -- Link content is provided by createInterpolateElement
+
+ ),
+ }
+ ) }
+
+
+ { isSubmitted && (
+
+
+ {
+ wcpayTracks.recordEvent(
+ wcpayTracks.events
+ .PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED,
+ {
+ dispute_status: dispute.status,
+ on_page: 'transaction_details',
+ }
+ );
+ } }
+ >
+ { __(
+ 'View submitted evidence',
+ 'woocommerce-payments'
+ ) }
+
+
+
+ ) }
+
+
+ );
+};
+
+const DisputeResolutionFooter: React.FC< {
+ dispute: Pick<
+ Dispute,
+ 'id' | 'metadata' | 'status' | 'balance_transactions'
+ >;
+} > = ( { dispute } ) => {
+ if ( dispute.status === 'under_review' ) {
+ return ;
+ }
+ if ( dispute.status === 'won' ) {
+ return ;
+ }
+ if ( dispute.status === 'lost' ) {
+ return ;
+ }
+ if ( dispute.status === 'warning_under_review' ) {
+ return ;
+ }
+ if ( dispute.status === 'warning_closed' ) {
+ return ;
+ }
+
+ return null;
+};
+
+export default DisputeResolutionFooter;
diff --git a/client/payment-details/dispute-details/dispute-steps.tsx b/client/payment-details/dispute-details/dispute-steps.tsx
new file mode 100644
index 00000000000..ab592b52ce3
--- /dev/null
+++ b/client/payment-details/dispute-details/dispute-steps.tsx
@@ -0,0 +1,278 @@
+/** @format **/
+
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { __, sprintf } from '@wordpress/i18n';
+import { createInterpolateElement } from '@wordpress/element';
+import { ExternalLink } from '@wordpress/components';
+import { dateI18n } from '@wordpress/date';
+import moment from 'moment';
+import HelpOutlineIcon from 'gridicons/dist/help-outline';
+
+/**
+ * Internal dependencies
+ */
+import type { Dispute } from 'wcpay/types/disputes';
+import { ChargeBillingDetails } from 'wcpay/types/charges';
+import { formatExplicitCurrency } from 'utils/currency';
+import { ClickTooltip } from 'wcpay/components/tooltip';
+import { getDisputeFeeFormatted } from 'wcpay/disputes/utils';
+import DisputeDueByDate from './dispute-due-by-date';
+
+interface Props {
+ dispute: Dispute;
+ customer: ChargeBillingDetails | null;
+ chargeCreated: number;
+}
+
+export const DisputeSteps: React.FC< Props > = ( {
+ dispute,
+ customer,
+ chargeCreated,
+} ) => {
+ let emailLink;
+ if ( customer?.email ) {
+ const chargeDate = dateI18n(
+ 'Y-m-d',
+ moment( chargeCreated * 1000 ).toISOString()
+ );
+ const disputeDate = dateI18n(
+ 'Y-m-d',
+ moment( dispute.created * 1000 ).toISOString()
+ );
+ const emailSubject = sprintf(
+ // Translators: %1$s is the store name, %2$s is the charge date.
+ __(
+ `Problem with your purchase from %1$s on %2$s?`,
+ 'woocommerce-payments'
+ ),
+ wcpaySettings.storeName,
+ chargeDate
+ );
+ const customerName = customer?.name || '';
+ const emailBody = sprintf(
+ // Translators: %1$s is the customer name, %2$s is the dispute date, %3$s is the dispute amount with currency-code e.g. $15 USD, %4$s is the charge date.
+ __(
+ `Hello %1$s,\n\n` +
+ `We noticed that on %2$s, you disputed a %3$s charge on %4$s. We wanted to contact you to make sure everything was all right with your purchase and see if there's anything else we can do to resolve any problems you might have had.\n\n` +
+ `Alternatively, if the dispute was a mistake, you can easily withdraw it by calling the number on the back of your card. Thank you so much - we appreciate your business and look forward to working with you.`,
+ 'woocommerce-payments'
+ ),
+ customerName,
+ disputeDate,
+ formatExplicitCurrency( dispute.amount, dispute.currency ),
+ chargeDate
+ );
+ emailLink = `mailto:${ customer.email }?subject=${ encodeURIComponent(
+ emailSubject
+ ) }&body=${ encodeURIComponent( emailBody ) }`;
+ }
+
+ return (
+
+
+ { __( 'Steps to resolve:', 'woocommerce-payments' ) }
+
+
+
+ { customer?.email
+ ? createInterpolateElement(
+ __(
+ 'Email the customer to identify the issue and work towards a resolution where possible.',
+ 'woocommerce-payments'
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
+
+ ),
+ }
+ )
+ : __(
+ 'Email the customer to identify the issue and work towards a resolution where possible.',
+ 'woocommerce-payments'
+ ) }
+
+
+ { createInterpolateElement(
+ __(
+ 'Assist the customer in withdrawing their dispute if they agree to do so.',
+ 'woocommerce-payments'
+ ),
+ {
+ a: (
+
+ ),
+ }
+ ) }
+
+
+ { createInterpolateElement(
+ __(
+ 'Challenge or accept the dispute by .',
+ 'woocommerce-payments'
+ ),
+ {
+ challengeIcon: (
+ }
+ buttonLabel={ __(
+ 'Challenge the dispute tooltip',
+ 'woocommerce-payments'
+ ) }
+ content={ __(
+ "Challenge the dispute if you consider the claim invalid. You'll need to provide evidence to back your claim. Keep in mind that challenging doesn't ensure a resolution in your favor.",
+ 'woocommerce-payments'
+ ) }
+ />
+ ),
+ acceptIcon: (
+ }
+ buttonLabel={ __(
+ 'Accept the dispute tooltip',
+ 'woocommerce-payments'
+ ) }
+ content={ sprintf(
+ // Translators: %s is a formatted currency amount, eg $10.00.
+ __(
+ `Accepting this dispute will automatically close it. The disputed amount and the %s dispute fee will not be returned to you.`,
+ 'woocommerce-payments'
+ ),
+ getDisputeFeeFormatted(
+ dispute,
+ true
+ ) || '-'
+ ) }
+ />
+ ),
+ dueByDate: (
+
+ ),
+ }
+ ) }
+
+
+
+ );
+};
+
+export const InquirySteps: React.FC< Props > = ( {
+ dispute,
+ customer,
+ chargeCreated,
+} ) => {
+ let emailLink;
+ if ( customer?.email ) {
+ const chargeDate = dateI18n(
+ 'Y-m-d',
+ moment( chargeCreated * 1000 ).toISOString()
+ );
+ const disputeDate = dateI18n(
+ 'Y-m-d',
+ moment( dispute.created * 1000 ).toISOString()
+ );
+ const emailSubject = sprintf(
+ // Translators: %1$s is the store name, %2$s is the charge date.
+ __(
+ `Problem with your purchase from %1$s on %2$s?`,
+ 'woocommerce-payments'
+ ),
+ wcpaySettings.storeName,
+ chargeDate
+ );
+ const customerName = customer?.name || '';
+ const emailBody = sprintf(
+ // Translators: %1$s is the customer name, %2$s is the dispute date, %3$s is the dispute amount with currency-code e.g. $15 USD, %4$s is the charge date.
+ __(
+ `Hello %1$s,\n\n` +
+ `We noticed that on %2$s, you disputed a %3$s charge on %4$s. We wanted to contact you to make sure everything was all right with your purchase and see if there's anything else we can do to resolve any problems you might have had.\n\n` +
+ `Alternatively, if the dispute was a mistake, you can easily withdraw it by calling the number on the back of your card. Thank you so much - we appreciate your business and look forward to working with you.`,
+ 'woocommerce-payments'
+ ),
+ customerName,
+ disputeDate,
+ formatExplicitCurrency( dispute.amount, dispute.currency ),
+ chargeDate
+ );
+ emailLink = `mailto:${ customer.email }?subject=${ encodeURIComponent(
+ emailSubject
+ ) }&body=${ encodeURIComponent( emailBody ) }`;
+ }
+
+ return (
+
+
+ { __( 'Steps to resolve:', 'woocommerce-payments' ) }
+
+
+
+ { customer?.email
+ ? createInterpolateElement(
+ __(
+ 'Email the customer to identify the issue and work towards a resolution where possible.',
+ 'woocommerce-payments'
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
+
+ ),
+ }
+ )
+ : __(
+ 'Email the customer to identify the issue and work towards a resolution where possible.',
+ 'woocommerce-payments'
+ ) }
+
+
+ { createInterpolateElement(
+ __(
+ 'Submit evidence or issue a refund by .',
+ 'woocommerce-payments'
+ ),
+ {
+ submitEvidenceIcon: (
+ }
+ buttonLabel={ __(
+ 'Submit evidence tooltip',
+ 'woocommerce-payments'
+ ) }
+ content={ createInterpolateElement(
+ __(
+ "To submit evidence, provide documentation that supports your case. Keep in mind that submitting evidence doesn't ensure a favorable outcome. If the cardholder agrees to withdraw the inquiry, you'll still need to officially submit your evidence to prevent bank escalation. Learn more ",
+ 'woocommerce-payments'
+ ),
+ {
+ learnMoreLink: (
+
+ ),
+ }
+ ) }
+ />
+ ),
+ dueByDate: (
+
+ ),
+ }
+ ) }
+
+
+
+ );
+};
diff --git a/client/payment-details/dispute-details/dispute-summary-row.tsx b/client/payment-details/dispute-details/dispute-summary-row.tsx
index 160abaeb000..ac6dada265e 100644
--- a/client/payment-details/dispute-details/dispute-summary-row.tsx
+++ b/client/payment-details/dispute-details/dispute-summary-row.tsx
@@ -6,34 +6,26 @@
import React from 'react';
import moment from 'moment';
import HelpOutlineIcon from 'gridicons/dist/help-outline';
-import { __, _n, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import { dateI18n } from '@wordpress/date';
-import classNames from 'classnames';
/**
* Internal dependencies
*/
import type { Dispute } from 'wcpay/types/disputes';
import { HorizontalList } from 'wcpay/components/horizontal-list';
-import { formatCurrency } from 'wcpay/utils/currency';
+import { formatExplicitCurrency } from 'wcpay/utils/currency';
import { reasons } from 'wcpay/disputes/strings';
import { formatStringValue } from 'wcpay/utils';
import { ClickTooltip } from 'wcpay/components/tooltip';
import Paragraphs from 'wcpay/components/paragraphs';
+import DisputeDueByDate from './dispute-due-by-date';
interface Props {
dispute: Dispute;
- daysRemaining: number;
}
-const DisputeSummaryRow: React.FC< Props > = ( { dispute, daysRemaining } ) => {
- const respondByDate = dispute.evidence_details?.due_by
- ? dateI18n(
- 'M j, Y, g:ia',
- moment( dispute.evidence_details?.due_by * 1000 ).toISOString()
- )
- : '–';
-
+const DisputeSummaryRow: React.FC< Props > = ( { dispute } ) => {
const disputeReason = formatStringValue(
reasons[ dispute.reason ]?.display || dispute.reason
);
@@ -42,7 +34,7 @@ const DisputeSummaryRow: React.FC< Props > = ( { dispute, daysRemaining } ) => {
const columns = [
{
title: __( 'Dispute Amount', 'woocommerce-payments' ),
- content: formatCurrency( dispute.amount, dispute.currency ),
+ content: formatExplicitCurrency( dispute.amount, dispute.currency ),
},
{
title: __( 'Disputed On', 'woocommerce-payments' ),
@@ -93,30 +85,7 @@ const DisputeSummaryRow: React.FC< Props > = ( { dispute, daysRemaining } ) => {
{
title: __( 'Respond By', 'woocommerce-payments' ),
content: (
-
- { respondByDate }
- 2,
- } ) }
- >
- { daysRemaining === 0
- ? __( '(Last day today)', 'woocommerce-payments' )
- : sprintf(
- // Translators: %s is the number of days left to respond to the dispute.
- _n(
- '(%s day left to respond)',
- '(%s days left to respond)',
- daysRemaining,
- 'woocommerce-payments'
- ),
- daysRemaining
- ) }
-
-
+
),
},
];
diff --git a/client/payment-details/dispute-details/evidence-list.tsx b/client/payment-details/dispute-details/evidence-list.tsx
index e6610b74206..687bb3630ca 100644
--- a/client/payment-details/dispute-details/evidence-list.tsx
+++ b/client/payment-details/dispute-details/evidence-list.tsx
@@ -103,14 +103,15 @@ const FileEvidence: React.FC< {
};
interface Props {
- issuerEvidence: IssuerEvidence | null;
+ issuerEvidence: IssuerEvidence[] | null;
}
const IssuerEvidenceList: React.FC< Props > = ( { issuerEvidence } ) => {
if (
- ! issuerEvidence ||
- ! issuerEvidence.file_evidence.length ||
- ! issuerEvidence.text_evidence
+ ! issuerEvidence?.some(
+ ( evidence ) =>
+ evidence.file_evidence.length || evidence.text_evidence
+ )
) {
return <>>;
}
@@ -122,20 +123,19 @@ const IssuerEvidenceList: React.FC< Props > = ( { issuerEvidence } ) => {
initialOpen={ false }
>
- { issuerEvidence.text_evidence && (
-
-
+ { issuerEvidence.map( ( evidence, i ) => (
+
+ { evidence.text_evidence && (
+
+ ) }
+ { evidence.file_evidence.map( ( fileId ) => (
+
+ ) ) }
- ) }
- { issuerEvidence.file_evidence.map(
- ( fileId: string, i: any ) => (
-
-
-
- )
- ) }
+ ) ) }
);
diff --git a/client/payment-details/dispute-details/index.tsx b/client/payment-details/dispute-details/index.tsx
deleted file mode 100644
index bb41511d293..00000000000
--- a/client/payment-details/dispute-details/index.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-/** @format **/
-
-/**
- * External dependencies
- */
-import React from 'react';
-import moment from 'moment';
-import { __ } from '@wordpress/i18n';
-import { Card, CardBody } from '@wordpress/components';
-import { edit } from '@wordpress/icons';
-
-/**
- * Internal dependencies
- */
-import type { Dispute } from 'wcpay/types/disputes';
-import { isAwaitingResponse } from 'wcpay/disputes/utils';
-import DisputeNotice from './dispute-notice';
-import IssuerEvidenceList from './evidence-list';
-import DisputeSummaryRow from './dispute-summary-row';
-import InlineNotice from 'components/inline-notice';
-import './style.scss';
-
-interface DisputeDetailsProps {
- dispute: Dispute;
-}
-
-const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => {
- const now = moment();
- const dueBy = moment.unix( dispute.evidence_details?.due_by ?? 0 );
- const countdownDays = Math.floor( dueBy.diff( now, 'days', true ) );
- const hasStagedEvidence = dispute.evidence_details?.has_evidence;
-
- return (
-
-
-
- { isAwaitingResponse( dispute.status ) &&
- countdownDays >= 0 && (
- <>
-
- { hasStagedEvidence && (
-
- { __(
- `You initiated a challenge to this dispute. Click 'Continue with challenge' to proceed with your draft response.`,
- 'woocommerce-payments'
- ) }
-
- ) }
-
-
- >
- ) }
-
-
-
- );
-};
-
-export default DisputeDetails;
diff --git a/client/payment-details/dispute-details/style.scss b/client/payment-details/dispute-details/style.scss
index 628ab098b39..e51559f4fcd 100644
--- a/client/payment-details/dispute-details/style.scss
+++ b/client/payment-details/dispute-details/style.scss
@@ -17,7 +17,7 @@
}
.dispute-summary-row {
- margin: 24px 0;
+ margin-top: 24px;
&__response-date {
display: flex;
@@ -34,6 +34,22 @@
}
}
}
+
+ &__actions {
+ display: flex;
+ justify-content: start;
+ gap: $grid-unit-10;
+ margin-top: 24px;
+
+ @media screen and ( max-width: $break-small ) {
+ flex-direction: column;
+
+ .components-button {
+ width: 100%;
+ justify-content: center;
+ }
+ }
+ }
}
}
.dispute-reason-tooltip {
@@ -48,6 +64,85 @@
margin-bottom: 8px;
}
}
+.dispute-steps {
+ margin-top: 24px;
+
+ &__header {
+ font-weight: 600;
+ font-size: 14px;
+ }
+ &__steps {
+ list-style-position: inside;
+ margin: 0;
+
+ > li {
+ margin: 0;
+ padding: 16px 10px 16px 4px;
+ border-bottom: 1px solid $wp-gray-5;
+ }
+
+ .wcpay-tooltip__content-wrapper > [role='button'] {
+ margin: 0;
+ }
+
+ &__response-date {
+ display: inline-flex;
+ align-items: center;
+ gap: var( --grid-unit-05, 4px );
+ flex-wrap: wrap;
+ font-weight: 600;
+
+ &--warning {
+ color: $wp-yellow-30;
+ font-weight: 700;
+ }
+ &--urgent {
+ color: $alert-red;
+ font-weight: 700;
+ }
+ }
+ }
+}
+.transaction-details-dispute-accept-modal {
+ max-width: 600px;
+
+ .components-modal__content {
+ padding-top: $grid-unit-30;
+ }
+
+ &__icon {
+ flex-shrink: 0;
+ padding: 6px;
+ margin-right: $grid-unit-10;
+ }
+
+ &__actions {
+ margin-top: $grid-unit-30;
+ }
+}
+
+.transaction-details-dispute-footer {
+ background-color: #f2f4f5;
+
+ &__actions {
+ flex-shrink: 0;
+ }
+
+ &--primary {
+ background-color: $wp-blue-0;
+ }
+
+ @media screen and ( max-width: $break-small ) {
+ .components-flex {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .components-flex-item {
+ margin: 10px 0;
+ }
+ }
+}
.dispute-evidence {
// Override WordPress core PanelBody boxy styles. Ours is more inline content.
diff --git a/client/payment-details/dispute-details/test/index.test.tsx b/client/payment-details/dispute-details/test/index.test.tsx
deleted file mode 100644
index 9787972fd17..00000000000
--- a/client/payment-details/dispute-details/test/index.test.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-/** @format */
-/**
- * External dependencies
- */
-import { render, screen } from '@testing-library/react';
-import React from 'react';
-
-/**
- * Internal dependencies
- */
-import type { Dispute } from 'wcpay/types/disputes';
-import type { Charge } from 'wcpay/types/charges';
-import DisputeDetails from '..';
-
-declare const global: {
- wcSettings: {
- locale: {
- siteLocale: string;
- };
- };
- wcpaySettings: {
- isSubscriptionsActive: boolean;
- zeroDecimalCurrencies: string[];
- currencyData: Record< string, any >;
- connect: {
- country: string;
- };
- featureFlags: {
- isAuthAndCaptureEnabled: boolean;
- };
- };
-};
-
-global.wcpaySettings = {
- isSubscriptionsActive: false,
- zeroDecimalCurrencies: [],
- connect: {
- country: 'US',
- },
- featureFlags: {
- isAuthAndCaptureEnabled: true,
- },
- currencyData: {
- US: {
- code: 'USD',
- symbol: '$',
- symbolPosition: 'left',
- thousandSeparator: ',',
- decimalSeparator: '.',
- precision: 2,
- },
- },
-};
-
-interface ChargeWithDisputeRequired extends Charge {
- dispute: Dispute;
-}
-
-const getBaseCharge = (): ChargeWithDisputeRequired =>
- ( {
- id: 'ch_38jdHA39KKA',
- /* Stripe data comes in seconds, instead of the default Date milliseconds */
- created: Date.parse( 'Sep 19, 2019, 5:24 pm' ) / 1000,
- amount: 2000,
- amount_refunded: 0,
- application_fee_amount: 70,
- disputed: true,
- dispute: {
- id: 'dp_1',
- amount: 6800,
- charge: 'ch_38jdHA39KKA',
- order: null,
- balance_transactions: [
- {
- amount: -2000,
- currency: 'usd',
- fee: 1500,
- },
- ],
- created: 1693453017,
- currency: 'usd',
- evidence: {
- billing_address: '123 test address',
- customer_email_address: 'test@email.com',
- customer_name: 'Test customer',
- shipping_address: '123 test address',
- },
- issuer_evidence: null,
- evidence_details: {
- due_by: 1694303999,
- has_evidence: false,
- past_due: false,
- submission_count: 0,
- },
- // issuer_evidence: null,
- metadata: [],
- payment_intent: 'pi_1',
- reason: 'fraudulent',
- status: 'needs_response',
- } as Dispute,
- currency: 'usd',
- type: 'charge',
- status: 'succeeded',
- paid: true,
- captured: true,
- balance_transaction: {
- amount: 2000,
- currency: 'usd',
- fee: 70,
- },
- refunds: {
- data: [],
- },
- order: {
- number: 45981,
- url: 'https://somerandomorderurl.com/?edit_order=45981',
- },
- billing_details: {
- name: 'Customer name',
- },
- payment_method_details: {
- card: {
- brand: 'visa',
- last4: '4242',
- },
- type: 'card',
- },
- outcome: {
- risk_level: 'normal',
- },
- } as any );
-
-describe( 'DisputeDetails', () => {
- beforeEach( () => {
- // mock Date.now that moment library uses to get current date for testing purposes
- Date.now = jest.fn( () =>
- new Date( '2023-09-08T12:33:37.000Z' ).getTime()
- );
- } );
- afterEach( () => {
- // roll it back
- Date.now = () => new Date().getTime();
- } );
- test( 'correctly renders dispute details', () => {
- const charge = getBaseCharge();
- render( );
-
- // Expect this warning to be logged to the console
- expect( console ).toHaveWarnedWith(
- 'List with items prop is deprecated is deprecated and will be removed in version 9.0.0. Note: See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.'
- );
-
- // Dispute Notice
- screen.getByText(
- /The cardholder claims this is an unauthorized transaction/,
- { ignore: '.a11y-speak-region' }
- );
-
- // Don't render the staged evidence message
- expect(
- screen.queryByText(
- /You initiated a dispute a challenge to this dispute/,
- { ignore: '.a11y-speak-region' }
- )
- ).toBeNull();
-
- // Dispute Summary Row
- expect(
- screen.getByText( /Dispute Amount/i ).nextSibling
- ).toHaveTextContent( /\$68.00/ );
- expect(
- screen.getByText( /Disputed On/i ).nextSibling
- ).toHaveTextContent( /Aug 30, 2023/ );
- expect( screen.getByText( /Reason/i ).nextSibling ).toHaveTextContent(
- /Transaction unauthorized/
- );
- expect(
- screen.getByText( /Respond By/i ).nextSibling
- ).toHaveTextContent( /Sep 9, 2023/ );
- } );
-
- test( 'correctly renders dispute details for a dispute with staged evidence', () => {
- const charge = getBaseCharge();
- charge.dispute.evidence_details = {
- has_evidence: true,
- due_by: 1694303999,
- past_due: false,
- submission_count: 0,
- };
-
- render( );
-
- screen.getByText(
- /The cardholder claims this is an unauthorized transaction/,
- { ignore: '.a11y-speak-region' }
- );
-
- // Render the staged evidence message
- screen.getByText( /You initiated a challenge to this dispute/, {
- ignore: '.a11y-speak-region',
- } );
- } );
-} );
diff --git a/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap b/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap
index 8ffb7ce60ec..d02d14d3476 100644
--- a/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap
+++ b/client/payment-details/order-details/test/__snapshots__/index.test.tsx.snap
@@ -45,7 +45,7 @@ exports[`Order details page should match the snapshot - Charge without payment i
>
- Fee:
+ Fees:
-$0.00
diff --git a/client/payment-details/payment-method/affirm/index.js b/client/payment-details/payment-method/affirm/index.js
new file mode 100644
index 00000000000..b53a55a0168
--- /dev/null
+++ b/client/payment-details/payment-method/affirm/index.js
@@ -0,0 +1,91 @@
+/** @format **/
+
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import Detail from '../detail';
+
+/**
+ * Extracts and formats payment method details from a charge.
+ *
+ * @param {Object} charge The charge object.
+ * @return {Object} A flat hash of all necessary values.
+ */
+const formatPaymentMethodDetails = ( charge ) => {
+ const { billing_details: billingDetails, payment_method: id } = charge;
+
+ const { name, email, formatted_address: formattedAddress } = billingDetails;
+
+ return {
+ id,
+ name,
+ email,
+ formattedAddress,
+ };
+};
+
+/**
+ * Placeholders to display while loading.
+ */
+const paymentMethodPlaceholders = {
+ id: 'id placeholder',
+ name: 'name placeholder',
+ email: 'email placeholder',
+ formattedAddress: 'address placeholder',
+};
+
+const CardDetails = ( { charge = {}, isLoading } ) => {
+ const details =
+ charge && charge.payment_method_details
+ ? formatPaymentMethodDetails( charge )
+ : paymentMethodPlaceholders;
+
+ const { id, name, email, formattedAddress } = details;
+
+ return (
+
+
+
+ { !! id ? id : '–' }
+
+
+
+
+
+ { name || '–' }
+
+
+
+ { email || '–' }
+
+
+
+
+
+
+
+ );
+};
+
+export default CardDetails;
diff --git a/client/payment-details/payment-method/afterpay-clearpay/index.js b/client/payment-details/payment-method/afterpay-clearpay/index.js
new file mode 100644
index 00000000000..99b6f20dc26
--- /dev/null
+++ b/client/payment-details/payment-method/afterpay-clearpay/index.js
@@ -0,0 +1,90 @@
+/** @format **/
+
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import Detail from '../detail';
+
+/**
+ * Extracts and formats payment method details from a charge.
+ *
+ * @param {Object} charge The charge object.
+ * @return {Object} A flat hash of all necessary values.
+ */
+const formatPaymentMethodDetails = ( charge ) => {
+ const { billing_details: billingDetails, payment_method: id } = charge;
+
+ const { name, email, formatted_address: formattedAddress } = billingDetails;
+
+ return {
+ id,
+ name,
+ email,
+ formattedAddress,
+ };
+};
+
+/**
+ * Placeholders to display while loading.
+ */
+const paymentMethodPlaceholders = {
+ id: 'id placeholder',
+ name: 'name placeholder',
+ email: 'email placeholder',
+ formattedAddress: 'address placeholder',
+};
+
+const CardDetails = ( { charge = {}, isLoading } ) => {
+ const details =
+ charge && charge.payment_method_details
+ ? formatPaymentMethodDetails( charge )
+ : paymentMethodPlaceholders;
+
+ const { id, name, email, formattedAddress } = details;
+
+ return (
+
+
+
+ { !! id ? id : '–' }
+
+
+
+
+ { name || '–' }
+
+
+
+ { email || '–' }
+
+
+
+
+
+
+
+ );
+};
+
+export default CardDetails;
diff --git a/client/payment-details/payment-method/card/index.js b/client/payment-details/payment-method/card/index.js
index 718e536e092..15893f0e299 100644
--- a/client/payment-details/payment-method/card/index.js
+++ b/client/payment-details/payment-method/card/index.js
@@ -52,7 +52,9 @@ const formatPaymentMethodDetails = ( charge ) => {
? sprintf(
// Translators: %1$s card brand, %2$s card funding (prepaid, credit, etc.).
__( '%1$s %2$s card', 'woocommerce-payments' ),
- network.charAt( 0 ).toUpperCase() + network.slice( 1 ), // Brand
+ network === 'jcb'
+ ? network.toUpperCase()
+ : network.charAt( 0 ).toUpperCase() + network.slice( 1 ), // Brand
fundingTypes[ funding ]
)
: undefined;
diff --git a/client/payment-details/payment-method/index.js b/client/payment-details/payment-method/index.js
index 11fde70c853..984ab87772b 100644
--- a/client/payment-details/payment-method/index.js
+++ b/client/payment-details/payment-method/index.js
@@ -3,32 +3,38 @@
/**
* External dependencies
*/
-import { __ } from '@wordpress/i18n';
import { Card, CardBody, CardHeader } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
/**
* Internal dependencies.
*/
import Loadable from 'components/loadable';
-import CardDetails from './card';
-import CardPresentDetails from './card-present';
+import AffirmDetails from './affirm';
+import AfterpayClearpayDetails from './afterpay-clearpay';
import BancontactDetails from './bancontact';
import BecsDetails from './becs';
+import CardDetails from './card';
+import CardPresentDetails from './card-present';
import EpsDetails from './eps';
import GiropayDetails from './giropay';
import IdealDetails from './ideal';
+import KlarnaDetails from './klarna';
import P24Details from './p24';
import SepaDetails from './sepa';
import SofortDetails from './sofort';
const detailsComponentMap = {
- card: CardDetails,
- card_present: CardPresentDetails,
+ affirm: AffirmDetails,
+ afterpay_clearpay: AfterpayClearpayDetails,
au_becs_debit: BecsDetails,
bancontact: BancontactDetails,
+ card: CardDetails,
+ card_present: CardPresentDetails,
eps: EpsDetails,
giropay: GiropayDetails,
ideal: IdealDetails,
+ klarna: KlarnaDetails,
p24: P24Details,
sepa_debit: SepaDetails,
sofort: SofortDetails,
diff --git a/client/payment-details/payment-method/klarna/index.js b/client/payment-details/payment-method/klarna/index.js
new file mode 100644
index 00000000000..0f580f036c5
--- /dev/null
+++ b/client/payment-details/payment-method/klarna/index.js
@@ -0,0 +1,131 @@
+/** @format **/
+
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import Detail from '../detail';
+
+/**
+ * Extracts and formats payment method details from a charge.
+ *
+ * @param {Object} charge The charge object.
+ * @return {Object} A flat hash of all necessary values.
+ */
+const formatPaymentMethodDetails = ( charge ) => {
+ const { billing_details: billingDetails, payment_method: id } = charge;
+
+ const {
+ payment_method_category: paymentMethodCategory,
+ preferred_locale: preferredLocale,
+ } = charge.payment_method_details.klarna;
+
+ const paymentMethodCategoryTranslations = {
+ pay_later: __( 'pay_later', 'woocommerce-payments' ),
+ pay_now: __( 'pay_now', 'woocommerce-payments' ),
+ pay_with_financing: __( 'pay_with_financing', 'woocommerce-payments' ),
+ pay_in_installments: __(
+ 'pay_in_installments',
+ 'woocommerce-payments'
+ ),
+ };
+
+ const { name, email, formatted_address: formattedAddress } = billingDetails;
+
+ return {
+ id,
+ name,
+ email,
+ formattedAddress,
+ paymentMethodCategory:
+ paymentMethodCategoryTranslations[ paymentMethodCategory ],
+ preferredLocale,
+ };
+};
+
+/**
+ * Placeholders to display while loading.
+ */
+const paymentMethodPlaceholders = {
+ id: 'id placeholder',
+ name: 'name placeholder',
+ email: 'email placeholder',
+ formattedAddress: 'address placeholder',
+ paymentMethodCategory: 'category placeholder',
+ preferredLocale: 'locale placeholder',
+};
+
+const KlarnaDetails = ( { charge = {}, isLoading } ) => {
+ const details = charge.payment_method_details
+ ? formatPaymentMethodDetails( charge )
+ : paymentMethodPlaceholders;
+
+ const {
+ id,
+ name,
+ email,
+ formattedAddress,
+ paymentMethodCategory,
+ preferredLocale,
+ } = details;
+
+ return (
+
+
+
+ { id }
+
+
+
+ { paymentMethodCategory }
+
+
+
+ { preferredLocale }
+
+
+
+
+
+ { name }
+
+
+
+ { email }
+
+
+
+
+
+
+
+ );
+};
+
+export default KlarnaDetails;
diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx
index e75f69f2caf..a5ebb9ebf5d 100644
--- a/client/payment-details/summary/index.tsx
+++ b/client/payment-details/summary/index.tsx
@@ -5,10 +5,17 @@
*/
import { __ } from '@wordpress/i18n';
import { dateI18n } from '@wordpress/date';
-import { Card, CardBody, CardFooter, CardDivider } from '@wordpress/components';
+import {
+ Card,
+ CardBody,
+ CardFooter,
+ CardDivider,
+ Flex,
+} from '@wordpress/components';
import moment from 'moment';
import React, { useContext } from 'react';
import { createInterpolateElement } from '@wordpress/element';
+import HelpOutlineIcon from 'gridicons/dist/help-outline';
/**
* Internal dependencies.
@@ -28,6 +35,12 @@ import riskMappings from 'components/risk-level/strings';
import OrderLink from 'components/order-link';
import { formatCurrency, formatExplicitCurrency } from 'utils/currency';
import CustomerLink from 'components/customer-link';
+import { ClickTooltip } from 'components/tooltip';
+import DisputeStatusChip from 'components/dispute-status-chip';
+import {
+ getDisputeFeeFormatted,
+ isAwaitingResponse,
+} from 'wcpay/disputes/utils';
import { useAuthorization } from 'wcpay/data';
import CaptureAuthorizationButton from 'wcpay/components/capture-authorization-button';
import './style.scss';
@@ -37,7 +50,9 @@ import WCPaySettingsContext from '../../settings/wcpay-settings-context';
import { FraudOutcome } from '../../types/fraud-outcome';
import CancelAuthorizationButton from '../../components/cancel-authorization-button';
import { PaymentIntent } from '../../types/payment-intents';
-import DisputeDetails from '../dispute-details';
+import DisputeAwaitingResponseDetails from '../dispute-details/dispute-awaiting-response-details';
+import DisputeResolutionFooter from '../dispute-details/dispute-resolution-footer';
+import ErrorBoundary from 'components/error-boundary';
declare const window: any;
@@ -154,10 +169,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
charge.currency && balance.currency !== charge.currency;
const {
- featureFlags: {
- isAuthAndCaptureEnabled,
- isDisputeOnTransactionPageEnabled,
- },
+ featureFlags: { isAuthAndCaptureEnabled },
} = useContext( WCPaySettingsContext );
// We should only fetch the authorization data if the payment is marked for manual capture and it is not already captured.
@@ -177,6 +189,20 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
const isFraudOutcomeReview = isOnHoldByFraudTools( charge, paymentIntent );
+ const disputeFee =
+ charge.dispute && getDisputeFeeFormatted( charge.dispute );
+
+ // Use the balance_transaction fee if available. If not (e.g. authorized but not captured), use the application_fee_amount.
+ const transactionFee = charge.balance_transaction
+ ? {
+ fee: charge.balance_transaction.fee,
+ currency: charge.balance_transaction.currency,
+ }
+ : {
+ fee: charge.application_fee_amount,
+ currency: charge.currency,
+ };
+
// WP translation strings are injected into Moment.js for relative time terms, since Moment's own translation library increases the bundle size significantly.
moment.updateLocale( 'en', {
relativeTime: {
@@ -209,12 +235,23 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
{ charge.currency || 'USD' }
-
+ { charge.dispute ? (
+
+ ) : (
+
+ ) }
@@ -228,10 +265,17 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
) : null }
{ balance.refunded ? (
- { `${ __(
- 'Refunded',
- 'woocommerce-payments'
- ) }: ` }
+ { `${
+ disputeFee
+ ? __(
+ 'Deducted',
+ 'woocommerce-payments'
+ )
+ : __(
+ 'Refunded',
+ 'woocommerce-payments'
+ )
+ }: ` }
{ formatExplicitCurrency(
-balance.refunded,
balance.currency
@@ -246,13 +290,66 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
placeholder="Fee amount"
>
{ `${ __(
- 'Fee',
+ 'Fees',
'woocommerce-payments'
) }: ` }
{ formatCurrency(
-balance.fee,
balance.currency
) }
+ { disputeFee && (
+ }
+ buttonLabel={ __(
+ 'Fee breakdown',
+ 'woocommerce-payments'
+ ) }
+ content={
+ <>
+
+
+ { __(
+ 'Transaction fee',
+ 'woocommerce-payments'
+ ) }
+
+
+ { formatCurrency(
+ transactionFee.fee,
+ transactionFee.currency
+ ) }
+
+
+
+
+ { __(
+ 'Dispute fee',
+ 'woocommerce-payments'
+ ) }
+
+
+ { disputeFee }
+
+
+
+
+ { __(
+ 'Total fees',
+ 'woocommerce-payments'
+ ) }
+
+
+ { formatCurrency(
+ balance.fee,
+ balance.currency
+ ) }
+
+
+ >
+ }
+ />
+ ) }
{ charge.paydown ? (
@@ -375,9 +472,22 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {
/>
- { isDisputeOnTransactionPageEnabled && charge.dispute && (
-
+
+ { charge.dispute && (
+
+ { isAwaitingResponse( charge.dispute.status ) ? (
+
+ ) : (
+
+ ) }
+
) }
+
{ isAuthAndCaptureEnabled &&
authorization &&
! authorization.captured && (
diff --git a/client/payment-details/summary/style.scss b/client/payment-details/summary/style.scss
index 7f15b50bfe0..cbe0450fb1e 100755
--- a/client/payment-details/summary/style.scss
+++ b/client/payment-details/summary/style.scss
@@ -37,13 +37,35 @@
.payment-details-summary__breakdown {
p {
@include font-size( 14 );
- color: $gray-50;
- display: inline-block;
+ color: $gray-700;
+ display: inline-flex;
margin: 0.25rem 1rem 0 0;
+ text-transform: uppercase;
+ font-weight: 600;
+ font-size: 12px;
}
p:last-child {
margin-right: 0;
}
+
+ &__fee-tooltip {
+ display: flex;
+ flex-direction: column;
+ padding: $grid-unit-15;
+ gap: $grid-unit-10;
+ font-weight: 400;
+ font-size: 14px;
+
+ & > *:last-child {
+ padding-top: $grid-unit-10;
+ border-top: 1px solid $gray-200;
+ }
+
+ label {
+ margin-right: $grid-unit-20;
+ color: $gray-700;
+ }
+ }
}
.payment-details-summary__fraud-outcome-action {
diff --git a/client/payment-details/summary/test/__snapshots__/index.tsx.snap b/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap
similarity index 96%
rename from client/payment-details/summary/test/__snapshots__/index.tsx.snap
rename to client/payment-details/summary/test/__snapshots__/index.test.tsx.snap
index 9dd86ba7c81..4a3bcae9a5c 100644
--- a/client/payment-details/summary/test/__snapshots__/index.tsx.snap
+++ b/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap
@@ -41,7 +41,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders ca
>
- Fee:
+ Fees:
$-0.70
@@ -144,7 +144,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders ca
>
Customer name
@@ -265,7 +265,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders ca
this charge within the next
7 days
@@ -342,7 +342,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th
>
- Fee:
+ Fees:
$-0.70
@@ -461,7 +461,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th
>
Customer name
@@ -582,7 +582,7 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th
this charge within the next
7 days
@@ -650,7 +650,7 @@ exports[`PaymentDetailsSummary correctly renders a charge 1`] = `
>
- Fee:
+ Fees:
$-0.70
@@ -753,7 +753,7 @@ exports[`PaymentDetailsSummary correctly renders a charge 1`] = `
>
Customer name
@@ -910,7 +910,7 @@ exports[`PaymentDetailsSummary renders a charge with subscriptions 1`] = `
>
- Fee:
+ Fees:
$-0.70
@@ -1013,7 +1013,7 @@ exports[`PaymentDetailsSummary renders a charge with subscriptions 1`] = `
>
Customer name
@@ -1200,7 +1200,7 @@ exports[`PaymentDetailsSummary renders fully refunded information for a charge 1
$-20.00
- Fee:
+ Fees:
$-0.70
@@ -1303,7 +1303,7 @@ exports[`PaymentDetailsSummary renders fully refunded information for a charge 1
>
Customer name
@@ -1458,7 +1458,7 @@ exports[`PaymentDetailsSummary renders loading state 1`] = `
>
- Fee:
+ Fees:
$0.00
@@ -1696,7 +1696,7 @@ exports[`PaymentDetailsSummary renders partially refunded information for a char
$-12.00
- Fee:
+ Fees:
$-0.70
@@ -1799,7 +1799,7 @@ exports[`PaymentDetailsSummary renders partially refunded information for a char
>
Customer name
@@ -1956,7 +1956,7 @@ exports[`PaymentDetailsSummary renders the Tap to Pay channel from metadata 1`]
>
- Fee:
+ Fees:
$-0.70
@@ -2059,7 +2059,7 @@ exports[`PaymentDetailsSummary renders the Tap to Pay channel from metadata 1`]
>
Customer name
@@ -2175,7 +2175,7 @@ exports[`PaymentDetailsSummary renders the Tap to Pay channel from metadata 1`]
`;
-exports[`PaymentDetailsSummary renders the information of a disputed charge 1`] = `
+exports[`PaymentDetailsSummary renders the information of a dispute-reversal charge 1`] = `
- Disputed: Under review
+ Disputed: Won
+
- Refunded:
- $-15.00
-
-
- Fee:
- $-15.70
+ Fees:
+ $-0.70
Net:
- $-10.70
+ $19.30
@@ -2322,7 +2319,7 @@ exports[`PaymentDetailsSummary renders the information of a disputed charge 1`]
>
Customer name
@@ -2421,6 +2418,50 @@ exports[`PaymentDetailsSummary renders the information of a disputed charge 1`]
+
;
+ connect: {
+ country: string;
+ };
+ featureFlags: {
+ isAuthAndCaptureEnabled: boolean;
+ };
+ };
+};
+
+const mockDisputeDoAccept = jest.fn();
+
+jest.mock( 'wcpay/data', () => ( {
+ useAuthorization: jest.fn( () => ( {
+ authorization: null,
+ } ) ),
+ useDisputeAccept: jest.fn( () => ( {
+ doAccept: mockDisputeDoAccept,
+ isLoading: false,
+ } ) ),
+} ) );
+
+jest.mock( '@wordpress/data', () => ( {
+ createRegistryControl: jest.fn(),
+ dispatch: jest.fn( () => ( {
+ setIsMatching: jest.fn(),
+ onLoad: jest.fn(),
+ } ) ),
+ registerStore: jest.fn(),
+ select: jest.fn(),
+ useDispatch: jest.fn( () => ( {
+ createErrorNotice: jest.fn(),
+ } ) ),
+ useSelect: jest.fn( () => ( { getNotices: jest.fn() } ) ),
+ withDispatch: jest.fn( () => jest.fn() ),
+ withSelect: jest.fn( () => jest.fn() ),
+} ) );
+
+const mockUseAuthorization = useAuthorization as jest.MockedFunction<
+ typeof useAuthorization
+>;
+
+const getBaseCharge = (): Charge =>
+ ( {
+ id: 'ch_38jdHA39KKA',
+ /* Stripe data comes in seconds, instead of the default Date milliseconds */
+ created: Date.parse( 'Sep 19, 2019, 5:24 pm' ) / 1000,
+ amount: 2000,
+ amount_refunded: 0,
+ application_fee_amount: 70,
+ disputed: false,
+ dispute: null,
+ currency: 'usd',
+ type: 'charge',
+ status: 'succeeded',
+ paid: true,
+ captured: true,
+ balance_transaction: {
+ amount: 2000,
+ currency: 'usd',
+ fee: 70,
+ },
+ refunds: {
+ data: [],
+ },
+ order: {
+ number: 45981,
+ url: 'https://somerandomorderurl.com/?edit_order=45981',
+ },
+ billing_details: {
+ name: 'Customer name',
+ email: 'mock@example.com',
+ },
+ payment_method_details: {
+ card: {
+ brand: 'visa',
+ last4: '4242',
+ },
+ type: 'card',
+ },
+ outcome: {
+ risk_level: 'normal',
+ },
+ } as any );
+
+const getBaseDispute = (): Dispute =>
+ ( {
+ id: 'dp_1',
+ amount: 2000,
+ charge: 'ch_38jdHA39KKA',
+ order: null,
+ balance_transactions: [
+ {
+ amount: -2000,
+ currency: 'usd',
+ fee: 1500,
+ reporting_category: 'dispute',
+ },
+ ],
+ created: 1693453017,
+ currency: 'usd',
+ evidence: {
+ billing_address: '123 test address',
+ customer_email_address: 'test@email.com',
+ customer_name: 'Test customer',
+ shipping_address: '123 test address',
+ },
+ evidence_details: {
+ due_by: 1694303999,
+ has_evidence: false,
+ past_due: false,
+ submission_count: 0,
+ },
+ issuer_evidence: null,
+ metadata: {},
+ payment_intent: 'pi_1',
+ reason: 'fraudulent',
+ status: 'needs_response',
+ } as Dispute );
+
+const getBaseMetadata = () => ( {
+ platform: 'ios',
+ reader_id: 'APPLEBUILTINSIMULATOR-1',
+ reader_model: 'COTS_DEVICE',
+} );
+
+function renderCharge(
+ charge: Charge,
+ metadata = {},
+ isLoading = false,
+ props = {}
+) {
+ const { container } = render(
+
+ );
+ return container;
+}
+
+describe( 'PaymentDetailsSummary', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+
+ global.wcpaySettings = {
+ isSubscriptionsActive: false,
+ shouldUseExplicitPrice: false,
+ zeroDecimalCurrencies: [ 'jpy' ],
+ connect: {
+ country: 'US',
+ },
+ featureFlags: {
+ isAuthAndCaptureEnabled: true,
+ },
+ currencyData: {
+ US: {
+ code: 'USD',
+ symbol: '$',
+ symbolPosition: 'left',
+ thousandSeparator: ',',
+ decimalSeparator: '.',
+ precision: 2,
+ },
+ JP: {
+ code: 'JPY',
+ symbol: '¥',
+ symbolPosition: 'left',
+ thousandSeparator: ',',
+ decimalSeparator: '.',
+ precision: 0,
+ },
+ },
+ };
+
+ // mock Date.now that moment library uses to get current date for testing purposes
+ Date.now = jest.fn( () =>
+ new Date( '2023-09-08T12:33:37.000Z' ).getTime()
+ );
+ } );
+
+ afterEach( () => {
+ Date.now = () => new Date().getTime();
+ } );
+
+ test( 'correctly renders a charge', () => {
+ expect( renderCharge( getBaseCharge() ) ).toMatchSnapshot();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ expect( console ).toHaveWarnedWith(
+ 'List with items prop is deprecated is deprecated and will be removed in version 9.0.0. Note: See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.'
+ );
+ } );
+
+ test( 'renders partially refunded information for a charge', () => {
+ const charge = getBaseCharge();
+ charge.refunded = false;
+ charge.amount_refunded = 1200;
+ charge.refunds?.data.push( {
+ balance_transaction: {
+ amount: -charge.amount_refunded,
+ currency: 'usd',
+ } as any,
+ } );
+
+ expect( renderCharge( charge ) ).toMatchSnapshot();
+ } );
+
+ test( 'renders fully refunded information for a charge', () => {
+ const charge = getBaseCharge();
+ charge.refunded = true;
+ charge.amount_refunded = 2000;
+ charge.refunds?.data.push( {
+ balance_transaction: {
+ amount: -charge.amount_refunded,
+ currency: 'usd',
+ } as any,
+ } );
+
+ const container = renderCharge( charge );
+ screen.getByText( /Refunded: \$-20.00/i );
+ expect( container ).toMatchSnapshot();
+ } );
+
+ test( 'renders the Tap to Pay channel from metadata', () => {
+ const charge = getBaseCharge();
+ const metadata = getBaseMetadata();
+
+ expect( renderCharge( charge, metadata ) ).toMatchSnapshot();
+ } );
+
+ test( 'renders a charge with subscriptions', () => {
+ global.wcpaySettings.isSubscriptionsActive = true;
+
+ const charge = getBaseCharge();
+ if ( charge.order ) {
+ charge.order.subscriptions = [
+ {
+ number: 246,
+ url: 'https://example.com/subscription/246',
+ },
+ ];
+ }
+
+ expect( renderCharge( charge ) ).toMatchSnapshot();
+ } );
+
+ test( 'renders loading state', () => {
+ expect( renderCharge( {} as any, true ) ).toMatchSnapshot();
+ } );
+
+ describe( 'capture notification and fraud buttons', () => {
+ beforeAll( () => {
+ // Mock current date and time to fixed value in moment
+ const fixedCurrentDate = new Date( '2023-01-01T01:00:00.000Z' );
+ jest.spyOn( Date, 'now' ).mockImplementation( () =>
+ fixedCurrentDate.getTime()
+ );
+ } );
+
+ afterAll( () => {
+ jest.spyOn( Date, 'now' ).mockRestore();
+ } );
+
+ test( 'renders capture section correctly', () => {
+ mockUseAuthorization.mockReturnValueOnce( {
+ authorization: {
+ captured: false,
+ charge_id: 'ch_mock',
+ amount: 1000,
+ currency: 'usd',
+ created: moment.utc().format(),
+ order_id: 123,
+ risk_level: 1,
+ customer_country: 'US',
+ customer_email: 'test@example.com',
+ customer_name: 'Test Customer',
+ payment_intent_id: 'pi_mock',
+ },
+ isLoading: false,
+ isRequesting: false,
+ doCaptureAuthorization: jest.fn(),
+ doCancelAuthorization: jest.fn(),
+ } );
+ const charge = getBaseCharge();
+ charge.captured = false;
+
+ const container = renderCharge( charge );
+
+ expect(
+ screen.getByRole( 'button', { name: /Capture/i } )
+ ).toBeInTheDocument();
+
+ expect( container ).toMatchSnapshot();
+ } );
+
+ test( 'renders the fraud outcome buttons', () => {
+ mockUseAuthorization.mockReturnValueOnce( {
+ authorization: {
+ captured: false,
+ charge_id: 'ch_mock',
+ amount: 1000,
+ currency: 'usd',
+ created: new Date( Date.now() ).toISOString(),
+ order_id: 123,
+ risk_level: 1,
+ customer_country: 'US',
+ customer_email: 'test@example.com',
+ customer_name: 'Test Customer',
+ payment_intent_id: 'pi_mock',
+ },
+ isLoading: false,
+ isRequesting: false,
+ doCaptureAuthorization: jest.fn(),
+ doCancelAuthorization: jest.fn(),
+ } );
+ const charge = getBaseCharge();
+ charge.captured = false;
+
+ const container = renderCharge( charge, {}, false, {
+ paymentIntent: paymentIntentMock,
+ } );
+
+ expect(
+ screen.getByRole( 'button', { name: /Approve Transaction/i } )
+ ).toBeInTheDocument();
+
+ expect(
+ screen.getByRole( 'button', { name: /Block Transaction/i } )
+ ).toBeInTheDocument();
+
+ expect(
+ screen.queryByRole( 'button', { name: /Capture/i } )
+ ).not.toBeInTheDocument();
+
+ expect(
+ screen.getByText(
+ /Approving this transaction will capture the charge./
+ )
+ ).toBeInTheDocument();
+
+ expect( container ).toMatchSnapshot();
+ } );
+ } );
+
+ test( 'renders the information of a disputed charge', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'needs_response';
+
+ renderCharge( charge );
+
+ // Dispute Notice
+ screen.getByText(
+ /The cardholder claims this is an unauthorized transaction/,
+ { ignore: '.a11y-speak-region' }
+ );
+
+ // Don't render the staged evidence message
+ expect(
+ screen.queryByText( /You initiated a challenge to this dispute/, {
+ ignore: '.a11y-speak-region',
+ } )
+ ).toBeNull();
+
+ // Dispute Summary Row
+ expect(
+ screen.getByText( /Dispute Amount/i ).nextSibling
+ ).toHaveTextContent( /\$20.00/ );
+ expect(
+ screen.getByText( /Disputed On/i ).nextSibling
+ ).toHaveTextContent( /Aug 30, 2023/ );
+ expect( screen.getByText( /Reason/i ).nextSibling ).toHaveTextContent(
+ /Transaction unauthorized/
+ );
+ expect(
+ screen.getByText( /Respond By/i ).nextSibling
+ ).toHaveTextContent( /Sep 9, 2023/ );
+
+ // Steps to resolve
+ screen.getByText( /Steps to resolve/i );
+ screen.getByRole( 'link', {
+ name: /Email the customer/i,
+ } );
+ screen.getByRole( 'link', {
+ name: /in withdrawing their dispute/i,
+ } );
+
+ // Actions
+ screen.getByRole( 'button', {
+ name: /Challenge dispute/,
+ } );
+ screen.getByRole( 'button', {
+ name: /Accept dispute/,
+ } );
+ } );
+
+ test( 'renders the information of a disputed charge when the store/charge currency differ', () => {
+ // True when multi-currency is enabled.
+ global.wcpaySettings.shouldUseExplicitPrice = true;
+
+ // In this case, charge currency is JPY, but store currency is NOK.
+ const charge = getBaseCharge();
+ charge.currency = 'jpy';
+ charge.amount = 10000;
+ charge.balance_transaction = {
+ amount: 72581,
+ currency: 'nok',
+ reporting_category: 'charge',
+ fee: 4152,
+ };
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'needs_response';
+ charge.dispute.amount = 10000;
+ charge.dispute.currency = 'jpy';
+ charge.dispute.balance_transactions = [
+ {
+ amount: -72581,
+ currency: 'nok',
+ fee: 15000,
+ reporting_category: 'dispute',
+ },
+ ];
+ renderCharge( charge );
+
+ // Disputed amount should show the shopper/transaction currency.
+ expect(
+ screen.getByText( /Dispute Amount/i ).nextSibling
+ ).toHaveTextContent( /¥10,000 JPY/i );
+ } );
+
+ test( 'renders the information of a dispute-reversal charge', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'won';
+
+ charge.dispute.balance_transactions = [
+ {
+ amount: -2000,
+ fee: 1500,
+ currency: 'usd',
+ reporting_category: 'dispute',
+ },
+ {
+ amount: 2000,
+ fee: -1500,
+ currency: 'usd',
+ reporting_category: 'dispute_reversal',
+ },
+ ];
+
+ const container = renderCharge( charge );
+ expect(
+ screen.queryByText( /Deducted: \$-15.00/i )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Fee breakdown/i,
+ } )
+ ).not.toBeInTheDocument();
+ expect( container ).toMatchSnapshot();
+ } );
+
+ test( 'renders the fee breakdown tooltip of a disputed charge', () => {
+ const charge = {
+ ...getBaseCharge(),
+ currency: 'jpy',
+ amount: 10000,
+ balance_transaction: {
+ amount: 2000,
+ currency: 'usd',
+ fee: 70,
+ },
+ disputed: true,
+ dispute: {
+ ...getBaseDispute(),
+ amount: 10000,
+ status: 'under_review',
+ balance_transactions: [
+ {
+ amount: -1500,
+ fee: 1500,
+ currency: 'usd',
+ reporting_category: 'dispute',
+ },
+ ],
+ } as Dispute,
+ };
+
+ renderCharge( charge );
+
+ // Open tooltip content
+ const tooltipButton = screen.getByRole( 'button', {
+ name: /Fee breakdown/i,
+ } );
+ userEvent.click( tooltipButton );
+
+ // Check fee breakdown calculated correctly
+ const tooltipContent = screen.getByRole( 'tooltip' );
+ expect(
+ within( tooltipContent ).getByLabelText( /Transaction fee/ )
+ ).toHaveTextContent( /\$0.70/ );
+
+ expect(
+ within( tooltipContent ).getByLabelText( /Dispute fee/ )
+ ).toHaveTextContent( /\$15.00/ );
+
+ expect(
+ within( tooltipContent ).getByLabelText( /Total fees/ )
+ ).toHaveTextContent( /\$15.70/ );
+ } );
+
+ test( 'renders the information of an inquiry when the store/charge currency differ', () => {
+ // True when multi-currency is enabled.
+ global.wcpaySettings.shouldUseExplicitPrice = true;
+
+ // In this case, charge currency is JPY, but store currency is NOK.
+ const charge = getBaseCharge();
+ charge.currency = 'jpy';
+ charge.amount = 10000;
+ charge.balance_transaction = {
+ amount: 72581,
+ currency: 'nok',
+ reporting_category: 'charge',
+ fee: 4152,
+ };
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'warning_needs_response';
+ charge.dispute.amount = 10000;
+ charge.dispute.currency = 'jpy';
+ // Inquiries don't have balance transactions.
+ charge.dispute.balance_transactions = [];
+ renderCharge( charge );
+
+ // Disputed amount should show the shopper/transaction currency.
+ expect(
+ screen.getByText( /Dispute Amount/i ).nextSibling
+ ).toHaveTextContent( /¥10,000 JPY/i );
+ } );
+
+ test( 'correctly renders dispute details for a dispute with staged evidence', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'needs_response';
+ charge.dispute.evidence_details = {
+ has_evidence: true,
+ due_by: 1694303999,
+ past_due: false,
+ submission_count: 0,
+ };
+
+ renderCharge( charge );
+
+ screen.getByText(
+ /The cardholder claims this is an unauthorized transaction/,
+ { ignore: '.a11y-speak-region' }
+ );
+
+ // Render the staged evidence message
+ screen.getByText( /You initiated a challenge to this dispute/, {
+ ignore: '.a11y-speak-region',
+ } );
+
+ screen.getByRole( 'button', {
+ name: /Continue with challenge/,
+ } );
+ } );
+
+ test( 'correctly renders the accept dispute modal and accepts', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'needs_response';
+
+ renderCharge( charge );
+
+ const openModalButton = screen.getByRole( 'button', {
+ name: /Accept dispute/,
+ } );
+
+ // Open the modal
+ openModalButton.click();
+
+ screen.getByRole( 'heading', {
+ name: /Accept the dispute?/,
+ } );
+ screen.getByText( /\$15.00 dispute fee/, {
+ ignore: '.a11y-speak-region',
+ } );
+
+ screen.getByRole( 'button', {
+ name: /Cancel/,
+ } );
+ const acceptButton = screen.getByRole( 'button', {
+ name: /Accept dispute/,
+ } );
+
+ // Accept the dispute
+ acceptButton.click();
+
+ expect( mockDisputeDoAccept ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ test( 'navigates to the dispute challenge screen when the challenge button is clicked', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'needs_response';
+ charge.dispute.id = 'dp_test123';
+
+ renderCharge( charge );
+
+ const challengeButton = screen.getByRole( 'button', {
+ name: /Challenge dispute/,
+ } );
+
+ challengeButton.click();
+
+ expect( window.location.href ).toContain(
+ `admin.php?page=wc-admin&path=%2Fpayments%2Fdisputes%2Fchallenge&id=${ charge.dispute.id }`
+ );
+ } );
+
+ test( 'correctly renders dispute details for "won" disputes', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'won';
+ charge.dispute.metadata.__evidence_submitted_at = '1693400000';
+ renderCharge( charge );
+
+ screen.getByText( /You won this dispute on/i, {
+ ignore: '.a11y-speak-region',
+ } );
+ screen.getByRole( 'button', { name: /View dispute details/i } );
+
+ // No actions or steps rendered
+ expect( screen.queryByText( /Steps to resolve/i ) ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Challenge/i,
+ } )
+ ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Accept/i,
+ } )
+ ).toBeNull();
+ } );
+
+ test( 'correctly renders dispute details for "under_review" disputes', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'under_review';
+ charge.dispute.metadata.__evidence_submitted_at = '1693400000';
+
+ renderCharge( charge );
+
+ screen.getByText( /You submitted evidence for this dispute/i, {
+ ignore: '.a11y-speak-region',
+ } );
+ screen.getByRole( 'button', { name: /View submitted evidence/i } );
+
+ // No actions or steps rendered
+ expect( screen.queryByText( /Steps to resolve/i ) ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Challenge/i,
+ } )
+ ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Accept/i,
+ } )
+ ).toBeNull();
+ } );
+
+ test( 'correctly renders dispute details for "accepted" disputes', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'lost';
+ charge.dispute.metadata.__closed_by_merchant = '1';
+ charge.dispute.metadata.__dispute_closed_at = '1693453017';
+
+ renderCharge( charge );
+
+ screen.getByText( /This dispute was accepted/i, {
+ ignore: '.a11y-speak-region',
+ } );
+ // Check for the correct fee amount
+ screen.getByText( /\$15.00 fee/i, {
+ ignore: '.a11y-speak-region',
+ } );
+
+ // No actions or steps rendered
+ expect( screen.queryByText( /Steps to resolve/i ) ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Challenge/i,
+ } )
+ ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Accept/i,
+ } )
+ ).toBeNull();
+ } );
+
+ test( 'correctly renders dispute details for "lost" disputes', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'lost';
+ charge.dispute.metadata.__evidence_submitted_at = '1693400000';
+ charge.dispute.metadata.__dispute_closed_at = '1693453017';
+
+ renderCharge( charge );
+
+ screen.getByText( /This dispute was lost/i, {
+ ignore: '.a11y-speak-region',
+ } );
+ // Check for the correct fee amount
+ screen.getByText( /\$15.00 fee/i, {
+ ignore: '.a11y-speak-region',
+ } );
+ screen.getByRole( 'button', { name: /View dispute details/i } );
+
+ // No actions or steps rendered
+ expect( screen.queryByText( /Steps to resolve/i ) ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Challenge/i,
+ } )
+ ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Accept/i,
+ } )
+ ).toBeNull();
+ } );
+
+ test( 'correctly renders dispute details for "warning_needs_response" inquiry disputes', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'warning_needs_response';
+
+ renderCharge( charge );
+
+ // Dispute Notice
+ screen.getByText(
+ /The cardholder claims this is an unauthorized transaction/,
+ { ignore: '.a11y-speak-region' }
+ );
+
+ // Steps to resolve
+ screen.getByText( /Steps to resolve/i );
+ screen.getByRole( 'link', {
+ name: /Email the customer/i,
+ } );
+ screen.getByText( /Submit evidence /i );
+
+ // Actions
+ screen.getByRole( 'button', {
+ name: /Submit evidence$/i,
+ } );
+ screen.getByRole( 'button', {
+ name: /Issue refund/i,
+ } );
+ } );
+
+ test( 'correctly renders dispute details for "warning_under_review" inquiry disputes', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'warning_under_review';
+ charge.dispute.metadata.__evidence_submitted_at = '1693400000';
+
+ renderCharge( charge );
+
+ screen.getByText( /You submitted evidence for this inquiry/i, {
+ ignore: '.a11y-speak-region',
+ } );
+ screen.getByRole( 'button', { name: /View submitted evidence/i } );
+
+ // No actions rendered
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Challenge/i,
+ } )
+ ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Accept/i,
+ } )
+ ).toBeNull();
+ } );
+
+ test( 'correctly renders dispute details for "warning_closed" inquiry disputes', () => {
+ const charge = getBaseCharge();
+ charge.disputed = true;
+ charge.dispute = getBaseDispute();
+ charge.dispute.status = 'warning_closed';
+ charge.dispute.metadata.__evidence_submitted_at = '1693400000';
+ charge.dispute.metadata.__dispute_closed_at = '1693453017';
+
+ renderCharge( charge );
+
+ screen.getByText( /This inquiry was closed/i, {
+ ignore: '.a11y-speak-region',
+ } );
+ screen.getByRole( 'button', { name: /View submitted evidence/i } );
+
+ // No actions rendered
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Challenge/i,
+ } )
+ ).toBeNull();
+ expect(
+ screen.queryByRole( 'button', {
+ name: /Accept/i,
+ } )
+ ).toBeNull();
+ } );
+} );
diff --git a/client/payment-details/summary/test/index.tsx b/client/payment-details/summary/test/index.tsx
deleted file mode 100755
index 79c4eb47c7c..00000000000
--- a/client/payment-details/summary/test/index.tsx
+++ /dev/null
@@ -1,311 +0,0 @@
-/** @format */
-/**
- * External dependencies
- */
-import { render, screen } from '@testing-library/react';
-import React from 'react';
-import moment from 'moment';
-import '@wordpress/jest-console';
-/**
- * Internal dependencies
- */
-import PaymentDetailsSummary from '../';
-import { Charge } from 'wcpay/types/charges';
-import { useAuthorization } from 'wcpay/data';
-import { paymentIntentMock } from 'wcpay/data/payment-intents/test/hooks';
-
-declare const global: {
- wcSettings: {
- locale: {
- siteLocale: string;
- };
- };
- wcpaySettings: {
- isSubscriptionsActive: boolean;
- zeroDecimalCurrencies: string[];
- currencyData: Record< string, any >;
- connect: {
- country: string;
- };
- featureFlags: {
- isAuthAndCaptureEnabled: boolean;
- };
- };
-};
-
-jest.mock( 'wcpay/data', () => ( {
- useAuthorization: jest.fn( () => ( {
- authorization: null,
- } ) ),
-} ) );
-
-const mockUseAuthorization = useAuthorization as jest.MockedFunction<
- typeof useAuthorization
->;
-
-const getBaseCharge = (): Charge =>
- ( {
- id: 'ch_38jdHA39KKA',
- /* Stripe data comes in seconds, instead of the default Date milliseconds */
- created: Date.parse( 'Sep 19, 2019, 5:24 pm' ) / 1000,
- amount: 2000,
- amount_refunded: 0,
- application_fee_amount: 70,
- disputed: false,
- dispute: null,
- currency: 'usd',
- type: 'charge',
- status: 'succeeded',
- paid: true,
- captured: true,
- balance_transaction: {
- amount: 2000,
- currency: 'usd',
- fee: 70,
- },
- refunds: {
- data: [],
- },
- order: {
- number: 45981,
- url: 'https://somerandomorderurl.com/?edit_order=45981',
- },
- billing_details: {
- name: 'Customer name',
- },
- payment_method_details: {
- card: {
- brand: 'visa',
- last4: '4242',
- },
- type: 'card',
- },
- outcome: {
- risk_level: 'normal',
- },
- } as any );
-
-const getBaseMetadata = () => ( {
- platform: 'ios',
- reader_id: 'APPLEBUILTINSIMULATOR-1',
- reader_model: 'COTS_DEVICE',
-} );
-
-function renderCharge(
- charge: Charge,
- metadata = {},
- isLoading = false,
- props = {}
-) {
- const { container } = render(
-
- );
- return container;
-}
-
-describe( 'PaymentDetailsSummary', () => {
- beforeEach( () => {
- jest.clearAllMocks();
-
- global.wcpaySettings = {
- isSubscriptionsActive: false,
- zeroDecimalCurrencies: [],
- connect: {
- country: 'US',
- },
- featureFlags: {
- isAuthAndCaptureEnabled: true,
- },
- currencyData: {
- US: {
- code: 'USD',
- symbol: '$',
- symbolPosition: 'left',
- thousandSeparator: ',',
- decimalSeparator: '.',
- precision: 2,
- },
- },
- };
- } );
-
- test( 'correctly renders a charge', () => {
- expect( renderCharge( getBaseCharge() ) ).toMatchSnapshot();
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- expect( console ).toHaveWarnedWith(
- 'List with items prop is deprecated is deprecated and will be removed in version 9.0.0. Note: See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.'
- );
- } );
-
- test( 'renders partially refunded information for a charge', () => {
- const charge = getBaseCharge();
- charge.refunded = false;
- charge.amount_refunded = 1200;
- charge.refunds?.data.push( {
- balance_transaction: {
- amount: -charge.amount_refunded,
- currency: 'usd',
- } as any,
- } );
-
- expect( renderCharge( charge ) ).toMatchSnapshot();
- } );
-
- test( 'renders fully refunded information for a charge', () => {
- const charge = getBaseCharge();
- charge.refunded = true;
- charge.amount_refunded = 2000;
- charge.refunds?.data.push( {
- balance_transaction: {
- amount: -charge.amount_refunded,
- currency: 'usd',
- } as any,
- } );
-
- expect( renderCharge( charge ) ).toMatchSnapshot();
- } );
-
- test( 'renders the information of a disputed charge', () => {
- const charge = getBaseCharge();
- charge.disputed = true;
- charge.dispute = {
- amount: 1500,
- status: 'under_review',
- balance_transactions: [
- {
- amount: -1500,
- fee: 1500,
- } as any,
- ],
- } as any;
-
- expect( renderCharge( charge ) ).toMatchSnapshot();
- } );
-
- test( 'renders the Tap to Pay channel from metadata', () => {
- const charge = getBaseCharge();
- const metadata = getBaseMetadata();
-
- expect( renderCharge( charge, metadata ) ).toMatchSnapshot();
- } );
-
- test( 'renders a charge with subscriptions', () => {
- global.wcpaySettings.isSubscriptionsActive = true;
-
- const charge = getBaseCharge();
- if ( charge.order ) {
- charge.order.subscriptions = [
- {
- number: 246,
- url: 'https://example.com/subscription/246',
- },
- ];
- }
-
- expect( renderCharge( charge ) ).toMatchSnapshot();
- } );
-
- test( 'renders loading state', () => {
- expect( renderCharge( {} as any, true ) ).toMatchSnapshot();
- } );
-
- describe( 'capture notification and fraud buttons', () => {
- beforeAll( () => {
- // Mock current date and time to fixed value in moment
- const fixedCurrentDate = new Date( '2023-01-01T01:00:00.000Z' );
- jest.spyOn( Date, 'now' ).mockImplementation( () =>
- fixedCurrentDate.getTime()
- );
- } );
-
- afterAll( () => {
- jest.spyOn( Date, 'now' ).mockRestore();
- } );
-
- test( 'renders capture section correctly', () => {
- mockUseAuthorization.mockReturnValueOnce( {
- authorization: {
- captured: false,
- charge_id: 'ch_mock',
- amount: 1000,
- currency: 'usd',
- created: moment.utc().format(),
- order_id: 123,
- risk_level: 1,
- customer_country: 'US',
- customer_email: 'test@example.com',
- customer_name: 'Test Customer',
- payment_intent_id: 'pi_mock',
- },
- isLoading: false,
- isRequesting: false,
- doCaptureAuthorization: jest.fn(),
- doCancelAuthorization: jest.fn(),
- } );
- const charge = getBaseCharge();
- charge.captured = false;
-
- const container = renderCharge( charge );
-
- expect(
- screen.getByRole( 'button', { name: /Capture/i } )
- ).toBeInTheDocument();
-
- expect( container ).toMatchSnapshot();
- } );
-
- test( 'renders the fraud outcome buttons', () => {
- mockUseAuthorization.mockReturnValueOnce( {
- authorization: {
- captured: false,
- charge_id: 'ch_mock',
- amount: 1000,
- currency: 'usd',
- created: new Date( Date.now() ).toISOString(),
- order_id: 123,
- risk_level: 1,
- customer_country: 'US',
- customer_email: 'test@example.com',
- customer_name: 'Test Customer',
- payment_intent_id: 'pi_mock',
- },
- isLoading: false,
- isRequesting: false,
- doCaptureAuthorization: jest.fn(),
- doCancelAuthorization: jest.fn(),
- } );
- const charge = getBaseCharge();
- charge.captured = false;
-
- const container = renderCharge( charge, {}, false, {
- paymentIntent: paymentIntentMock,
- } );
-
- expect(
- screen.getByRole( 'button', { name: /Approve Transaction/i } )
- ).toBeInTheDocument();
-
- expect(
- screen.getByRole( 'button', { name: /Block Transaction/i } )
- ).toBeInTheDocument();
-
- expect(
- screen.queryByRole( 'button', { name: /Capture/i } )
- ).not.toBeInTheDocument();
-
- expect(
- screen.getByText(
- /Approving this transaction will capture the charge./
- )
- ).toBeInTheDocument();
-
- expect( container ).toMatchSnapshot();
- } );
- } );
-} );
diff --git a/client/payment-details/test/__snapshots__/index.test.tsx.snap b/client/payment-details/test/__snapshots__/index.test.tsx.snap
index 05dff02074d..b2ed253f99d 100644
--- a/client/payment-details/test/__snapshots__/index.test.tsx.snap
+++ b/client/payment-details/test/__snapshots__/index.test.tsx.snap
@@ -483,7 +483,7 @@ exports[`Payment details page should match the snapshot - Payment Intent query p
>
- Fee:
+ Fees:
-$74.00
diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js
index 3682c378e51..91fb76fb187 100644
--- a/client/payment-details/timeline/map-events.js
+++ b/client/payment-details/timeline/map-events.js
@@ -782,12 +782,6 @@ const mapEventToTimelineItems = ( event ) => {
);
}
- const disputeUrl = getAdminUrl( {
- page: 'wc-admin',
- path: '/payments/disputes/details',
- id: event.dispute_id,
- } );
-
let depositTimelineItem;
if ( event.amount === null ) {
depositTimelineItem = {
@@ -845,16 +839,7 @@ const mapEventToTimelineItems = ( event ) => {
getMainTimelineItem(
event,
reasonHeadline,
-
,
- [
- // eslint-disable-next-line react/jsx-key
-
- { __( 'View dispute', 'woocommerce-payments' ) }
- ,
- ]
+
),
];
case 'dispute_in_review':
diff --git a/client/payment-details/timeline/test/__snapshots__/index.js.snap b/client/payment-details/timeline/test/__snapshots__/index.js.snap
index 435c3835742..df78052123c 100644
--- a/client/payment-details/timeline/test/__snapshots__/index.js.snap
+++ b/client/payment-details/timeline/test/__snapshots__/index.js.snap
@@ -721,17 +721,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon
+ />
,
},
Object {
- "body": Array [
-
- View dispute
- ,
- ],
+ "body": Array [],
"date": 2020-04-02T02:06:14.000Z,
"headline": "Payment disputed as Transaction unauthorized.",
"icon": ,
},
Object {
- "body": Array [
-
- View dispute
- ,
- ],
+ "body": Array [],
"date": 2020-04-02T02:06:14.000Z,
"headline": "Payment disputed as Transaction unauthorized.",
"icon": ,
},
Object {
- "body": Array [
-
- View dispute
- ,
- ],
+ "body": Array [],
"date": 2020-04-02T02:06:14.000Z,
"headline": "Payment disputed as Transaction unauthorized.",
"icon": (
props
@@ -238,14 +239,26 @@ const PaymentMethodInformationObject: Record<
),
icon: iconComponent( JCBIcon, 'JCB' ),
currencies: [ 'JPY' ],
- stripe_key: 'card_payments',
+ stripe_key: 'jcb_payments',
allows_manual_capture: false,
allows_pay_later: false,
- setup_required: true,
- setup_tooltip: __(
- 'JCB is coming soon to your country.',
+ },
+ klarna: {
+ id: 'klarna',
+ label: __( 'Klarna', 'woocommerce-payments' ),
+ brandTitles: {
+ affirm: __( 'Klarna', 'woocommerce-payments' ),
+ },
+ description: __(
+ // translators: %s is the store currency.
+ 'Allow customers to pay over time with Klarna. Available to all customers paying in %s.',
'woocommerce-payments'
),
+ icon: iconComponent( KlarnaIcon, 'Klarna' ),
+ currencies: [ 'EUR', 'GBP', 'USD' ],
+ stripe_key: 'klarna_payments',
+ allows_manual_capture: false,
+ allows_pay_later: true,
},
};
diff --git a/client/payment-methods/capability-request/capability-request-dismiss-modal.tsx b/client/payment-methods/capability-request/capability-request-dismiss-modal.tsx
new file mode 100644
index 00000000000..842e7534455
--- /dev/null
+++ b/client/payment-methods/capability-request/capability-request-dismiss-modal.tsx
@@ -0,0 +1,54 @@
+/** @format */
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { __, sprintf } from '@wordpress/i18n';
+import { Button } from '@wordpress/components';
+import { DismissConfirmationModalProps } from './types';
+
+/**
+ * Internal dependencies
+ */
+import ConfirmationModal from 'wcpay/components/confirmation-modal';
+
+const DismissConfirmationModal: React.FC< DismissConfirmationModalProps > = ( {
+ onClose,
+ onSubmit,
+ label,
+} ): JSX.Element => {
+ const buttonContent = (
+ <>
+
+ { __( 'Cancel', 'woocommerce-payments' ) }
+
+
+ { __( 'Yes, continue', 'woocommerce-payments' ) }
+
+ >
+ );
+
+ return (
+
+
+ { sprintf(
+ /** translators: %s is the capability label. */
+ __(
+ 'Choosing to continue will remove the option to accept %s cards from your customers. ' +
+ 'The option to enable %s will not appear again.',
+ 'woocommerce-payments'
+ ),
+ label,
+ label
+ ) }
+
+
+ );
+};
+export default DismissConfirmationModal;
diff --git a/client/payment-methods/capability-request/capability-request-map.ts b/client/payment-methods/capability-request/capability-request-map.ts
new file mode 100644
index 00000000000..3c2cceebce6
--- /dev/null
+++ b/client/payment-methods/capability-request/capability-request-map.ts
@@ -0,0 +1,58 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { CapabilityRequestMap } from './types';
+
+const CapabilityRequestList: Array< CapabilityRequestMap > = [
+ {
+ id: 'jcb',
+ label: __( 'JCB', 'woocommerce-payments' ),
+ country: 'JP',
+ states: {
+ unrequested: {
+ status: 'info',
+ content: __(
+ 'Enable JCB for your customers, the only international payment brand based in Japan.',
+ 'woocommerce-payments'
+ ),
+ actions: 'request',
+ actionsLabel: __( 'Enable JCB', 'woocommerce-payments' ),
+ },
+ pending_verification: {
+ status: 'warning',
+ content: __(
+ 'To enable JCB for your customers, you need to provide more information.',
+ 'woocommerce-payments'
+ ),
+ actions: 'link',
+ actionUrl:
+ 'https://woocommerce.com/document/woopayments/payment-methods/#jcb',
+ actionsLabel: __( 'Finish setup', 'woocommerce-payments' ),
+ },
+ pending: {
+ status: 'info',
+ content: __(
+ 'Your information has been submitted and your JCB account is pending approval.',
+ 'woocommerce-payments'
+ ),
+ },
+ inactive: {
+ status: 'info',
+ content: __(
+ 'Your JCB account was rejected based on the information provided.',
+ 'woocommerce-payments'
+ ),
+ },
+ active: {
+ status: 'info',
+ content: __(
+ 'JCB is now enabled on your store.',
+ 'woocommerce-payments'
+ ),
+ },
+ },
+ },
+];
+
+export default CapabilityRequestList;
diff --git a/client/payment-methods/capability-request/capability-request-notice.tsx b/client/payment-methods/capability-request/capability-request-notice.tsx
new file mode 100644
index 00000000000..f040825c138
--- /dev/null
+++ b/client/payment-methods/capability-request/capability-request-notice.tsx
@@ -0,0 +1,182 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { __ } from '@wordpress/i18n';
+
+import { useGetPaymentMethodStatuses } from 'wcpay/data';
+import { useState } from '@wordpress/element';
+import { upeCapabilityStatuses } from 'wcpay/additional-methods-setup/constants';
+import methodsConfiguration from '../../payment-methods-map';
+import InlineNotice from 'components/inline-notice';
+import { select, useDispatch } from '@wordpress/data';
+import { NAMESPACE, STORE_NAME } from 'wcpay/data/constants';
+import apiFetch from '@wordpress/api-fetch';
+import DismissConfirmationModal from './capability-request-dismiss-modal';
+import { CapabilityNoticeProps } from './types';
+import { Action } from 'wcpay/types/notices';
+
+const CapabilityNotice = ( {
+ id,
+ label,
+ country,
+ states,
+}: CapabilityNoticeProps ): JSX.Element | null => {
+ const { updateOptions } = useDispatch( 'wc/admin/options' );
+ const { createNotice } = useDispatch( 'core/notices' );
+ const { capabilityRequestNotices } = wcpaySettings;
+ const [ isDismissed, setIsDismissed ] = useState(
+ capabilityRequestNotices[ id ] ?? false
+ );
+ const [ isDismissModalOpen, setDismissModalOpen ] = useState( false );
+ const [ isLoading, setIsLoading ] = useState( false );
+
+ const paymentMethodStatuses = useGetPaymentMethodStatuses() as Record<
+ string,
+ Record< string, string >
+ >;
+
+ const validStatuses: Array< string > = Object.entries(
+ upeCapabilityStatuses
+ ).map( ( [ value ] ) => {
+ return value;
+ } );
+
+ const settings = select( STORE_NAME ).getSettings() as Record<
+ string,
+ any
+ >;
+
+ // Retrieve the capability status
+ const stripeKey = methodsConfiguration[ id ].stripe_key ?? null;
+ const stripeStatusContainer = paymentMethodStatuses[ stripeKey ] ?? [];
+ const status = ! stripeStatusContainer
+ ? upeCapabilityStatuses.UNREQUESTED
+ : stripeStatusContainer.status;
+
+ // Display the notice if the capability has status.
+ if ( validStatuses.includes( status ) ) {
+ return null;
+ }
+
+ // Skip the notice if the country doesn't match.
+ if (
+ typeof country !== 'undefined' &&
+ settings.account_country !== country
+ ) {
+ return null;
+ }
+
+ // If the status data doesnt exist, hide the notice.
+ const noticeData = states[ status ] ?? null;
+ if ( ! noticeData ) return null;
+
+ const requestCapability = async () => {
+ setIsLoading( true );
+
+ try {
+ await apiFetch< string >( {
+ path: `${ NAMESPACE }/settings/request-capability`,
+ data: {
+ id: id,
+ },
+ method: 'POST',
+ } );
+
+ setIsDismissed( true );
+
+ createNotice(
+ 'success',
+ __(
+ 'Capability requested successfully!',
+ 'woocommerce-payments'
+ )
+ );
+
+ setIsLoading( false );
+ } catch ( exception ) {
+ createNotice(
+ 'error',
+ __( 'Error requesting the capability!', 'woocommerce-payments' )
+ );
+
+ setIsLoading( false );
+ }
+ };
+
+ const moreDetails = () => {
+ if ( typeof noticeData.actionUrl !== 'undefined' ) {
+ window.location.href = noticeData.actionUrl;
+ }
+ };
+
+ const closeModal = () => {
+ setDismissModalOpen( false );
+ };
+
+ const dismissNotice = () => {
+ updateOptions( {
+ wcpay_capability_request_dismissed_notices: {
+ ...capabilityRequestNotices,
+ [ id ]: true,
+ },
+ } );
+ wcpaySettings.capabilityRequestNotices = {
+ ...capabilityRequestNotices,
+ [ id ]: true,
+ };
+
+ setIsDismissed( true );
+ };
+
+ const dismissModal = () => {
+ if ( status === 'unrequested' || status === 'pending_verification' ) {
+ setDismissModalOpen( true );
+ } else {
+ dismissNotice();
+ }
+ };
+
+ let actions;
+ if ( noticeData.actions === 'request' || noticeData.actions === 'link' ) {
+ actions = [
+ {
+ label: noticeData.actionsLabel,
+ onClick:
+ noticeData.actions === 'request'
+ ? requestCapability
+ : moreDetails,
+ isBusy: isLoading,
+ disabled: isLoading,
+ },
+ ];
+ }
+
+ if ( isDismissed ) {
+ return null;
+ }
+
+ return (
+ <>
+
+ { noticeData.content }
+
+
+ { isDismissModalOpen && (
+
+ ) }
+ >
+ );
+};
+
+export default CapabilityNotice;
diff --git a/client/payment-methods/capability-request/index.tsx b/client/payment-methods/capability-request/index.tsx
new file mode 100644
index 00000000000..541a33ca2f7
--- /dev/null
+++ b/client/payment-methods/capability-request/index.tsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { __ } from '@wordpress/i18n';
+
+import CapabilityRequestList from './capability-request-map';
+import CapabilityNotice from './capability-request-notice';
+
+const CapabilityRequestNotice = (): JSX.Element => {
+ return (
+ <>
+ { CapabilityRequestList.map( ( request ) => (
+
+ ) ) }
+ >
+ );
+};
+
+export default CapabilityRequestNotice;
diff --git a/client/payment-methods/capability-request/test/__snapshots__/index.test.js.snap b/client/payment-methods/capability-request/test/__snapshots__/index.test.js.snap
new file mode 100644
index 00000000000..341ff4bf104
--- /dev/null
+++ b/client/payment-methods/capability-request/test/__snapshots__/index.test.js.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CapabilityRequestNotice should match the snapshot - Render UNREQUESTED CapabilityNotice 1`] = `
+
+
+
+
+
+ Enable JCB for your customers, the only international payment brand based in Japan.
+
+
+ Enable JCB
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/client/payment-methods/capability-request/test/dismiss-modal.test.js b/client/payment-methods/capability-request/test/dismiss-modal.test.js
new file mode 100644
index 00000000000..280a799e82f
--- /dev/null
+++ b/client/payment-methods/capability-request/test/dismiss-modal.test.js
@@ -0,0 +1,36 @@
+/** @format **/
+
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import DismissConfirmationModal from '../capability-request-dismiss-modal';
+
+describe( 'DismissConfirmationModal', () => {
+ it( 'calls the onClose handler on cancel', async () => {
+ const handleCloseMock = jest.fn();
+ render( );
+
+ expect( handleCloseMock ).not.toHaveBeenCalled();
+
+ userEvent.click( screen.getByText( 'Cancel' ) );
+
+ expect( handleCloseMock ).toHaveBeenCalled();
+ } );
+
+ it( 'calls the onSubmit handler on cancel', async () => {
+ const handleConfirmMock = jest.fn();
+ render( );
+
+ expect( handleConfirmMock ).not.toHaveBeenCalled();
+
+ userEvent.click( screen.getByText( 'Yes, continue' ) );
+
+ expect( handleConfirmMock ).toHaveBeenCalled();
+ } );
+} );
diff --git a/client/payment-methods/capability-request/test/index.test.js b/client/payment-methods/capability-request/test/index.test.js
new file mode 100644
index 00000000000..93e27151818
--- /dev/null
+++ b/client/payment-methods/capability-request/test/index.test.js
@@ -0,0 +1,136 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { upeCapabilityStatuses } from 'wcpay/additional-methods-setup/constants';
+import { useGetPaymentMethodStatuses } from '../../../data';
+import CapabilityNotice from '../capability-request-notice';
+
+const CapabilityRequestListMock = {
+ id: 'jcb',
+ label: __( 'JCB', 'woocommerce-payments' ),
+ country: 'JP',
+ states: {
+ unrequested: {
+ status: 'info',
+ content: __(
+ 'Enable JCB for your customers, the only international payment brand based in Japan.',
+ 'woocommerce-payments'
+ ),
+ actions: 'request',
+ actionsLabel: __( 'Enable JCB', 'woocommerce-payments' ),
+ },
+ },
+};
+
+jest.mock( '../../../data', () => ( {
+ useGetPaymentMethodStatuses: jest.fn(),
+} ) );
+
+jest.mock( '@wordpress/data', () => ( {
+ createReduxStore: jest.fn(),
+ register: jest.fn(),
+ registerStore: jest.fn(),
+ combineReducers: jest.fn(),
+ useSelect: jest.fn().mockReturnValue( {} ),
+ select: jest.fn().mockReturnValue( {
+ getSettings: jest.fn().mockReturnValue( {
+ account_country: 'JP',
+ } ),
+ } ),
+ useDispatch: jest.fn( () => ( {
+ updateOptions: jest.fn(),
+ createNotice: jest.fn(),
+ } ) ),
+} ) );
+
+jest.mock( '@wordpress/api-fetch', () => jest.fn() );
+
+global.wcpaySettings = {
+ capabilityRequestNotices: [],
+};
+
+describe( 'CapabilityRequestNotice', () => {
+ beforeEach( () => {
+ useGetPaymentMethodStatuses.mockReturnValue( {
+ jcb_payments: {
+ status: upeCapabilityStatuses.UNREQUESTED,
+ requirements: [],
+ },
+ } );
+ } );
+
+ afterEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ it( 'should match the snapshot - Render UNREQUESTED CapabilityNotice', () => {
+ const { container } = render(
+
+ );
+
+ expect( container ).toMatchSnapshot();
+ } );
+
+ it( 'should render content and button - Render CapabilityNotice', () => {
+ render(
+
+ );
+
+ expect(
+ screen.queryByText( /Enable JCB for your customers/, {
+ ignore: '.a11y-speak-region',
+ } )
+ ).toBeInTheDocument();
+
+ expect(
+ screen.queryByRole( 'button', { name: 'Enable JCB' } )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should not render if country is not JP - CapabilityNotice', () => {
+ render(
+
+ );
+
+ expect(
+ screen.queryByRole( 'button', { name: 'Enable JCB' } )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should not render if state is unknown - CapabilityNotice', () => {
+ render(
+
+ );
+
+ expect(
+ screen.queryByRole( 'button', { name: 'Enable JCB' } )
+ ).not.toBeInTheDocument();
+ } );
+} );
diff --git a/client/payment-methods/capability-request/types.ts b/client/payment-methods/capability-request/types.ts
new file mode 100644
index 00000000000..bb33f849e14
--- /dev/null
+++ b/client/payment-methods/capability-request/types.ts
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+import { Status } from '@wordpress/notices';
+
+export interface CapabilityStatus {
+ status: Status;
+ content: string;
+ actions?: string;
+ actionsLabel?: string;
+ actionUrl?: string;
+}
+
+export interface CapabilityRequestMap {
+ id: string;
+ label: string;
+ country?: string;
+ states: Record< string, CapabilityStatus >;
+}
+
+export interface CapabilityNoticeProps {
+ id: string;
+ label: string;
+ country?: string;
+ states: Record< string, CapabilityStatus >;
+}
+
+export interface DismissConfirmationModalProps {
+ onClose: () => void;
+ onSubmit: () => void;
+ label: string;
+}
diff --git a/client/payment-methods/constants.ts b/client/payment-methods/constants.ts
index 2facf48e83c..1a6794aa44c 100644
--- a/client/payment-methods/constants.ts
+++ b/client/payment-methods/constants.ts
@@ -11,13 +11,13 @@ enum PAYMENT_METHOD_IDS {
CARD = 'card',
CARD_PRESENT = 'card_present',
EPS = 'eps',
+ KLARNA = 'klarna',
GIROPAY = 'giropay',
IDEAL = 'ideal',
LINK = 'link',
P24 = 'p24',
SEPA_DEBIT = 'sepa_debit',
SOFORT = 'sofort',
- JCB = 'jcb',
}
// This constant is used for rendering tooltip titles for payment methods in transaction list and details pages.
diff --git a/client/payment-methods/index.js b/client/payment-methods/index.js
index d47b2ddb9e1..cc009184163 100644
--- a/client/payment-methods/index.js
+++ b/client/payment-methods/index.js
@@ -39,7 +39,6 @@ import DisableUPEModal from '../settings/disable-upe-modal';
import PaymentMethodsList from 'components/payment-methods-list';
import PaymentMethod from 'components/payment-methods-list/payment-method';
import WCPaySettingsContext from '../settings/wcpay-settings-context';
-import Pill from '../components/pill';
import methodsConfiguration from '../payment-methods-map';
import CardBody from '../settings/card-body';
import { upeCapabilityStatuses } from 'wcpay/additional-methods-setup/constants';
@@ -47,24 +46,36 @@ import ConfirmPaymentMethodActivationModal from './activation-modal';
import ConfirmPaymentMethodDeleteModal from './delete-modal';
import { getAdminUrl } from 'wcpay/utils';
import { getPaymentMethodDescription } from 'wcpay/utils/payment-methods';
+import CapabilityRequestNotice from './capability-request';
import InlineNotice from 'wcpay/components/inline-notice';
-import interpolateComponents from '@automattic/interpolate-components';
const PaymentMethodsDropdownMenu = ( { setOpenModal } ) => {
+ const { isUpeEnabled, upeType } = useContext( WcPayUpeContext );
+ const isDisablePossible =
+ isUpeEnabled && upeType !== 'deferred_intent_upe_without_fallback';
+ const label = isDisablePossible
+ ? __( 'Add feedback or disable', 'woocommerce-payments' )
+ : __( 'Add feedback', 'woocommerce-payments' );
+
+ const buttons = [
+ {
+ title: __( 'Provide feedback', 'woocommerce-payments' ),
+ onClick: () => setOpenModal( 'survey' ),
+ },
+ ];
+
+ if ( isDisablePossible ) {
+ buttons.push( {
+ title: 'Disable',
+ onClick: () => setOpenModal( 'disable' ),
+ } );
+ }
+
return (
setOpenModal( 'survey' ),
- },
- {
- title: 'Disable',
- onClick: () => setOpenModal( 'disable' ),
- },
- ] }
+ label={ label }
+ controls={ buttons }
/>
);
};
@@ -225,6 +236,11 @@ const PaymentMethods = () => {
const { isUpeEnabled, status, upeType } = useContext( WcPayUpeContext );
const [ openModalIdentifier, setOpenModalIdentifier ] = useState( '' );
+ const rollbackNoticeForLegacyUPE = __(
+ // eslint-disable-next-line max-len
+ 'You have been switched from the new checkout to your previous checkout experience. We will keep you posted on the new checkout availability.',
+ 'woocommerce-payments'
+ );
return (
<>
@@ -260,17 +276,6 @@ const PaymentMethods = () => {
'woocommerce-payments'
) }
- { upeType !== 'split' && (
- <>
- { ' ' }
-
- { __(
- 'Early access',
- 'woocommerce-payments'
- ) }
-
- >
- ) }
{
status="warning"
isDismissible={ false }
>
- { interpolateComponents( {
- mixedString: __(
- 'The new WooPayments checkout experience will become the default on October 11, 2023.' +
- ' {{learnMoreLink}}Learn more{{/learnMoreLink}}',
- 'woocommerce-payments'
- ),
- components: {
- learnMoreLink: (
- // eslint-disable-next-line max-len
-
- ),
- },
- } ) }
+ { rollbackNoticeForLegacyUPE }
) }
+
+
{ availableMethods.map(
( {
diff --git a/client/payment-methods/test/index.js b/client/payment-methods/test/index.js
index 5d2ebf69ff9..70023318ec1 100644
--- a/client/payment-methods/test/index.js
+++ b/client/payment-methods/test/index.js
@@ -6,6 +6,7 @@
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
+import { select } from '@wordpress/data';
/**
* Internal dependencies
@@ -49,6 +50,7 @@ jest.mock( '@wordpress/data', () => ( {
useDispatch: jest
.fn()
.mockReturnValue( { updateAvailablePaymentMethodIds: jest.fn() } ),
+ select: jest.fn(),
} ) );
describe( 'PaymentMethods', () => {
@@ -81,8 +83,14 @@ describe( 'PaymentMethods', () => {
useManualCapture.mockReturnValue( [ false, jest.fn() ] );
global.wcpaySettings = {
accountEmail: 'admin@example.com',
+ capabilityRequestNotices: {},
};
useAccountDomesticCurrency.mockReturnValue( 'usd' );
+ select.mockImplementation( () => ( {
+ getSettings: jest.fn().mockReturnValue( {
+ account_country: 'US',
+ } ),
+ } ) );
} );
test( 'payment methods are rendered correctly', () => {
@@ -424,7 +432,7 @@ describe( 'PaymentMethods', () => {
expect( disableUPEButton ).toBeInTheDocument();
expect(
screen.queryByText( 'Payment methods' ).parentElement
- ).toHaveTextContent( 'Payment methods Early access' );
+ ).toHaveTextContent( 'Payment methods' );
} );
test( 'Does not render the feedback elements when UPE is disabled', () => {
@@ -484,6 +492,69 @@ describe( 'PaymentMethods', () => {
);
} );
+ it( 'should only be able to leave feedback when deferred upe after migration is enabled', () => {
+ render(
+
+
+
+ );
+ const kebabMenuWithFeedbackOnly = screen.queryByRole( 'button', {
+ name: 'Add feedback',
+ } );
+
+ const kebabMenuWithFeedbackAndDisable = screen.queryByRole( 'button', {
+ name: 'Add feedback or disable',
+ } );
+
+ expect( kebabMenuWithFeedbackOnly ).toBeInTheDocument();
+ expect( kebabMenuWithFeedbackAndDisable ).not.toBeInTheDocument();
+ } );
+
+ it( 'should only be able to leave feedback and disable when deferred upe was enabled manually for legacy card stores', () => {
+ render(
+
+
+
+ );
+ const kebabMenuWithFeedbackOnly = screen.queryByRole( 'button', {
+ name: 'Add feedback',
+ } );
+
+ const kebabMenuWithFeedbackAndDisable = screen.queryByRole( 'button', {
+ name: 'Add feedback or disable',
+ } );
+
+ expect( kebabMenuWithFeedbackAndDisable ).toBeInTheDocument();
+ expect( kebabMenuWithFeedbackOnly ).not.toBeInTheDocument();
+ } );
+
+ it( 'should be able to leave feedback and disable for non-deferred-upe', () => {
+ render(
+
+
+
+ );
+ const kebabMenuWithFeedbackOnly = screen.queryByRole( 'button', {
+ name: 'Add feedback',
+ } );
+
+ const kebabMenuWithFeedbackAndDisable = screen.queryByRole( 'button', {
+ name: 'Add feedback or disable',
+ } );
+
+ expect( kebabMenuWithFeedbackAndDisable ).toBeInTheDocument();
+ expect( kebabMenuWithFeedbackOnly ).not.toBeInTheDocument();
+ } );
+
it( 'should render the activation modal when requirements exist for the payment method', () => {
useEnabledPaymentMethodIds.mockReturnValue( [ [], jest.fn() ] );
useGetAvailablePaymentMethodIds.mockReturnValue( [ 'bancontact' ] );
diff --git a/client/payment-request/index.js b/client/payment-request/index.js
index 8596874a4eb..e9c77679c61 100644
--- a/client/payment-request/index.js
+++ b/client/payment-request/index.js
@@ -381,14 +381,37 @@ jQuery( ( $ ) => {
$.when( wcpayPaymentRequest.getSelectedProductData() )
.then( ( response ) => {
- $.when(
- paymentRequest.update( {
- total: response.total,
- displayItems: response.displayItems,
- } )
- ).then( () => {
+ // If a variation doesn't need shipping, re-init the `wcpayPaymentRequest` with response params.
+ if (
+ wcpayPaymentRequestParams.product.needs_shipping !==
+ response.needs_shipping
+ ) {
+ wcpayPaymentRequestParams.product.needs_shipping =
+ response.needs_shipping;
+ wcpayPaymentRequestParams.product.total =
+ response.total;
+ wcpayPaymentRequestParams.product.displayItems =
+ response.displayItems;
+ wcpayPaymentRequest.init();
wcpayPaymentRequest.unblockPaymentRequestButton();
- } );
+ } else {
+ const responseTotal = response.total;
+
+ // If a variation `needs_shipping` is `false`, the `pending` param needs to be set to `false`.
+ // Because the additional shipping address call is not executed to set the pending to `false`.
+ if ( response.needs_shipping === false ) {
+ responseTotal.pending = false;
+ }
+
+ $.when(
+ paymentRequest.update( {
+ total: responseTotal,
+ displayItems: response.displayItems,
+ } )
+ ).then( () => {
+ wcpayPaymentRequest.unblockPaymentRequestButton();
+ } );
+ }
} )
.catch( () => {
wcpayPaymentRequest.hide();
diff --git a/client/product-details/index.js b/client/product-details/index.js
index d26397f6b5e..26ff916efa8 100644
--- a/client/product-details/index.js
+++ b/client/product-details/index.js
@@ -7,51 +7,124 @@
import { initializeBnplSiteMessaging } from './bnpl-site-messaging';
jQuery( function ( $ ) {
+ /**
+ * Check for the existence of the `wcpayStripeSiteMessaging` variable on the window object.
+ * This variable holds the configuration for Stripe site messaging and contains the following keys:
+ * - productId: The ID of the product.
+ * - productVariations: Variations of the product.
+ * - country: The country of the customer. Defaults to the store's country.
+ * - publishableKey: The key used for Stripe's API calls.
+ * - paymentMethods: Enabled BNPL payment methods.
+ *
+ * If this variable is not set, the script will exit early to prevent further execution.
+ */
+ if ( ! window.wcpayStripeSiteMessaging ) {
+ return;
+ }
+
+ const { productVariations, productId } = window.wcpayStripeSiteMessaging;
+ const {
+ amount: baseProductAmount = 0,
+ currency: productCurrency,
+ } = productVariations[ productId ];
+ const QUANTITY_INPUT_SELECTOR = '.quantity input[type=number]';
+ const SINGLE_VARIATION_SELECTOR = '.single_variation_wrap';
+ const VARIATIONS_SELECTOR = '.variations';
+ const RESET_VARIATIONS_SELECTOR = '.reset_variations';
+ const VARIATION_ID_SELECTOR = 'input[name="variation_id"]';
+
+ const quantityInput = $( QUANTITY_INPUT_SELECTOR );
const bnplPaymentMessageElement = initializeBnplSiteMessaging();
- const { productVariations } = window.wcpayStripeSiteMessaging;
- let { productId } = window.wcpayStripeSiteMessaging;
+ const hasVariations = Object.keys( productVariations ).length > 1;
+
+ /**
+ * Safely parses a given value to an integer number.
+ * If the parsed value is NaN, the function returns 0.
+ *
+ * @param {string|number} value - The value to be parsed to integer number.
+ * @return {number} The parsed number, or 0 if the parsed value is NaN.
+ */
+ const parseIntOrReturnZero = ( value ) => {
+ const result = parseInt( value, 10 );
+ return isNaN( result ) ? 0 : result;
+ };
+
+ /**
+ * Updates the BNPL payment message displayed on the page.
+ * The function takes an amount, a currency, and an optional quantity.
+ * If the amount is less than or equal to zero, or if the currency is not provided,
+ * the function will exit early without making updates.
+ *
+ * @param {number} amount - The total amount for the BNPL message.
+ * @param {string} currency - The currency code (e.g., 'USD', 'EUR') for the BNPL message.
+ * @param {number} [quantity=1] - The quantity of the product being purchased. Defaults to 1.
+ */
+ const updateBnplPaymentMessage = ( amount, currency, quantity = 1 ) => {
+ const totalAmount =
+ parseIntOrReturnZero( amount ) * parseIntOrReturnZero( quantity );
+
+ if ( totalAmount <= 0 || ! currency ) {
+ return;
+ }
+
+ bnplPaymentMessageElement.update( { amount: totalAmount, currency } );
+ };
+ /**
+ * Resets the BNPL payment message displayed on the page.
+ * The function updates the BNPL message using the global `baseProductAmount` and the current value
+ * from `quantityInput` by calling `updateBnplPaymentMessage`.
+ */
const resetBnplPaymentMessage = () => {
- const quantity = $( '.quantity input[type=number]' ).val();
- productId = 'base_product';
- bnplPaymentMessageElement.update( {
- amount:
- parseInt( productVariations.base_product.amount, 10 ) *
- quantity,
- currency: productVariations.base_product.currency,
- } );
+ updateBnplPaymentMessage(
+ baseProductAmount,
+ productCurrency,
+ quantityInput.val()
+ );
};
- $( '.quantity input[type=number]' ).on( 'change', function ( event ) {
- const newQuantity = event.target.value;
- const price = productVariations[ productId ].amount;
- bnplPaymentMessageElement.update( {
- amount: parseInt( price, 10 ) * newQuantity,
- currency: productVariations[ productId ].currency,
- } );
+ // Update BNPL message based on the quantity change
+ quantityInput.on( 'change', ( event ) => {
+ let amount = baseProductAmount;
+ const variationId = $( VARIATION_ID_SELECTOR ).val();
+
+ // If the product has variations, get the amount from the selected variation.
+ if (
+ hasVariations &&
+ productVariations.hasOwnProperty( variationId )
+ ) {
+ amount = productVariations[ variationId ]?.amount;
+ }
+
+ updateBnplPaymentMessage( amount, productCurrency, event.target.value );
} );
// Handle BNPL messaging for variable products.
- if ( Object.keys( productVariations ).length > 1 ) {
- $( '.single_variation_wrap' ).on( 'show_variation', function (
- event,
- variation
- ) {
- const quantity = $( '.quantity input[type=number]' ).val();
- const variationPrice =
- productVariations[ variation.variation_id ].amount;
- productId = variation.variation_id;
- bnplPaymentMessageElement.update( {
- amount: parseInt( variationPrice, 10 ) * quantity,
- currency: productVariations[ variation.variation_id ].currency,
- } );
- } );
+ if ( hasVariations ) {
+ // Update BNPL message based on product variation
+ $( SINGLE_VARIATION_SELECTOR ).on(
+ 'show_variation',
+ ( event, variation ) => {
+ if ( ! productVariations[ variation.variation_id ] ) {
+ return;
+ }
+
+ updateBnplPaymentMessage(
+ productVariations[ variation.variation_id ].amount,
+ productCurrency,
+ quantityInput.val()
+ );
+ }
+ );
- // If variation is changed back to default, reset BNPL messaging.
- $( '.variations' ).on( 'change', function ( event ) {
- if ( event.target.value === '' ) resetBnplPaymentMessage();
+ // Reset BNPL message if variation is changed back to default
+ $( VARIATIONS_SELECTOR ).on( 'change', ( event ) => {
+ if ( event.target.value === '' ) {
+ resetBnplPaymentMessage();
+ }
} );
- $( '.reset_variations' ).on( 'click', resetBnplPaymentMessage );
+ // Reset BNPL message on variations reset
+ $( RESET_VARIATIONS_SELECTOR ).on( 'click', resetBnplPaymentMessage );
}
} );
diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js
index 0f1f3f2937b..687ffa723b9 100644
--- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js
+++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js
@@ -35,7 +35,7 @@ const WCPaySubscriptionsToggle = () => {
* for wcpay subscriptions or if wcpay subscriptions are already enabled.
*/
return ! wcpaySettings.isSubscriptionsActive &&
- ( isWCPaySubscriptionsEligible || isWCPaySubscriptionsEnabled ) ? (
+ isWCPaySubscriptionsEligible ? (
{
const { saveSettings, isSaving, isLoading, settings } = useSettings();
@@ -23,6 +24,15 @@ const SaveSettingsSection = ( { disabled = false } ) => {
initialIsPaymentRequestEnabled,
setInitialIsPaymentRequestEnabled,
] = useState( null );
+ // Keep the inital value of is_woopay_enabled
+ // in state for showing the feedback modal on change.
+ const [ initialIsWooPayEnabled, setInitialIsWooPayEnabled ] = useState(
+ null
+ );
+ const [
+ isWooPayDisableFeedbackOpen,
+ setIsWooPayDisableFeedbackOpen,
+ ] = useState( false );
if (
initialIsPaymentRequestEnabled === null &&
@@ -34,15 +44,26 @@ const SaveSettingsSection = ( { disabled = false } ) => {
);
}
+ if (
+ initialIsWooPayEnabled === null &&
+ settings &&
+ typeof settings.is_woopay_enabled !== 'undefined'
+ ) {
+ setInitialIsWooPayEnabled( settings.is_woopay_enabled );
+ }
+
const saveOnClick = async () => {
const isSuccess = await saveSettings();
+ if ( ! isSuccess ) {
+ return;
+ }
+
// Track the event when the value changed and the
// settings were successfully saved.
if (
- isSuccess &&
initialIsPaymentRequestEnabled !==
- settings.is_payment_request_enabled
+ settings.is_payment_request_enabled
) {
wcpayTracks.recordEvent(
wcpayTracks.events.PAYMENT_REQUEST_SETTINGS_CHANGE,
@@ -56,6 +77,35 @@ const SaveSettingsSection = ( { disabled = false } ) => {
settings.is_payment_request_enabled
);
}
+
+ // Show the feedback modal when WooPay is disabled.
+ if ( initialIsWooPayEnabled && ! settings.is_woopay_enabled ) {
+ const { woopayLastDisableDate } = wcpaySettings;
+
+ // Do not show feedback modal if WooPay
+ // was disabled in the last 7 days.
+ if ( woopayLastDisableDate ) {
+ const date1 = new Date( woopayLastDisableDate );
+ const date2 = new Date();
+ const diffTime = Math.abs( date2 - date1 );
+ const diffDays = Math.ceil(
+ diffTime / ( 1000 * 60 * 60 * 24 )
+ );
+
+ if ( diffDays < 7 ) {
+ return;
+ }
+ }
+
+ setIsWooPayDisableFeedbackOpen( true );
+
+ // Prevent show modal again.
+ setInitialIsPaymentRequestEnabled( true );
+ // Set last disable date to prevent feedback window opening up
+ // on successive "Save button" clicks. This value is overwritten
+ // on page refresh.
+ wcpaySettings.woopayLastDisableDate = new Date();
+ }
};
return (
@@ -68,6 +118,13 @@ const SaveSettingsSection = ( { disabled = false } ) => {
>
{ __( 'Save changes', 'woocommerce-payments' ) }
+ { isWooPayDisableFeedbackOpen ? (
+
+ setIsWooPayDisableFeedbackOpen( false )
+ }
+ />
+ ) : null }
);
};
diff --git a/client/settings/wcpay-settings-context.js b/client/settings/wcpay-settings-context.js
index 0966d6e9c47..531e806ff27 100644
--- a/client/settings/wcpay-settings-context.js
+++ b/client/settings/wcpay-settings-context.js
@@ -9,7 +9,7 @@ const WCPaySettingsContext = createContext( {
accountStatus: {},
featureFlags: {
isAuthAndCaptureEnabled: false,
- isDisputeOnTransactionPageEnabled: false,
+ isDisputeIssuerEvidenceEnabled: false,
woopay: false,
},
} );
diff --git a/client/settings/woopay-disable-feedback/index.js b/client/settings/woopay-disable-feedback/index.js
new file mode 100644
index 00000000000..a46f093e888
--- /dev/null
+++ b/client/settings/woopay-disable-feedback/index.js
@@ -0,0 +1,50 @@
+/**
+ * External dependencies
+ */
+import React, { useState } from 'react';
+import { __ } from '@wordpress/i18n';
+import { Modal } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import Loadable from 'wcpay/components/loadable';
+import WooPayIcon from 'assets/images/woopay.svg?asset';
+
+const WooPayDisableFeedback = ( { onRequestClose } ) => {
+ const [ isLoading, setIsLoading ] = useState( true );
+
+ return (
+
+ }
+ isDismissible={ true }
+ shouldCloseOnClickOutside={ false } // Should be false because of the iframe.
+ shouldCloseOnEsc={ true }
+ onRequestClose={ onRequestClose }
+ className="woopay-disable-feedback"
+ >
+
+
+
+ );
+};
+
+export default WooPayDisableFeedback;
diff --git a/client/settings/woopay-disable-feedback/style.scss b/client/settings/woopay-disable-feedback/style.scss
new file mode 100644
index 00000000000..08580261c61
--- /dev/null
+++ b/client/settings/woopay-disable-feedback/style.scss
@@ -0,0 +1,23 @@
+.woopay-disable-feedback {
+ @media ( min-width: 960px ) {
+ max-height: calc( 100% - 120px );
+ }
+
+ .components-modal__content {
+ padding: 0;
+ }
+
+ &-iframe {
+ width: 100%;
+ height: 100%;
+
+ @media ( min-width: 600px ) {
+ width: 600px;
+ height: 650px;
+ }
+ }
+
+ &-logo {
+ height: 40px;
+ }
+}
diff --git a/client/tracks/index.js b/client/tracks/index.js
index 3c750ee2784..4dc253734ec 100644
--- a/client/tracks/index.js
+++ b/client/tracks/index.js
@@ -68,6 +68,12 @@ const events = {
DEPOSITS_ROW_CLICK: 'wcpay_deposits_row_click',
DEPOSITS_DOWNLOAD_CSV_CLICK: 'wcpay_deposits_download',
DISPUTES_ROW_ACTION_CLICK: 'wcpay_disputes_row_action_click',
+ DISPUTE_CHALLENGE_CLICKED: 'wcpay_dispute_challenge_clicked',
+ DISPUTE_ACCEPT_CLICK: 'wcpay_dispute_accept_click',
+ DISPUTE_ACCEPT_MODAL_VIEW: 'wcpay_dispute_accept_modal_view',
+ DISPUTE_INQUIRY_REFUND_CLICK: 'wcpay_dispute_inquiry_refund_click',
+ DISPUTE_INQUIRY_REFUND_MODAL_VIEW:
+ 'wcpay_dispute_inquiry_refund_modal_view',
ORDER_DISPUTE_NOTICE_BUTTON_CLICK:
'wcpay_order_dispute_notice_action_click',
OVERVIEW_BALANCES_CURRENCY_CLICK:
@@ -77,6 +83,8 @@ const events = {
OVERVIEW_DEPOSITS_CHANGE_SCHEDULE_CLICK:
'wcpay_overview_deposits_change_schedule_click',
OVERVIEW_TASK_CLICK: 'wcpay_overview_task_click',
+ PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED:
+ 'wcpay_view_submitted_evidence_clicked',
SETTINGS_DEPOSITS_MANAGE_IN_STRIPE_CLICK:
'wcpay_settings_deposits_manage_in_stripe_click',
MULTI_CURRENCY_ENABLED_CURRENCIES_UPDATED:
diff --git a/client/types/balance-transactions.d.ts b/client/types/balance-transactions.d.ts
index e14e65cd2e5..dccb7535902 100644
--- a/client/types/balance-transactions.d.ts
+++ b/client/types/balance-transactions.d.ts
@@ -2,4 +2,5 @@ export interface BalanceTransaction {
currency: string;
amount: number;
fee: number;
+ reporting_category?: 'dispute' | 'dispute_reversal' | string;
}
diff --git a/client/types/charges.d.ts b/client/types/charges.d.ts
index 99dd079795f..df26095d92c 100644
--- a/client/types/charges.d.ts
+++ b/client/types/charges.d.ts
@@ -39,6 +39,7 @@ export interface PaymentMethodDetails {
| 'eps'
| 'giropay'
| 'ideal'
+ | 'klarna'
| 'p24'
| 'sepa_debit'
| 'sofort';
diff --git a/client/types/disputes.d.ts b/client/types/disputes.d.ts
index cea3ea886c9..0db395b9cc8 100644
--- a/client/types/disputes.d.ts
+++ b/client/types/disputes.d.ts
@@ -76,18 +76,39 @@ export type DisputeStatus =
export interface Dispute {
status: DisputeStatus;
id: string;
- evidence_details?: EvidenceDetails;
- metadata: Record< string, any >;
+ evidence_details: EvidenceDetails;
+ metadata: {
+ /* eslint-disable @typescript-eslint/naming-convention -- required to allow underscores in keys */
+ /**
+ * '1' if the dispute was closed/accepted by the merchant, '0' if the dispute was closed by Stripe.
+ */
+ __closed_by_merchant?: '1' | '0';
+ /**
+ * Unix timestamp of when the dispute was closed.
+ */
+ __dispute_closed_at?: string;
+ /**
+ * Unix timestamp of when dispute evidence was submitted.
+ */
+ __evidence_submitted_at?: string;
+ /* eslint-enable @typescript-eslint/naming-convention */
+ };
order: null | OrderDetails;
evidence: Evidence;
- issuer_evidence: IssuerEvidence | null;
+ issuer_evidence: IssuerEvidence[] | null;
fileSize?: Record< string, number >;
reason: DisputeReason;
charge: Charge | string;
amount: number;
currency: string;
created: number;
+ /**
+ * List of zero, one, or two balance transactions that show funds withdrawn and reinstated to the Stripe account as a result of this dispute.
+ * One balance transaction with `reporting_category: 'dispute'` will be present if funds have been withdrawn from the account.
+ * A second balance transaction with the `reporting_category: 'dispute_reversal'` will be present if funds have been reinstated to the account.
+ */
balance_transactions: BalanceTransaction[];
+ payment_intent: string;
}
export interface CachedDispute {
diff --git a/client/types/notices.d.ts b/client/types/notices.d.ts
new file mode 100644
index 00000000000..b74af1f7b6e
--- /dev/null
+++ b/client/types/notices.d.ts
@@ -0,0 +1,14 @@
+/**
+ * External dependencies
+ */
+import { Notice } from '@wordpress/components';
+
+interface ActionButton extends Notice.ButtonAction {
+ isBusy?: boolean;
+ disabled?: boolean;
+}
+interface URLAction extends Notice.BaseAction {
+ url: string;
+}
+
+export type Action = ActionButton | URLAction;
diff --git a/client/types/payment-methods.d.ts b/client/types/payment-methods.d.ts
index 2681e212745..504c2927270 100644
--- a/client/types/payment-methods.d.ts
+++ b/client/types/payment-methods.d.ts
@@ -14,11 +14,11 @@ export type PaymentMethod =
| 'card'
| 'card_present'
| 'eps'
+ | 'klarna'
| 'giropay'
| 'ideal'
| 'p24'
| 'sepa_debit'
| 'sofort'
- | 'jcb'
| 'affirm'
| 'afterpay_clearpay';
diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx
index f7cdd94a149..e2018013c3a 100644
--- a/client/utils/account-fees.tsx
+++ b/client/utils/account-fees.tsx
@@ -395,6 +395,8 @@ export const getTransactionsPaymentMethodName = (
return __( 'Affirm transactions', 'woocommerce-payments' );
case 'afterpay_clearpay':
return __( 'Afterpay transactions', 'woocommerce-payments' );
+ case 'klarna':
+ return __( 'Klarna transactions', 'woocommerce-payments' );
default:
return __( 'Unknown transactions', 'woocommerce-payments' );
}
diff --git a/composer.json b/composer.json
index 668bf0f26a7..999b2cab67d 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,7 @@
"automattic/jetpack-autoloader": "2.11.18",
"automattic/jetpack-identity-crisis": "0.8.43",
"automattic/jetpack-sync": "1.47.7",
- "woocommerce/subscriptions-core": "6.2.0",
+ "woocommerce/subscriptions-core": "6.3.0",
"psr/container": "^1.1"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index a5963e45fdb..67b78b093c5 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "86b5f217949dc6931b79653be4a6dca8",
+ "content-hash": "f45536a544c784b5c85d2e507cb43d42",
"packages": [
{
"name": "automattic/jetpack-a8c-mc-stats",
@@ -988,16 +988,16 @@
},
{
"name": "woocommerce/subscriptions-core",
- "version": "6.2.0",
+ "version": "6.3.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/woocommerce-subscriptions-core.git",
- "reference": "47cfe92d60239d1b8b12a5f640a3772b0e4e1272"
+ "reference": "8ba0249f97df46caafd625950853f8f5e5a29984"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/woocommerce-subscriptions-core/zipball/47cfe92d60239d1b8b12a5f640a3772b0e4e1272",
- "reference": "47cfe92d60239d1b8b12a5f640a3772b0e4e1272",
+ "url": "https://api.github.com/repos/Automattic/woocommerce-subscriptions-core/zipball/8ba0249f97df46caafd625950853f8f5e5a29984",
+ "reference": "8ba0249f97df46caafd625950853f8f5e5a29984",
"shasum": ""
},
"require": {
@@ -1038,10 +1038,10 @@
"description": "Sell products and services with recurring payments in your WooCommerce Store.",
"homepage": "https://github.com/Automattic/woocommerce-subscriptions-core",
"support": {
- "source": "https://github.com/Automattic/woocommerce-subscriptions-core/tree/6.2.0",
+ "source": "https://github.com/Automattic/woocommerce-subscriptions-core/tree/6.3.0",
"issues": "https://github.com/Automattic/woocommerce-subscriptions-core/issues"
},
- "time": "2023-08-10T23:43:48+00:00"
+ "time": "2023-10-06T04:31:08+00:00"
}
],
"packages-dev": [
diff --git a/dev/phpcs/Sniffs/DisallowHooksInConstructorSniff.php b/dev/phpcs/Sniffs/DisallowHooksInConstructorSniff.php
new file mode 100644
index 00000000000..519082159a7
--- /dev/null
+++ b/dev/phpcs/Sniffs/DisallowHooksInConstructorSniff.php
@@ -0,0 +1,69 @@
+
+ */
+ public function register() {
+ return [ \T_FUNCTION ];
+ }
+
+ /**
+ * Processes the sniff if one of its tokens is encountered.
+ *
+ * @param File $phpcsFile The current file being checked.
+ * @param int $stackPtr The position of the current token in the stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process( File $phpcsFile, $stackPtr ) {
+ $tokens = $phpcsFile->getTokens();
+
+ // Check that we're looking at the __construct function.
+ $namePtr = $phpcsFile->findNext( T_STRING, $stackPtr + 1 );
+ $nameToken = $tokens[ $namePtr ];
+ if ( $nameToken['content'] !== '__construct' ) {
+ return;
+ }
+
+ // Retrieve the current token.
+ $token = $tokens[ $stackPtr ];
+
+ // Check the current token's scope.
+ $scopeOpener = $token['scope_opener'];
+ $scopeCloser = $token['scope_closer'];
+
+ // For every string token in the scope...
+ for ( $i = $scopeOpener; $i < $scopeCloser; $i++ ) {
+ if ( $tokens[ $i ]['type'] !== 'T_STRING' ) {
+ continue;
+ }
+ // ...check if it's one of the forbidden functions.
+ $currentToken = $tokens[ $i ];
+ $currentTokenContent = $currentToken['content'];
+ if ( in_array( $currentTokenContent, $this->forbiddenFunctions ) ) {
+ $phpcsFile->addError(
+ "Usage of $currentTokenContent in __construct() is not allowed",
+ $i,
+ 'WCPay.CodingStandards.DisallowHooksInConstructor',
+ [ $token['content'] ]
+ );
+ }
+ }
+ }
+}
diff --git a/dev/phpcs/ruleset.xml b/dev/phpcs/ruleset.xml
new file mode 100644
index 00000000000..21b9b1fa970
--- /dev/null
+++ b/dev/phpcs/ruleset.xml
@@ -0,0 +1,28 @@
+
+
+ WCPay custom coding standards.
+
+
+ */includes/subscriptions/*
+ */includes/compat/subscriptions/*
+
+
+ */includes/emails/*
+
+
+ */includes/class-woopay-tracker.php
+ */includes/woopay/*
+ */includes/woopay-user/*
+ */includes/class-wc-payments-order-success-page.php
+
+
+ */includes/class-wc-payments-apple-pay-registration.php
+ */includes/class-wc-payments-express-checkout-button-display-handler.php
+
+
+ */includes/class-wc-payments-customer-service.php
+ */includes/class-wc-payments-token-service.php
+
+
+ */includes/class-wc-payments-webhook-reliability-service.php
+
diff --git a/docs/rest-api/source/includes/wp-api-v3/customer.md b/docs/rest-api/source/includes/wp-api-v3/customer.md
new file mode 100644
index 00000000000..692224f8b87
--- /dev/null
+++ b/docs/rest-api/source/includes/wp-api-v3/customer.md
@@ -0,0 +1,93 @@
+# Customer
+
+The Customers API endpoints provide access to customer data. This includes payment methods and other key information useful for your application.
+
+
+## Get customer's payment methods
+
+_@since v6.6.0_
+
+Return all customer's payment methods.
+
+### Error codes
+
+- `rest_forbidden` - indicates that the user or application does not have the necessary permissions to perform the requested action.
+
+### HTTP request
+
+
+
+ GET
+
/wp-json/wc/v3/payments/customers/<customer_id>/payment_methods
+
+
+
+```shell
+curl -X GET https://example.com/wp-json/wc/v3/payments/customers/cus_123456/payment_methods \
+ -u consumer_key:consumer_secret \
+ -H "Content-Type: application/json"
+```
+
+> JSON response example:
+
+```json
+[
+ {
+ "id": "pm_1AxXc2a5dGhIZQYPlaLbKj1Z",
+ "type": "card",
+ "billing_details": {
+ "address": {
+ "city": "Los Angeles",
+ "country": "US",
+ "line1": "123 Anywhere St",
+ "line2": null,
+ "postal_code": "90002",
+ "state": "CA"
+ },
+ "email": "john.doe@example.com",
+ "name": "John Doe",
+ "phone": null
+ },
+ "card": {
+ "brand": "visa",
+ "last4": "1122",
+ "exp_month": 10,
+ "exp_year": 2028
+ }
+ },
+ {
+ "id": "pm_2BcYd3e6hKjZLQWQmMnOjK45",
+ "type": "card",
+ "billing_details": {
+ "address": {
+ "city": "New York",
+ "country": "US",
+ "line1": "456 Broadway Ave",
+ "line2": null,
+ "postal_code": "10012",
+ "state": "NY"
+ },
+ "email": "jane.smith@example.com",
+ "name": "Jane Smith",
+ "phone": null
+ },
+ "card": {
+ "brand": "mastercard",
+ "last4": "3344",
+ "exp_month": 12,
+ "exp_year": 2027
+ }
+ }
+]
+
+```
+
+```json
+{
+ "code":"rest_forbidden",
+ "message":"Sorry, you are not allowed to do that.",
+ "data":{
+ "status":401
+ }
+}
+```
diff --git a/docs/rest-api/source/includes/wp-api-v3/intent.md b/docs/rest-api/source/includes/wp-api-v3/intent.md
new file mode 100644
index 00000000000..4f2daf7984f
--- /dev/null
+++ b/docs/rest-api/source/includes/wp-api-v3/intent.md
@@ -0,0 +1,87 @@
+# Payment intents
+
+The Payment Intents API provides comprehensive functionality for managing payment intents. You can create and manage them seamlessly through its endpoints.
+
+
+## Create payment intent
+
+_@since v6.6.0_
+
+Create new payment intent.
+
+### Error codes
+
+- `rest_forbidden` - indicates that the user or application does not have the necessary permissions to perform the requested action.
+- `wcpay_server_error` - Indicates that API had error processing the request. Usually occurs when request params are invalid like order is not found, and similar.
+
+### HTTP request
+
+
+
+ POST
+
/wp-json/wc/v3/payments/payment_intents
+
+
+
+```shell
+curl -X POST https://example.com/wp-json/wc/v3/payments/payment_intents \
+ -u consumer_key:consumer_secret \
+ -H "Content-Type: application/json" \
+ -d '{"payment_method":"","customer":"","order_id":""}'
+```
+
+> JSON response example:
+
+```json
+{
+ "id": "pi_4NxlPtR3eYmZSVZP0PPpwee8",
+ "amount": 1023,
+ "currency": "USD",
+ "created": 1696496578,
+ "customer": "cus_OUQoHGzJLw87Tk",
+ "payment_method": "pm_2NlT19R3eYmZSVZPRJEvxvwF",
+ "status": "succeeded",
+ "charge": {
+ "id": "ch_4NxlPtR3eYmZSVZP0ZLpKjfI",
+ "amount": 1023,
+ "application_fee_amount": 62,
+ "status": "succeeded",
+ "billing_details": {
+ "address": {
+ "city": "San Francisco",
+ "country": "US",
+ "line1": "123 Random St",
+ "line2": "-",
+ "postal_code": "94101",
+ "state": "CA"
+ },
+ "email": "random.email@example.com",
+ "name": "John Doe",
+ "phone": "5555555555"
+ },
+ "payment_method_details": {
+ "card": {
+ "amount_authorized": 1023,
+ "brand": "mastercard",
+ "capture_before": "",
+ "country": "US",
+ "exp_month": 5,
+ "exp_year": 2030,
+ "last4": "4321",
+ "three_d_secure": ""
+ }
+ }
+ }
+}
+
+```
+
+```json
+{
+ "code":"rest_forbidden",
+ "message":"Sorry, you are not allowed to do that.",
+ "data":{
+ "status":401
+ }
+}
+```
diff --git a/docs/rest-api/source/includes/wp-api-v3/order.md b/docs/rest-api/source/includes/wp-api-v3/order.md
index dbb566b6f80..6a6527c1023 100644
--- a/docs/rest-api/source/includes/wp-api-v3/order.md
+++ b/docs/rest-api/source/includes/wp-api-v3/order.md
@@ -161,6 +161,60 @@ curl -X POST https://example.com/wp-json/wc/v3/payments/orders/42/capture_author
}
}
```
+## Cancel an authorization
+
+_@since v5.7.0_
+
+Cancel the authorization of an existing uncaptured payment intent.
+
+### POST params
+
+- payment_intent_id: string
+
+### Error codes
+
+- `wcpay_missing_order` - Order not found
+- `wcpay_refunded_order_uncapturable` - Payment cannot be canceled
+- `wcpay_payment_uncapturable` - The payment cannot be canceled if intent status is not one of 'processing', 'requires_capture', or 'succeeded'
+- `wcpay_intent_order_mismatch` - Payment cannot be canceled because the order id does not match with payment intent id
+- `wcpay_cancel_error` - Unknown error
+
+### HTTP request
+
+
+
+ POST
+
/wp-json/wc/v3/payments/orders/<order_id>/cancel_authorization
+
+
+
+```shell
+curl -X POST https://example.com/wp-json/wc/v3/payments/orders/42/cancel_authorization \
+ -u consumer_key:consumer_secret \
+ -H "Content-Type: application/json" \
+ -d '{
+ "payment_intent_id": "pi_ZZZZZZZZZZZZZZZZAAAAAAAA"
+}'
+```
+
+> JSON response example:
+
+```json
+{
+ "status": "canceled",
+ "id": "pi_ZZZZZZZZZZZZZZZZAAAAAAAA"
+}
+```
+
+```json
+{
+ "code": "wcpay_missing_order",
+ "message": "Order not found",
+ "data": {
+ "status": 409
+ }
+}
+```
## Create customer
diff --git a/docs/rest-api/source/index.html.md b/docs/rest-api/source/index.html.md
index a34dc466d1a..6498e134d05 100644
--- a/docs/rest-api/source/index.html.md
+++ b/docs/rest-api/source/index.html.md
@@ -14,6 +14,8 @@ includes:
- wp-api-v3/authentication
- wp-api-v3/order
- wp-api-v3/authorization
+ - wp-api-v3/customer
+ - wp-api-v3/intent
search: false
---
diff --git a/includes/admin/class-wc-payments-admin-sections-overwrite.php b/includes/admin/class-wc-payments-admin-sections-overwrite.php
index 29d45e4bed8..ca46c93a887 100644
--- a/includes/admin/class-wc-payments-admin-sections-overwrite.php
+++ b/includes/admin/class-wc-payments-admin-sections-overwrite.php
@@ -25,7 +25,14 @@ class WC_Payments_Admin_Sections_Overwrite {
*/
public function __construct( WC_Payments_Account $account ) {
$this->account = $account;
+ }
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_filter( 'woocommerce_get_sections_checkout', [ $this, 'add_checkout_sections' ] );
}
diff --git a/includes/admin/class-wc-payments-admin-settings.php b/includes/admin/class-wc-payments-admin-settings.php
index 09796f6eaa7..49a062a57ee 100644
--- a/includes/admin/class-wc-payments-admin-settings.php
+++ b/includes/admin/class-wc-payments-admin-settings.php
@@ -35,7 +35,14 @@ class WC_Payments_Admin_Settings {
*/
public function __construct( WC_Payment_Gateway_WCPay $gateway ) {
$this->gateway = $gateway;
+ }
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'display_test_mode_notice' ] );
add_filter( 'plugin_action_links_' . plugin_basename( WCPAY_PLUGIN_FILE ), [ $this, 'add_plugin_links' ] );
}
diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php
index 42be3812564..2e2f309a4fb 100644
--- a/includes/admin/class-wc-payments-admin.php
+++ b/includes/admin/class-wc-payments-admin.php
@@ -136,21 +136,6 @@ public function __construct(
$this->incentives_service = $incentives_service;
$this->database_cache = $database_cache;
- add_action( 'admin_notices', [ $this, 'display_not_supported_currency_notice' ], 9999 );
- add_action( 'admin_notices', [ $this, 'display_isk_decimal_notice' ] );
-
- add_action( 'woocommerce_admin_order_data_after_payment_info', [ $this, 'render_order_edit_payment_details_container' ] );
-
- // Add menu items.
- add_action( 'admin_menu', [ $this, 'add_payments_menu' ], 0 );
- add_action( 'admin_init', [ $this, 'maybe_redirect_to_onboarding' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic.
- add_action( 'admin_enqueue_scripts', [ $this, 'maybe_redirect_overview_to_connect' ], 1 ); // Run this late (after `admin_init`) but before any scripts are actually enqueued.
- add_action( 'admin_enqueue_scripts', [ $this, 'register_payments_scripts' ], 9 );
- add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ], 9 );
- add_action( 'woocommerce_admin_field_payment_gateways', [ $this, 'payment_gateways_container' ] );
- add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'show_woopay_payment_method_name_admin' ] );
- add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'display_wcpay_transaction_fee' ] );
-
$this->admin_child_pages = [
'wc-payments-overview' => [
'id' => 'wc-payments-overview',
@@ -195,6 +180,29 @@ public function __construct(
];
}
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
+ add_action( 'admin_notices', [ $this, 'display_not_supported_currency_notice' ], 9999 );
+ add_action( 'admin_notices', [ $this, 'display_isk_decimal_notice' ] );
+
+ add_action( 'woocommerce_admin_order_data_after_payment_info', [ $this, 'render_order_edit_payment_details_container' ] );
+
+ // Add menu items.
+ add_action( 'admin_menu', [ $this, 'add_payments_menu' ], 0 );
+ add_action( 'admin_init', [ $this, 'maybe_redirect_to_onboarding' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic.
+ add_action( 'admin_enqueue_scripts', [ $this, 'maybe_redirect_overview_to_connect' ], 1 ); // Run this late (after `admin_init`) but before any scripts are actually enqueued.
+ add_action( 'admin_enqueue_scripts', [ $this, 'maybe_redirect_onboarding_flow_to_connect' ] );
+ add_action( 'admin_enqueue_scripts', [ $this, 'register_payments_scripts' ], 9 );
+ add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ], 9 );
+ add_action( 'woocommerce_admin_field_payment_gateways', [ $this, 'payment_gateways_container' ] );
+ add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'show_woopay_payment_method_name_admin' ] );
+ add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'display_wcpay_transaction_fee' ] );
+ }
+
/**
* Add notice explaining that the selected currency is not available.
*/
@@ -465,14 +473,16 @@ public function add_payments_menu() {
'path' => '/payments/transactions/details',
]
);
+
wc_admin_register_page(
[
- 'id' => 'wc-payments-disputes-details',
+ 'id' => 'wc-payments-disputes-details-legacy-redirect',
'title' => __( 'Dispute details', 'woocommerce-payments' ),
'parent' => 'wc-payments-disputes',
'path' => '/payments/disputes/details',
]
);
+
wc_admin_register_page(
[
'id' => 'wc-payments-disputes-challenge',
@@ -844,8 +854,11 @@ private function get_js_settings(): array {
'storeCurrency' => get_option( 'woocommerce_currency' ),
'isBnplAffirmAfterpayEnabled' => WC_Payments_Features::is_bnpl_affirm_afterpay_enabled(),
'isWooPayStoreCountryAvailable' => WooPay_Utilities::is_store_country_available(),
+ 'woopayLastDisableDate' => $this->wcpay_gateway->get_option( 'platform_checkout_last_disable_date' ),
'isStripeBillingEnabled' => WC_Payments_Features::is_stripe_billing_enabled(),
'isStripeBillingEligible' => WC_Payments_Features::is_stripe_billing_eligible(),
+ 'capabilityRequestNotices' => get_option( 'wcpay_capability_request_dismissed_notices ', [] ),
+ 'storeName' => get_bloginfo( 'name' ),
];
return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings );
@@ -1087,6 +1100,25 @@ public function maybe_redirect_overview_to_connect() {
$this->account->redirect_to_onboarding_welcome_page();
}
+ /**
+ * Prevent access to onboarding flow if the server is not connected.
+ * Redirect back to the connect page with an error message.
+ */
+ public function maybe_redirect_onboarding_flow_to_connect(): void {
+ $url_params = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification
+ if ( isset( $url_params['page'] ) && 'wc-admin' === $url_params['page']
+ && isset( $url_params['path'] ) && '/payments/onboarding' === $url_params['path'] && ! $this->payments_api_client->is_server_connected() ) {
+ $this->account->redirect_to_onboarding_welcome_page(
+ sprintf(
+ /* translators: %s: WooPayments */
+ __( 'Please connect to WordPress.com to start using %s.', 'woocommerce-payments' ),
+ 'WooPayments'
+ )
+ );
+ return;
+ }
+ }
+
/**
* Add woopay as a payment method to the edit order on admin.
*
@@ -1208,7 +1240,8 @@ public function add_transactions_notification_badge() {
private function get_disputes_awaiting_response_count() {
$send_callback = function() {
$request = Request::get( WC_Payments_API_Client::DISPUTES_API . '/status_counts' );
- return $request->send( 'wcpay_get_dispute_status_counts' );
+ $request->assign_hook( 'wcpay_get_dispute_status_counts' );
+ return $request->send();
};
$disputes_status_counts = $this->database_cache->get_or_add(
@@ -1237,7 +1270,8 @@ private function get_uncaptured_transactions_count() {
$send_callback = function() {
$request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API . '/summary' );
- return $request->send( 'wc_pay_get_authorizations_summary' );
+ $request->assign_hook( 'wc_pay_get_authorizations_summary' );
+ return $request->send();
};
$authorization_summary = $this->database_cache->get_or_add(
$cache_key,
diff --git a/includes/admin/class-wc-rest-payments-authorizations-controller.php b/includes/admin/class-wc-rest-payments-authorizations-controller.php
index 1965aa6c032..1b2993e3c71 100644
--- a/includes/admin/class-wc-rest-payments-authorizations-controller.php
+++ b/includes/admin/class-wc-rest-payments-authorizations-controller.php
@@ -63,7 +63,7 @@ public function register_routes() {
public function get_authorizations( WP_REST_Request $request ) {
$wcpay_request = List_Authorizations::from_rest_request( $request );
- return $wcpay_request->handle_rest_request( 'wcpay_list_authorizations_request' );
+ return $wcpay_request->handle_rest_request();
}
/**
@@ -74,7 +74,8 @@ public function get_authorizations( WP_REST_Request $request ) {
public function get_authorization( WP_REST_Request $request ) {
$payment_intent_id = $request->get_param( 'payment_intent_id' );
$request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API, $payment_intent_id );
- return $request->handle_rest_request( 'wcpay_get_authorization_request' );
+ $request->assign_hook( 'wcpay_get_authorization_request' );
+ return $request->handle_rest_request();
}
/**
@@ -82,6 +83,7 @@ public function get_authorization( WP_REST_Request $request ) {
*/
public function get_authorizations_summary() {
$request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API . '/summary' );
- return $request->handle_rest_request( 'wc_pay_get_authorizations_summary' );
+ $request->assign_hook( 'wc_pay_get_authorizations_summary' );
+ return $request->handle_rest_request();
}
}
diff --git a/includes/admin/class-wc-rest-payments-capital-controller.php b/includes/admin/class-wc-rest-payments-capital-controller.php
index 640063ea73f..b1635dfeb63 100644
--- a/includes/admin/class-wc-rest-payments-capital-controller.php
+++ b/includes/admin/class-wc-rest-payments-capital-controller.php
@@ -50,7 +50,8 @@ public function register_routes() {
*/
public function get_active_loan_summary() {
$request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/active_loan_summary' );
- return $request->handle_rest_request( 'wcpay_get_active_loan_summary_request' );
+ $request->assign_hook( 'wcpay_get_active_loan_summary_request' );
+ return $request->handle_rest_request();
}
/**
@@ -58,7 +59,8 @@ public function get_active_loan_summary() {
*/
public function get_loans() {
$request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/loans' );
- return $request->handle_rest_request( 'wcpay_get_loans_request' );
+ $request->assign_hook( 'wcpay_get_loans_request' );
+ return $request->handle_rest_request();
}
}
diff --git a/includes/admin/class-wc-rest-payments-charges-controller.php b/includes/admin/class-wc-rest-payments-charges-controller.php
index 906142f57ec..1c12cec0767 100644
--- a/includes/admin/class-wc-rest-payments-charges-controller.php
+++ b/includes/admin/class-wc-rest-payments-charges-controller.php
@@ -56,7 +56,7 @@ public function get_charge( $request ) {
try {
$wcpay_request = Get_Charge::create( $charge_id );
- $charge = $wcpay_request->send( 'wcpay_get_charge_request' );
+ $charge = $wcpay_request->send();
} catch ( API_Exception $e ) {
return rest_ensure_response( new WP_Error( 'wcpay_get_charge', $e->getMessage() ) );
}
diff --git a/includes/admin/class-wc-rest-payments-customer-controller.php b/includes/admin/class-wc-rest-payments-customer-controller.php
new file mode 100644
index 00000000000..e5a109e9d20
--- /dev/null
+++ b/includes/admin/class-wc-rest-payments-customer-controller.php
@@ -0,0 +1,281 @@
+customer_service = $customer_service;
+ }
+
+ /**
+ * Configure REST API routes.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P\w+)/payment_methods',
+ [
+ [
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => [ $this, 'get_customer_payment_methods' ],
+ 'permission_callback' => [ $this, 'check_permission' ],
+ ],
+ 'schema' => [ $this, 'get_item_schema' ],
+ ]
+ );
+ }
+
+ /**
+ * Retrieve transaction to respond with via API.
+ *
+ * @param WP_REST_Request $request Full data about the request.
+ */
+ public function get_customer_payment_methods( $request ) {
+ $customer_id = $request->get_param( 'customer_id' );
+ $payment_methods_types = WC_Payments::get_gateway()->get_upe_enabled_payment_method_ids() ?? [];
+ $payment_methods = [];
+
+ // Perhaps we can fetch it directly from server and avoid looping to get payment methods from cache.
+ foreach ( $payment_methods_types as $type ) {
+ try {
+ $payment_methods[] = $this->customer_service->get_payment_methods_for_customer( $customer_id, $type );
+ } catch ( API_Exception $e ) {
+ wp_send_json_error(
+ wp_strip_all_tags( $e->getMessage() ),
+ 403
+ );
+ }
+ }
+
+ $payment_methods = array_merge( ...$payment_methods );
+ $data = [];
+ foreach ( $payment_methods as $payment_method ) {
+ $response = $this->prepare_item_for_response( $payment_method, $request );
+ $data[] = $this->prepare_response_for_collection( $response );
+ }
+
+ return rest_ensure_response( $data );
+ }
+
+ /**
+ * Prepare each item for response.
+ *
+ * @param array|mixed $item Item to prepare.
+ * @param WP_REST_Request $request Request instance.
+ *
+ * @return WP_REST_Response|WP_Error|WP_REST_Response
+ */
+ public function prepare_item_for_response( $item, $request ) {
+
+ $prepared_item = [];
+
+ $prepared_item['id'] = $item['id'];
+ $prepared_item['type'] = $item['type'];
+ $prepared_item['billing_details'] = $item['billing_details'];
+ if ( array_key_exists( 'card', $item ) ) {
+ $prepared_item['card'] = [
+ 'brand' => $item['card']['brand'],
+ 'last4' => $item['card']['last4'],
+ 'exp_month' => $item['card']['exp_month'],
+ 'exp_year' => $item['card']['exp_year'],
+ ];
+ }
+ if ( array_key_exists( 'card', $item ) ) {
+ $prepared_item['card'] = [
+ 'brand' => $item['card']['brand'],
+ 'last4' => $item['card']['last4'],
+ 'exp_month' => $item['card']['exp_month'],
+ 'exp_year' => $item['card']['exp_year'],
+ ];
+ } elseif ( array_key_exists( 'sepa_debit', $item ) ) {
+ $prepared_item['sepa_debit'] = [
+ 'last4' => $item['sepa_debit']['last4'],
+ ];
+ } elseif ( array_key_exists( 'link', $item ) ) {
+ $prepared_item['link'] = [
+ 'email' => $item['link']['email'],
+ ];
+ }
+
+ $context = $request['context'] ?? 'view';
+ $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request );
+ $prepared_item = $this->filter_response_by_context( $prepared_item, $context );
+
+ return rest_ensure_response( $prepared_item );
+ }
+
+ /**
+ * Item schema.
+ *
+ * @return array
+ */
+ public function get_item_schema() {
+ return [
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'payment_method',
+ 'type' => 'object',
+ 'properties' => [
+ 'id' => [
+ 'description' => __( 'ID for the payment method.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'type' => [
+ 'description' => __( 'Type of the payment method.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'enum' => [ 'card', 'sepa_debit', 'link' ],
+ 'context' => [ 'view' ],
+ ],
+ 'billing_details' => [
+ 'description' => __( 'Billing details for the payment method.', 'woocommerce-payments' ),
+ 'type' => 'object',
+ 'context' => [ 'view' ],
+ 'properties' => [
+ 'address' => [
+ 'description' => __( 'Address associated with the billing details.', 'woocommerce-payments' ),
+ 'type' => 'object',
+ 'context' => [ 'view' ],
+ 'properties' => [
+ 'city' => [
+ 'description' => __( 'City of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'country' => [
+ 'description' => __( 'Country of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'line1' => [
+ 'description' => __( 'Line 1 of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'line2' => [
+ 'description' => __( 'Line 2 of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'postal_code' => [
+ 'description' => __( 'Postal code of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'state' => [
+ 'description' => __( 'State of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ ],
+ ],
+ 'email' => [
+ 'description' => __( 'Email associated with the billing details.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'format' => 'email',
+ 'context' => [ 'view' ],
+ ],
+ 'name' => [
+ 'description' => __( 'Name associated with the billing details.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'phone' => [
+ 'description' => __( 'Phone number associated with the billing details.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ ],
+ ],
+ 'card' => [
+ 'description' => __( 'Card details for the payment method.', 'woocommerce-payments' ),
+ 'type' => 'object',
+ 'context' => [ 'view' ],
+ 'properties' => [
+ 'brand' => [
+ 'description' => __( 'Brand of the card.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'last4' => [
+ 'description' => __( 'Last 4 digits of the card.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'exp_month' => [
+ 'description' => __( 'Expiration month of the card.', 'woocommerce-payments' ),
+ 'type' => 'integer',
+ 'context' => [ 'view' ],
+ ],
+ 'exp_year' => [
+ 'description' => __( 'Expiration year of the card.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ ],
+ ],
+ 'sepa_debit' => [
+ 'description' => __( 'SEPA Debit details for the payment method.', 'woocommerce-payments' ),
+ 'type' => 'object',
+ 'context' => [ 'view' ],
+ 'properties' => [
+ 'last4' => [
+ 'description' => __( 'Last 4 digits of the SEPA Debit.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ ],
+ ],
+ 'link' => [
+ 'description' => __( 'Link details for the payment method.', 'woocommerce-payments' ),
+ 'type' => 'object',
+ 'context' => [ 'view' ],
+ 'properties' => [
+ 'email' => [
+ 'description' => __( 'Email associated with the link.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'format' => 'email',
+ 'context' => [ 'view' ],
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+}
diff --git a/includes/admin/class-wc-rest-payments-deposits-controller.php b/includes/admin/class-wc-rest-payments-deposits-controller.php
index 84f62c5381f..d4406ee19ca 100644
--- a/includes/admin/class-wc-rest-payments-deposits-controller.php
+++ b/includes/admin/class-wc-rest-payments-deposits-controller.php
@@ -101,7 +101,7 @@ public function register_routes() {
public function get_deposits( $request ) {
$wcpay_request = List_Deposits::from_rest_request( $request );
- return $wcpay_request->handle_rest_request( 'wcpay_list_deposits_request' );
+ return $wcpay_request->handle_rest_request();
}
/**
@@ -119,7 +119,8 @@ public function get_deposits_summary( $request ) {
*/
public function get_deposits_overview() {
$request = Request::get( WC_Payments_API_Client::DEPOSITS_API . '/overview' );
- return $request->handle_rest_request( 'wcpay_get_deposits_overview' );
+ $request->assign_hook( 'wcpay_get_deposits_overview' );
+ return $request->handle_rest_request();
}
/**
@@ -127,7 +128,8 @@ public function get_deposits_overview() {
*/
public function get_all_deposits_overviews() {
$request = Request::get( WC_Payments_API_Client::DEPOSITS_API . '/overview-all' );
- return $request->handle_rest_request( 'wcpay_get_all_deposits_overviews' );
+ $request->assign_hook( 'wcpay_get_all_deposits_overviews' );
+ return $request->handle_rest_request();
}
/**
@@ -138,7 +140,8 @@ public function get_all_deposits_overviews() {
public function get_deposit( $request ) {
$deposit_id = $request->get_param( 'deposit_id' );
$wcpay_request = Request::get( WC_Payments_API_Client::DEPOSITS_API, $deposit_id );
- return $wcpay_request->handle_rest_request( 'wcpay_get_deposit' );
+ $wcpay_request->assign_hook( 'wcpay_get_deposit' );
+ return $wcpay_request->handle_rest_request();
}
/**
diff --git a/includes/admin/class-wc-rest-payments-disputes-controller.php b/includes/admin/class-wc-rest-payments-disputes-controller.php
index 036ef1319b6..e2efc774a83 100644
--- a/includes/admin/class-wc-rest-payments-disputes-controller.php
+++ b/includes/admin/class-wc-rest-payments-disputes-controller.php
@@ -89,7 +89,7 @@ public function register_routes() {
public function get_disputes( WP_REST_Request $request ) {
$wcpay_request = List_Disputes::from_rest_request( $request );
- return $wcpay_request->handle_rest_request( 'wcpay_list_disputes_request', $request );
+ return $wcpay_request->handle_rest_request();
}
/**
diff --git a/includes/admin/class-wc-rest-payments-documents-controller.php b/includes/admin/class-wc-rest-payments-documents-controller.php
index ab0c5fc72e9..2e73f06404b 100644
--- a/includes/admin/class-wc-rest-payments-documents-controller.php
+++ b/includes/admin/class-wc-rest-payments-documents-controller.php
@@ -62,7 +62,7 @@ public function register_routes() {
*/
public function get_documents( $request ) {
$wcpay_request = List_Documents::from_rest_request( $request );
- return $wcpay_request->handle_rest_request( 'wcpay_list_documents_request' );
+ return $wcpay_request->handle_rest_request();
}
/**
diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php
index b5915678ca7..28511909686 100644
--- a/includes/admin/class-wc-rest-payments-orders-controller.php
+++ b/includes/admin/class-wc-rest-payments-orders-controller.php
@@ -155,9 +155,25 @@ public function capture_terminal_payment( WP_REST_Request $request ) {
);
}
+ // Do not process already processed orders to prevent double-charging.
+ $processed_order_intent_statuses = [
+ Intent_Status::SUCCEEDED,
+ Intent_Status::CANCELED,
+ Intent_Status::PROCESSING,
+ ];
+ $stored_intent_id = $order->get_meta( WC_Payments_Order_Service::INTENT_ID_META_KEY );
+ $stored_intent_status = $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY );
+ if (
+ in_array( $stored_intent_status, $processed_order_intent_statuses, true ) ||
+ ( $stored_intent_id && $stored_intent_id !== $intent_id )
+ ) {
+ return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured for completed or processed orders.', 'woocommerce-payments' ), [ 'status' => 409 ] );
+ }
+
// Do not process intents that can't be captured.
$request = Get_Intention::create( $intent_id );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
@@ -197,7 +213,7 @@ public function capture_terminal_payment( WP_REST_Request $request ) {
'id' => $intent->get_id(),
];
- $result = $is_intent_captured ? $result_for_captured_intent : $this->gateway->capture_charge( $order, false );
+ $result = $is_intent_captured ? $result_for_captured_intent : $this->gateway->capture_charge( $order, false, $intent_metadata );
if ( Intent_Status::SUCCEEDED !== $result['status'] ) {
$http_code = $result['http_code'] ?? 502;
@@ -257,7 +273,8 @@ public function capture_authorization( WP_REST_Request $request ) {
// Do not process intents that can't be captured.
$request = Get_Intention::create( $intent_id );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
@@ -272,7 +289,7 @@ public function capture_authorization( WP_REST_Request $request ) {
$this->add_fraud_outcome_manual_entry( $order, 'approve' );
- $result = $this->gateway->capture_charge( $order, false );
+ $result = $this->gateway->capture_charge( $order, false, $intent_metadata );
if ( Intent_Status::SUCCEEDED !== $result['status'] ) {
return new WP_Error(
@@ -390,7 +407,8 @@ public function create_terminal_intent( $request ) {
$wcpay_server_request->set_metadata( $metadata );
$wcpay_server_request->set_payment_method_types( $this->get_terminal_intent_payment_method( $request ) );
$wcpay_server_request->set_capture_method( 'manual' === $this->get_terminal_intent_capture_method( $request ) );
- $intent = $wcpay_server_request->send( 'wcpay_create_intent_request', $order );
+ $wcpay_server_request->set_hook_args( $order );
+ $intent = $wcpay_server_request->send();
return rest_ensure_response(
[
@@ -482,7 +500,8 @@ public function cancel_authorization( WP_REST_Request $request ) {
// Do not process intents that can't be canceled.
$request = Get_Intention::create( $intent_id );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
diff --git a/includes/admin/class-wc-rest-payments-payment-intents-controller.php b/includes/admin/class-wc-rest-payments-payment-intents-controller.php
index 47af2983363..53d02e8afa3 100644
--- a/includes/admin/class-wc-rest-payments-payment-intents-controller.php
+++ b/includes/admin/class-wc-rest-payments-payment-intents-controller.php
@@ -1,11 +1,16 @@
forward_request( 'get_intent', [ $payment_intent_id ] );
}
+
}
diff --git a/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php b/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php
new file mode 100644
index 00000000000..37b711ebc2e
--- /dev/null
+++ b/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php
@@ -0,0 +1,376 @@
+namespace,
+ '/' . $this->rest_base,
+ [
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => [ $this, 'create_payment_intent' ],
+ 'permission_callback' => [ $this, 'check_permission' ],
+ 'schema' => [ $this, 'get_item_schema' ],
+ ]
+ );
+ }
+
+ /**
+ * WC_REST_Payments_Payment_Intents_Create_Controller constructor.
+ *
+ * @param WC_Payments_API_Client $api_client WooCommerce Payments API client.
+ * @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway.
+ * @param OrderService $order_service The new order servie.
+ * @param Level3Service $level3_service Level3 service instance.
+ */
+ public function __construct(
+ WC_Payments_API_Client $api_client,
+ WC_Payment_Gateway_WCPay $gateway,
+ OrderService $order_service,
+ Level3Service $level3_service
+ ) {
+ parent::__construct( $api_client );
+
+ $this->gateway = $gateway;
+ $this->order_service = $order_service;
+ $this->level3_service = $level3_service;
+ }
+
+ /**
+ * Create a payment intent.
+ *
+ * @param WP_REST_Request $request data about the request.
+ *
+ * @throws Rest_Request_Exception
+ */
+ public function create_payment_intent( $request ) {
+ try {
+
+ $order_id = $request->get_param( 'order_id' );
+ $order = wc_get_order( $order_id );
+ if ( ! $order ) {
+ throw new Rest_Request_Exception( __( 'Order not found', 'woocommerce-payments' ) );
+ }
+
+ $wcpay_server_request = Create_And_Confirm_Intention::create();
+
+ $currency = strtolower( $order->get_currency() );
+ $amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency );
+ $wcpay_server_request->set_currency_code( $currency );
+ $wcpay_server_request->set_amount( $amount );
+
+ $metadata = $this->order_service->get_payment_metadata( $order_id, Payment_Type::SINGLE() );
+ $wcpay_server_request->set_metadata( $metadata );
+
+ $wcpay_server_request->set_customer( $request->get_param( 'customer' ) );
+ $wcpay_server_request->set_level3( $this->level3_service->get_data_from_order( $order_id ) );
+ $wcpay_server_request->set_payment_method( $request->get_param( 'payment_method' ) );
+ $wcpay_server_request->set_payment_method_types( [ 'card' ] );
+ $wcpay_server_request->set_off_session( true );
+ $wcpay_server_request->set_capture_method( $this->gateway->get_option( 'manual_capture' ) && ( 'yes' === $this->gateway->get_option( 'manual_capture' ) ) );
+
+ $wcpay_server_request->assign_hook( 'wcpay_create_and_confirm_intent_request_api' );
+ $intent = $wcpay_server_request->send();
+
+ $response = $this->prepare_item_for_response( $intent, $request );
+ return rest_ensure_response( $this->prepare_response_for_collection( $response ) );
+
+ } catch ( \Throwable $e ) {
+ Logger::error( 'Failed to create an intention via REST API: ' . $e );
+ return new WP_Error( 'wcpay_server_error', $e->getMessage(), [ 'status' => 500 ] );
+ }
+ }
+
+
+ /**
+ * Item schema.
+ *
+ * @return array
+ */
+ public function get_item_schema() {
+ return [
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'payment_intent',
+ 'type' => 'object',
+ 'properties' => [
+ 'id' => [
+ 'description' => __( 'ID for the payment intent.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'amount' => [
+ 'description' => __( 'The amount of the transaction.', 'woocommerce-payments' ),
+ 'type' => 'integer',
+ 'context' => [ 'view' ],
+ ],
+ 'currency' => [
+ 'description' => __( 'The currency of the transaction.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'created' => [
+ 'description' => __( 'Timestamp for when the payment intent was created.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'customer' => [
+ 'description' => __( 'The customer id of the intent', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'status' => [
+ 'description' => __( 'The status of the payment intent.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'charge' => [
+ 'description' => __( 'Charge object associated with this payment intention.', 'woocommerce-payments' ),
+ 'type' => 'object',
+ 'context' => [ 'view' ],
+ 'properties' => [
+ 'id' => [
+ 'description' => 'ID for the charge.',
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'amount' => [
+ 'description' => 'The amount of the charge.',
+ 'type' => 'integer',
+ 'context' => [ 'view' ],
+ ],
+ 'payment_method_details' => [
+ 'description' => 'Details for the payment method used for the charge.',
+ 'type' => 'object',
+ 'properties' => [
+ 'card' => [
+ 'description' => 'Details for a card payment method.',
+ 'type' => 'object',
+ 'properties' => [
+ 'amount_authorized' => [
+ 'description' => 'The amount authorized by the card.',
+ 'type' => 'integer',
+ ],
+ 'brand' => [
+ 'description' => 'The brand of the card.',
+ 'type' => 'string',
+ ],
+ 'capture_before' => [
+ 'description' => 'Timestamp for when the authorization must be captured.',
+ 'type' => 'string',
+ ],
+ 'country' => [
+ 'description' => 'The ISO country code.',
+ 'type' => 'string',
+ ],
+ 'exp_month' => [
+ 'description' => 'The expiration month of the card.',
+ 'type' => 'integer',
+ ],
+ 'exp_year' => [
+ 'description' => 'The expiration year of the card.',
+ 'type' => 'integer',
+ ],
+ 'last4' => [
+ 'description' => 'The last 4 digits of the card.',
+ 'type' => 'string',
+ ],
+ 'three_d_secure' => [
+ 'description' => 'Details for 3D Secure authentication.',
+ 'type' => 'object',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'billing_details' => [
+ 'description' => __( 'Billing details for the payment method.', 'woocommerce-payments' ),
+ 'type' => 'object',
+ 'context' => [ 'view' ],
+ 'properties' => [
+ 'address' => [
+ 'description' => __( 'Address associated with the billing details.', 'woocommerce-payments' ),
+ 'type' => 'object',
+ 'context' => [ 'view' ],
+ 'properties' => [
+ 'city' => [
+ 'description' => __( 'City of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'country' => [
+ 'description' => __( 'Country of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'line1' => [
+ 'description' => __( 'Line 1 of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'line2' => [
+ 'description' => __( 'Line 2 of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'postal_code' => [
+ 'description' => __( 'Postal code of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'state' => [
+ 'description' => __( 'State of the billing address.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ ],
+ ],
+ 'email' => [
+ 'description' => __( 'Email associated with the billing details.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'format' => 'email',
+ 'context' => [ 'view' ],
+ ],
+ 'name' => [
+ 'description' => __( 'Name associated with the billing details.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'phone' => [
+ 'description' => __( 'Phone number associated with the billing details.', 'woocommerce-payments' ),
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ ],
+ ],
+ 'payment_method' => [
+ 'description' => 'The payment method associated with this charge.',
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ 'application_fee_amount' => [
+ 'description' => 'The application fee amount.',
+ 'type' => 'integer',
+ 'context' => [ 'view' ],
+ ],
+ 'status' => [
+ 'description' => 'The status of the payment intent created.',
+ 'type' => 'string',
+ 'context' => [ 'view' ],
+ ],
+ ],
+ ],
+
+ ],
+ ];
+ }
+
+ /**
+ * Prepare each item for response.
+ *
+ * @param array|mixed $item Item to prepare.
+ * @param WP_REST_Request $request Request instance.
+ *
+ * @return WP_REST_Response|WP_Error|WP_REST_Response
+ */
+ public function prepare_item_for_response( $item, $request ) {
+ $prepared_item = [];
+ $prepared_item['id'] = $item->get_id();
+ $prepared_item['amount'] = $item->get_amount();
+ $prepared_item['currency'] = $item->get_currency();
+ $prepared_item['created'] = $item->get_created()->getTimestamp();
+ $prepared_item['customer'] = $item->get_customer_id();
+ $prepared_item['payment_method'] = $item->get_payment_method_id();
+ $prepared_item['status'] = $item->get_status();
+
+ try {
+ $charge = $item->get_charge();
+ $prepared_item['charge']['id'] = $charge->get_id();
+ $prepared_item['charge']['amount'] = $charge->get_amount();
+ $prepared_item['charge']['application_fee_amount'] = $charge->get_application_fee_amount();
+ $prepared_item['charge']['status'] = $charge->get_status();
+
+ $billing_details = $charge->get_billing_details();
+ if ( isset( $billing_details['address'] ) ) {
+ $prepared_item['charge']['billing_details']['address']['city'] = $billing_details['address']['city'] ?? '';
+ $prepared_item['charge']['billing_details']['address']['country'] = $billing_details['address']['country'] ?? '';
+ $prepared_item['charge']['billing_details']['address']['line1'] = $billing_details['address']['line1'] ?? '';
+ $prepared_item['charge']['billing_details']['address']['line2'] = $billing_details['address']['line2'] ?? '';
+ $prepared_item['charge']['billing_details']['address']['postal_code'] = $billing_details['address']['postal_code'] ?? '';
+ $prepared_item['charge']['billing_details']['address']['state'] = $billing_details['address']['state'] ?? '';
+ }
+ $prepared_item['charge']['billing_details']['email'] = $billing_details['email'] ?? '';
+ $prepared_item['charge']['billing_details']['name'] = $billing_details['name'] ?? '';
+ $prepared_item['charge']['billing_details']['phone'] = $billing_details['phone'] ?? '';
+
+ $payment_method_details = $charge->get_payment_method_details();
+ if ( isset( $payment_method_details['card'] ) ) {
+ $prepared_item['charge']['payment_method_details']['card']['amount_authorized'] = $payment_method_details['card']['amount_authorized'] ?? '';
+ $prepared_item['charge']['payment_method_details']['card']['brand'] = $payment_method_details['card']['brand'] ?? '';
+ $prepared_item['charge']['payment_method_details']['card']['capture_before'] = $payment_method_details['card']['capture_before'] ?? '';
+ $prepared_item['charge']['payment_method_details']['card']['country'] = $payment_method_details['card']['country'] ?? '';
+ $prepared_item['charge']['payment_method_details']['card']['exp_month'] = $payment_method_details['card']['exp_month'] ?? '';
+ $prepared_item['charge']['payment_method_details']['card']['exp_year'] = $payment_method_details['card']['exp_year'] ?? '';
+ $prepared_item['charge']['payment_method_details']['card']['last4'] = $payment_method_details['card']['last4'] ?? '';
+ $prepared_item['charge']['payment_method_details']['card']['three_d_secure'] = $payment_method_details['card']['three_d_secure'] ?? '';
+ }
+ } catch ( \Throwable $e ) {
+ Logger::error( 'Failed to prepare payment intent for response: ' . $e );
+ }
+
+ $context = $request['context'] ?? 'view';
+ $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request );
+ $prepared_item = $this->filter_response_by_context( $prepared_item, $context );
+
+ return rest_ensure_response( $prepared_item );
+ }
+
+
+}
diff --git a/includes/admin/class-wc-rest-payments-reader-controller.php b/includes/admin/class-wc-rest-payments-reader-controller.php
index 2da16d48b88..fa6fa5192e0 100644
--- a/includes/admin/class-wc-rest-payments-reader-controller.php
+++ b/includes/admin/class-wc-rest-payments-reader-controller.php
@@ -238,8 +238,10 @@ private function fetch_readers(): array {
if ( ! $readers ) {
// Retrieve terminal readers.
- $request = Request::get( WC_Payments_API_Client::TERMINAL_READERS_API );
- $readers_data = $request->send( 'wcpay_get_terminal_readers_request' );
+ $request = Request::get( WC_Payments_API_Client::TERMINAL_READERS_API );
+ $request->assign_hook( 'wcpay_get_terminal_readers_request' );
+
+ $readers_data = $request->send();
// Retrieve the readers by charges.
$reader_by_charges = $this->api_client->get_readers_charge_summary( gmdate( 'Y-m-d', time() ) );
@@ -275,7 +277,7 @@ public function generate_print_receipt( $request ) {
try {
/* Collect the data, available on the server side. */
$wcpay_request = Get_Intention::create( $request->get_param( 'payment_intent_id' ) );
- $payment_intent = $wcpay_request->send( 'wcpay_get_intent_request' );
+ $payment_intent = $wcpay_request->send();
if ( Intent_Status::SUCCEEDED !== $payment_intent->get_status() ) {
throw new \RuntimeException( __( 'Invalid payment intent', 'woocommerce-payments' ) );
}
@@ -283,7 +285,7 @@ public function generate_print_receipt( $request ) {
$charge = $payment_intent->get_charge();
$charge_id = $charge ? $charge->get_id() : null;
$charge_request = Get_Charge::create( $charge_id );
- $charge_array = $charge_request->send( 'wcpay_get_charge_request' );
+ $charge_array = $charge_request->send();
/* Collect receipt data, stored on the store side. */
$order = wc_get_order( $charge_array['order']['number'] );
diff --git a/includes/admin/class-wc-rest-payments-settings-controller.php b/includes/admin/class-wc-rest-payments-settings-controller.php
index ae5ca0361d2..71b96a382ec 100644
--- a/includes/admin/class-wc-rest-payments-settings-controller.php
+++ b/includes/admin/class-wc-rest-payments-settings-controller.php
@@ -303,6 +303,15 @@ public function register_routes() {
'permission_callback' => [ $this, 'check_permission' ],
]
);
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/request-capability',
+ [
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => [ $this, 'request_capability' ],
+ 'permission_callback' => [ $this, 'check_permission' ],
+ ]
+ );
}
/**
@@ -529,7 +538,6 @@ public function update_settings( WP_REST_Request $request ) {
$this->update_payment_request_enabled_locations( $request );
$this->update_payment_request_appearance( $request );
$this->update_is_saved_cards_enabled( $request );
- $this->update_account( $request );
$this->update_is_woopay_enabled( $request );
$this->update_woopay_store_logo( $request );
$this->update_woopay_custom_message( $request );
@@ -539,6 +547,12 @@ public function update_settings( WP_REST_Request $request ) {
$this->update_fraud_protection_settings( $request );
$this->update_is_stripe_billing_enabled( $request );
+ $update_account_result = $this->update_account( $request );
+
+ if ( is_wp_error( $update_account_result ) ) {
+ return new WP_REST_Response( [ 'server_error' => $update_account_result->get_error_message() ], 400 );
+ }
+
return new WP_REST_Response( [], 200 );
}
@@ -757,7 +771,7 @@ private function update_account( WP_REST_Request $request ) {
$updated_fields['deposit_schedule_interval'] = $this->wcpay_gateway->get_option( 'deposit_schedule_interval' );
}
- $this->wcpay_gateway->update_account_settings( $updated_fields );
+ return $this->wcpay_gateway->update_account_settings( $updated_fields );
}
/**
@@ -881,6 +895,11 @@ private function update_woopay_enabled_locations( WP_REST_Request $request ) {
return;
}
+ $is_woopay_enabled = WC_Payments_Features::is_woopay_enabled();
+ if ( ! $is_woopay_enabled ) {
+ return;
+ }
+
$woopay_enabled_locations = $request->get_param( 'woopay_enabled_locations' );
$all_locations = $this->wcpay_gateway->form_fields['payment_request_button_locations']['options'];
@@ -988,6 +1007,28 @@ public function schedule_stripe_billing_migration( WP_REST_Request $request = nu
}
}
+ /**
+ * Request a specific capability.
+ *
+ * @param WP_REST_Request $request The request object. Optional. If passed, the function will return a REST response.
+ *
+ * @return WP_REST_Response|WP_Error The response object, if this is a REST request.
+ */
+ public function request_capability( WP_REST_Request $request = null ) {
+ $id = $request->get_param( 'id' );
+ $capability_key_map = $this->wcpay_gateway->get_payment_method_capability_key_map();
+ $payment_method_statuses = $this->wcpay_gateway->get_upe_enabled_payment_method_statuses();
+ $stripe_key = $capability_key_map[ $id ] ?? null;
+
+ if ( array_key_exists( $stripe_key, $payment_method_statuses )
+ && 'unrequested' === $payment_method_statuses[ $stripe_key ]['status'] ) {
+ $request_result = $this->api_client->request_capability( $stripe_key, true );
+ $this->wcpay_gateway->refresh_cached_account_data();
+ }
+
+ return rest_ensure_response( $request_result );
+ }
+
/**
* Get the AVS check enabled status from the ruleset config.
*
diff --git a/includes/admin/class-wc-rest-payments-terminal-locations-controller.php b/includes/admin/class-wc-rest-payments-terminal-locations-controller.php
index 4cee86908eb..c0f0f7754ee 100644
--- a/includes/admin/class-wc-rest-payments-terminal-locations-controller.php
+++ b/includes/admin/class-wc-rest-payments-terminal-locations-controller.php
@@ -238,8 +238,8 @@ public function get_location( $request ) {
}
// If the location is missing, fetch it individually and reload the transient.
$request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API, $location_id );
-
- $location = $request->send( 'wcpay_get_terminal_location' );
+ $request->assign_hook( 'wcpay_get_terminal_location' );
+ $location = $request->send();
$this->reload_locations();
return rest_ensure_response( $this->extract_location_fields( $location ) );
@@ -304,8 +304,9 @@ private function extract_location_fields( array $location ): array {
private function fetch_locations(): array {
$locations = get_transient( static::STORE_LOCATIONS_TRANSIENT_KEY );
if ( ! $locations ) {
- $request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API );
- $locations = $request->send( 'wcpay_get_terminal_locations' );
+ $request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API );
+ $request->assign_hook( 'wcpay_get_terminal_locations' );
+ $locations = $request->send();
set_transient( static::STORE_LOCATIONS_TRANSIENT_KEY, $locations, DAY_IN_SECONDS );
}
@@ -319,8 +320,10 @@ private function fetch_locations(): array {
* @throws API_Exception If request to server fails.
*/
private function reload_locations() {
- $request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API );
- $locations = $request->send( 'wcpay_get_terminal_locations' );
+ $request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API );
+ $request->assign_hook( 'wcpay_get_terminal_locations' );
+
+ $locations = $request->send();
set_transient( static::STORE_LOCATIONS_TRANSIENT_KEY, $locations, DAY_IN_SECONDS );
}
}
diff --git a/includes/admin/class-wc-rest-payments-tos-controller.php b/includes/admin/class-wc-rest-payments-tos-controller.php
index 3e3a3a8864e..b5c0311376b 100644
--- a/includes/admin/class-wc-rest-payments-tos-controller.php
+++ b/includes/admin/class-wc-rest-payments-tos-controller.php
@@ -142,7 +142,7 @@ private function handle_tos_accepted() {
$request = Add_Account_Tos_Agreement::create();
$request->set_source( 'settings-popup' );
$request->set_user_name( $user_name );
- $request->send( 'wcpay_add_account_tos_agreement' );
+ $request->send();
$this->account->refresh_account_data();
}
diff --git a/includes/admin/class-wc-rest-payments-transactions-controller.php b/includes/admin/class-wc-rest-payments-transactions-controller.php
index 7f7f56c1f84..5c353b1113b 100644
--- a/includes/admin/class-wc-rest-payments-transactions-controller.php
+++ b/includes/admin/class-wc-rest-payments-transactions-controller.php
@@ -118,7 +118,7 @@ public function get_transactions( $request ) {
$wcpay_request = List_Transactions::from_rest_request( $request );
- return $wcpay_request->handle_rest_request( 'wcpay_list_transactions_request' );
+ return $wcpay_request->handle_rest_request();
}
/**
diff --git a/includes/admin/class-wc-rest-payments-vat-controller.php b/includes/admin/class-wc-rest-payments-vat-controller.php
index d342521b57a..02ac264cf12 100644
--- a/includes/admin/class-wc-rest-payments-vat-controller.php
+++ b/includes/admin/class-wc-rest-payments-vat-controller.php
@@ -71,7 +71,8 @@ public function register_routes() {
public function validate_vat( $request ) {
$vat_number = sanitize_text_field( $request->get_param( 'vat_number' ) );
$server_request = Request::get( WC_Payments_API_Client::VAT_API, $vat_number );
- return $server_request->handle_rest_request( 'wcpay_validate_vat_request' );
+ $server_request->assign_hook( 'wcpay_validate_vat_request' );
+ return $server_request->handle_rest_request();
}
/**
diff --git a/includes/admin/tasks/class-wc-payments-task-disputes.php b/includes/admin/tasks/class-wc-payments-task-disputes.php
index bfc27c59ad5..bbefc441e17 100644
--- a/includes/admin/tasks/class-wc-payments-task-disputes.php
+++ b/includes/admin/tasks/class-wc-payments-task-disputes.php
@@ -223,8 +223,8 @@ public function get_action_url() {
add_query_arg(
[
'page' => 'wc-admin',
- 'path' => '%2Fpayments%2Fdisputes%2Fdetails',
- 'id' => $dispute['dispute_id'],
+ 'path' => '%2Fpayments%2Ftransactions%2Fdetails',
+ 'id' => $dispute['charge_id'],
],
'admin.php'
)
diff --git a/includes/class-database-cache.php b/includes/class-database-cache.php
index 08a22ab66b8..fd6d6290eea 100644
--- a/includes/class-database-cache.php
+++ b/includes/class-database-cache.php
@@ -17,7 +17,6 @@ class Database_Cache {
const ONBOARDING_FIELDS_DATA_KEY = 'wcpay_onboarding_fields_data';
const BUSINESS_TYPES_KEY = 'wcpay_business_types_data';
const CURRENCIES_KEY = 'wcpay_multi_currency_cached_currencies';
- const CUSTOMER_CURRENCIES_KEY = 'wcpay_multi_currency_customer_currencies';
const PAYMENT_PROCESS_FACTORS_KEY = 'wcpay_payment_process_factors';
/**
@@ -80,7 +79,14 @@ class Database_Cache {
*/
public function __construct() {
$this->refresh_disabled = false;
+ }
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_action( 'action_scheduler_before_execute', [ $this, 'disable_refresh' ] );
}
diff --git a/includes/class-duplicate-payment-prevention-service.php b/includes/class-duplicate-payment-prevention-service.php
index 47a5e69a386..2b7ff8d3099 100644
--- a/includes/class-duplicate-payment-prevention-service.php
+++ b/includes/class-duplicate-payment-prevention-service.php
@@ -91,8 +91,9 @@ public function check_payment_intent_attached_to_order_succeeded( WC_Order $orde
}
try {
- $request = Get_Intention::create( $intent_id );
- $intent = $request->send( 'wcpay_get_intention_request' );
+ $request = Get_Intention::create( $intent_id );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$intent_status = $intent->get_status();
} catch ( Exception $e ) {
Logger::error( 'Failed to fetch attached payment intent: ' . $e );
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index a03819bbf98..cc33a732379 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -44,6 +44,9 @@
use WCPay\Internal\Service\PaymentProcessingService;
use WCPay\Internal\Payment\Factor;
use WCPay\Internal\Payment\Router;
+use WCPay\Internal\Payment\State\CompletedState;
+use WCPay\Internal\Service\Level3Service;
+use WCPay\Internal\Service\OrderService;
/**
* Gateway class for WooPayments
@@ -398,6 +401,8 @@ public function __construct(
'link' => 'link_payments',
'affirm' => 'affirm_payments',
'afterpay_clearpay' => 'afterpay_clearpay_payments',
+ 'klarna' => 'klarna_payments',
+ 'jcb' => 'jcb_payments',
];
// WooPay utilities.
@@ -805,11 +810,23 @@ function_exists( 'wcs_order_contains_subscription' )
*
* @param WC_Order $order Order that needs payment.
* @return array|null Array if processed, null if the new process is not supported.
+ * @throws Exception If the payment process could not be completed.
*/
public function new_process_payment( WC_Order $order ) {
+ $manual_capture = $this->get_capture_type() === Payment_Capture_Type::MANUAL();
+
// Important: No factors are provided here, they were meant just for `Feature`.
$service = wcpay_get_container()->get( PaymentProcessingService::class );
- return $service->process_payment( $order->get_id() );
+ $state = $service->process_payment( $order->get_id(), $manual_capture );
+
+ if ( $state instanceof CompletedState ) {
+ return [
+ 'result' => 'success',
+ 'redirect' => $this->get_return_url( $order ),
+ ];
+ }
+
+ throw new Exception( __( 'The payment process could not be completed.', 'woocommerce-payments' ) );
}
/**
@@ -1210,7 +1227,8 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
if ( ! empty( $woopay_intent_id ) ) {
// If the intent is included in the request use that intent.
$request = Get_Intention::create( $woopay_intent_id );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$intent_meta_order_id_raw = $intent->get_metadata()['order_id'] ?? '';
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
@@ -1234,7 +1252,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$request->set_off_session( $payment_information->is_merchant_initiated() );
$request->set_payment_methods( $payment_methods );
$request->set_cvc_confirmation( $payment_information->get_cvc_confirmation() );
-
+ $request->set_hook_args( $payment_information );
// Add specific payment method parameters to the request.
$this->modify_create_intent_parameters_when_processing_payment( $request, $payment_information, $order );
@@ -1267,17 +1285,13 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
}
}
- // For Stripe Link with deferred intent UPE, we must create mandate to acknowledge that terms have been shown to customer.
- if (
- WC_Payments_Features::is_upe_deferred_intent_enabled() &&
- Payment_Method::CARD === $this->get_selected_stripe_payment_type_id() &&
- in_array( Payment_Method::LINK, $this->get_upe_enabled_payment_method_ids(), true )
- ) {
+ // For Stripe Link & SEPA with deferred intent UPE, we must create mandate to acknowledge that terms have been shown to customer.
+ if ( WC_Payments_Features::is_upe_deferred_intent_enabled() && $this->is_mandate_data_required() ) {
$request->set_mandate_data( $this->get_mandate_data() );
}
/** @var WC_Payments_API_Payment_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $intent = $request->send( 'wcpay_create_and_confirm_intent_request', $payment_information );
+ $intent = $request->send();
}
$intent_id = $intent->get_id();
@@ -1308,7 +1322,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
// If the setup intent is included in the request use that intent.
$setup_intent_request = Get_Setup_Intention::create( $woopay_intent_id );
/** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $intent = $setup_intent_request->send( 'wcpay_get_setup_intent_request' );
+ $intent = $setup_intent_request->send();
$intent_metadata = $intent->get_metadata();
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
@@ -1342,6 +1356,8 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
$request->set_customer( $customer_id );
$request->set_payment_method( $payment_information->get_payment_method() );
$request->set_metadata( $metadata );
+ $request->assign_hook( 'wcpay_create_and_confirm_setup_intention_request' );
+ $request->set_hook_args( $payment_information, false, $save_user_in_woopay );
if (
WC_Payments_Features::is_upe_deferred_intent_enabled() &&
@@ -1353,7 +1369,7 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
}
/** @var WC_Payments_API_Setup_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $intent = $request->send( 'wcpay_create_and_confirm_setup_intention_request', $payment_information, false, $save_user_in_woopay );
+ $intent = $request->send();
}
$intent_id = $intent->get_id();
@@ -1483,6 +1499,17 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
];
}
+ /**
+ * The parent method which allows to modify the child class implementation, while supporting the current design where the parent process_payment method is called from the child class.
+ * Mandate must be shown and acknowledged under certain conditions for Stripe Link and SEPA.
+ * Since WC_Payment_Gateway_WCPay represents card payment, which does not require mandate, we return false.
+ *
+ * @return boolean False since card payment does not require mandate.
+ */
+ protected function is_mandate_data_required() {
+ return false;
+ }
+
/**
* Get the payment method chosen by the customer for the payment processing.
* This payment method is needed in case of the deferred intent creation flow only, because this is the only time when the current gateway might process payments other than of the card type.
@@ -1592,34 +1619,19 @@ public function set_payment_method_title_for_order( $order, $payment_method_type
* @return array Array of keyed metadata values.
*/
protected function get_metadata_from_order( $order, $payment_type ) {
+ $service = wcpay_get_container()->get( OrderService::class );
+ $metadata = $service->get_payment_metadata( $order->get_id(), $payment_type );
+
if ( $this instanceof UPE_Split_Payment_Gateway ) {
- $gateway_type = 'split_upe';
+ $gateway_type = 'split_upe_with_deferred_intent_creation';
} elseif ( $this instanceof UPE_Payment_Gateway ) {
- $gateway_type = 'upe';
+ $gateway_type = 'legacy_upe';
} else {
- $gateway_type = 'classic';
- }
- $name = sanitize_text_field( $order->get_billing_first_name() ) . ' ' . sanitize_text_field( $order->get_billing_last_name() );
- $email = sanitize_email( $order->get_billing_email() );
- $metadata = [
- 'customer_name' => $name,
- 'customer_email' => $email,
- 'site_url' => esc_url( get_site_url() ),
- 'order_id' => $order->get_id(),
- 'order_number' => $order->get_order_number(),
- 'order_key' => $order->get_order_key(),
- 'payment_type' => $payment_type,
- 'gateway_type' => $gateway_type,
- 'checkout_type' => $order->get_created_via(),
- 'client_version' => WCPAY_VERSION_NUMBER,
- 'subscription_payment' => 'no',
- ];
-
- if ( 'recurring' === (string) $payment_type && function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order, 'any' ) ) {
- $metadata['subscription_payment'] = wcs_order_contains_renewal( $order ) ? 'renewal' : 'initial';
- $metadata['payment_context'] = WC_Payments_Features::should_use_stripe_billing() ? 'wcpay_subscription' : 'regular_subscription';
+ $gateway_type = 'legacy_card';
}
- return apply_filters( 'wcpay_metadata_from_order', $metadata, $order, $payment_type );
+ $metadata['gateway_type'] = $gateway_type;
+
+ return $metadata;
}
/**
@@ -1648,7 +1660,8 @@ public function attach_exchange_info_to_order( $order, $charge_id ) {
// We check that the currency used in the order is different than the one set in the WC Payments account
// to avoid requesting the charge if not needed.
$request = Get_Charge::create( $charge_id );
- $charge = $request->send( 'wcpay_get_charge_request', $charge_id );
+ $request->set_hook_args( $charge_id );
+ $charge = $request->send();
$exchange_rate = $charge['balance_transaction']['exchange_rate'] ?? null;
if ( isset( $exchange_rate ) ) {
@@ -1747,7 +1760,7 @@ public function process_refund( $order_id, $amount = null, $reason = '' ) {
$list_charge_refund_request = List_Charge_Refunds::create();
$list_charge_refund_request->set_charge( $charge_id );
- $list_charge_refund_response = $list_charge_refund_request->send( 'wcpay_list_charge_refunds_request' );
+ $list_charge_refund_response = $list_charge_refund_request->send();
$refunds = array_filter(
$list_charge_refund_response['data'] ?? [],
@@ -1771,7 +1784,7 @@ static function ( $refund ) use ( $refund_amount ) {
if ( null !== $amount ) {
$refund_request->set_amount( WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) );
}
- $refund = $refund_request->send( 'wcpay_refund_charge_request' );
+ $refund = $refund_request->send();
}
$currency = strtoupper( $refund['currency'] );
Tracker::track_admin( 'wcpay_edit_order_refund_success' );
@@ -1859,8 +1872,11 @@ private function get_payment_method_type_for_order( $order ): string {
$payment_method_details = $this->payments_api_client->get_payment_method( $payment_method_id );
} elseif ( $this->order_service->get_intent_id_for_order( $order ) ) {
$payment_intent_id = $this->order_service->get_intent_id_for_order( $order );
- $request = Get_Intention::create( $payment_intent_id );
- $payment_intent = $request->send( 'wcpay_get_intent_request', $order );
+
+ $request = Get_Intention::create( $payment_intent_id );
+ $request->set_hook_args( $order );
+
+ $payment_intent = $request->send();
$charge = $payment_intent ? $payment_intent->get_charge() : null;
$payment_method_details = $charge ? $charge->get_payment_method_details() : [];
@@ -2019,6 +2035,10 @@ public function update_is_woopay_enabled( $is_woopay_enabled ) {
$this->update_option( 'platform_checkout', $is_woopay_enabled ? 'yes' : 'no' );
+ if ( ! $is_woopay_enabled ) {
+ $this->update_option( 'platform_checkout_last_disable_date', gmdate( 'Y-m-d' ) );
+ }
+
if ( ! $is_woopay_enabled ) {
WooPay_Order_Status_Sync::remove_webhook();
} elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) {
@@ -2053,16 +2073,22 @@ protected function get_capture_type() {
* Map fields that need to be updated and update the fields server side.
*
* @param array $settings Plugin settings.
- * @return array Updated fields.
+ *
+ * @return array|WP_Error Updated fields.
*/
- public function update_account_settings( array $settings ) : array {
+ public function update_account_settings( array $settings ) {
$account_settings = [];
foreach ( static::ACCOUNT_SETTINGS_MAPPING as $name => $account_key ) {
if ( isset( $settings[ $name ] ) ) {
$account_settings[ $account_key ] = $settings[ $name ];
}
}
- $this->update_account( $account_settings );
+
+ $result = $this->update_account( $account_settings );
+
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
return $account_settings;
}
@@ -2573,18 +2599,24 @@ public function get_icon_url() {
* Supported: statement_descriptor, business_name, business_url, business_support_address,
* business_support_email, business_support_phone, branding_logo, branding_icon,
* branding_primary_color, branding_secondary_color.
+ *
+ * $return array | WP_Error Update account result.
+ *
+ * @throws Exception
*/
public function update_account( $account_settings ) {
if ( empty( $account_settings ) ) {
return;
}
- $error_message = $this->account->update_stripe_account( $account_settings );
+ $stripe_account_update_response = $this->account->update_stripe_account( $account_settings );
- if ( is_string( $error_message ) ) {
- $msg = __( 'Failed to update Stripe account. ', 'woocommerce-payments' ) . $error_message;
+ if ( is_wp_error( $stripe_account_update_response ) ) {
+ $msg = __( 'Failed to update Stripe account. ', 'woocommerce-payments' ) . $stripe_account_update_response->get_error_message();
$this->add_error( $msg );
}
+
+ return $stripe_account_update_response;
}
/**
@@ -2654,10 +2686,11 @@ public function add_order_actions( $actions ) {
*
* @param WC_Order $order - Order to capture charge on.
* @param bool $include_level3 - Whether to include level 3 data in payment intent.
+ * @param array $intent_metadata - Intent metadata retrieved earlier in the calling method.
*
* @return array An array containing the status (succeeded/failed), id (intent ID), message (error message if any), and http code
*/
- public function capture_charge( $order, $include_level3 = true ) {
+ public function capture_charge( $order, $include_level3 = true, $intent_metadata = [] ) {
$amount = $order->get_total();
$is_authorization_expired = false;
$intent = null;
@@ -2666,27 +2699,19 @@ public function capture_charge( $order, $include_level3 = true ) {
$http_code = null;
try {
- $intent_id = $order->get_transaction_id();
-
- $request = Get_Intention::create( $intent_id );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
-
- $payment_type = $this->is_payment_recurring( $order->get_id() ) ? Payment_Type::RECURRING() : Payment_Type::SINGLE();
-
- $metadata_from_intent = $intent->get_metadata(); // mobile app may have set metadata.
- $metadata_from_order = $this->get_metadata_from_order( $order, $payment_type );
- $merged_metadata = array_merge( (array) $metadata_from_order, (array) $metadata_from_intent ); // prioritize metadata from mobile app.
-
- $wcpay_request = Update_Intention::create( $intent_id );
- $wcpay_request->set_metadata( $merged_metadata );
- $wcpay_request->send( 'wcpay_prepare_intention_for_capture', $order );
+ $intent_id = $order->get_transaction_id();
+ $payment_type = $this->is_payment_recurring( $order->get_id() ) ? Payment_Type::RECURRING() : Payment_Type::SINGLE();
+ $metadata_from_order = $this->get_metadata_from_order( $order, $payment_type );
+ $merged_metadata = array_merge( (array) $metadata_from_order, (array) $intent_metadata ); // prioritize metadata from mobile app.
$capture_intention_request = Capture_Intention::create( $intent_id );
$capture_intention_request->set_amount_to_capture( WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) );
+ $capture_intention_request->set_metadata( $merged_metadata );
+ $capture_intention_request->set_hook_args( $order );
if ( $include_level3 ) {
$capture_intention_request->set_level3( $this->get_level3_data_from_order( $order ) );
}
- $intent = $capture_intention_request->send( 'wcpay_capture_intent_request', $order );
+ $intent = $capture_intention_request->send();
$status = $intent->get_status();
$http_code = 200;
@@ -2695,8 +2720,10 @@ public function capture_charge( $order, $include_level3 = true ) {
$error_message = $e->getMessage();
$http_code = $e->get_http_code();
+ $request = Get_Intention::create( $intent_id );
+ $request->set_hook_args( $order );
// Fetch the Intent to check if it's already expired and the site missed the "charge.expired" webhook.
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $intent = $request->send();
if ( Intent_Status::CANCELED === $intent->get_status() ) {
$is_authorization_expired = true;
@@ -2751,15 +2778,17 @@ public function cancel_authorization( $order ) {
$http_code = null;
try {
- $request = Cancel_Intention::create( $order->get_transaction_id() );
- $intent = $request->send( 'wcpay_cancel_intent_request', $order );
+ $request = Cancel_Intention::create( $order->get_transaction_id() );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$status = $intent->get_status();
$http_code = 200;
} catch ( API_Exception $e ) {
try {
// Fetch the Intent to check if it's already expired and the site missed the "charge.expired" webhook.
$request = Get_Intention::create( $order->get_transaction_id() );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$status = $intent->get_status();
if ( Intent_Status::CANCELED !== $status ) {
@@ -2850,81 +2879,7 @@ public function get_shipping_data_from_order( WC_Order $order ): array {
* @return array The level 3 data to send to Stripe.
*/
public function get_level3_data_from_order( WC_Order $order ): array {
- $merchant_country = $this->account->get_account_country();
- // We do not need to send level3 data if merchant account country is non-US.
- if ( 'US' !== $merchant_country ) {
- return [];
- }
-
- // Get the order items. Don't need their keys, only their values.
- // Order item IDs are used as keys in the original order items array.
- $order_items = array_values( $order->get_items( [ 'line_item', 'fee' ] ) );
- $currency = $order->get_currency();
-
- $process_item = static function( $item ) use ( $currency ) {
- // Check to see if it is a WC_Order_Item_Product or a WC_Order_Item_Fee.
- if ( is_a( $item, 'WC_Order_Item_Product' ) ) {
- $subtotal = $item->get_subtotal();
- $product_id = $item->get_variation_id()
- ? $item->get_variation_id()
- : $item->get_product_id();
- $product_code = substr( $product_id, 0, 12 );
- } else {
- $subtotal = $item->get_total();
- $product_code = substr( sanitize_title( $item->get_name() ), 0, 12 );
- }
-
- $description = substr( $item->get_name(), 0, 26 );
- $quantity = ceil( $item->get_quantity() );
- $tax_amount = WC_Payments_Utils::prepare_amount( $item->get_total_tax(), $currency );
- if ( $subtotal >= 0 ) {
- $unit_cost = WC_Payments_Utils::prepare_amount( $subtotal / $quantity, $currency );
- $discount_amount = WC_Payments_Utils::prepare_amount( $subtotal - $item->get_total(), $currency );
- } else {
- // It's possible to create products with negative price - represent it as free one with discount.
- $discount_amount = abs( WC_Payments_Utils::prepare_amount( $subtotal / $quantity, $currency ) );
- $unit_cost = 0;
- }
-
- return (object) [
- 'product_code' => (string) $product_code, // Up to 12 characters that uniquely identify the product.
- 'product_description' => $description, // Up to 26 characters long describing the product.
- 'unit_cost' => $unit_cost, // Cost of the product, in cents, as a non-negative integer.
- 'quantity' => $quantity, // The number of items of this type sold, as a non-negative integer.
- 'tax_amount' => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer.
- 'discount_amount' => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer.
- ];
- };
- $items_to_send = array_map( $process_item, $order_items );
-
- if ( count( $items_to_send ) > 200 ) {
- // If more than 200 items are present, bundle the last ones in a single item.
- $items_to_send = array_merge(
- array_slice( $items_to_send, 0, 199 ),
- [ $this->bundle_level3_data_from_items( array_slice( $items_to_send, 200 ) ) ]
- );
- }
-
- $level3_data = [
- 'merchant_reference' => (string) $order->get_id(), // An alphanumeric string of up to characters in length. This unique value is assigned by the merchant to identify the order. Also known as an “Order ID”.
- 'customer_reference' => (string) $order->get_id(),
- 'shipping_amount' => WC_Payments_Utils::prepare_amount( (float) $order->get_shipping_total() + (float) $order->get_shipping_tax(), $currency ), // The shipping cost, in cents, as a non-negative integer.
- 'line_items' => $items_to_send,
- ];
-
- // The customer’s U.S. shipping ZIP code.
- $shipping_address_zip = $order->get_shipping_postcode();
- if ( WC_Payments_Utils::is_valid_us_zip_code( $shipping_address_zip ) ) {
- $level3_data['shipping_address_zip'] = $shipping_address_zip;
- }
-
- // The merchant’s U.S. shipping ZIP code.
- $store_postcode = get_option( 'woocommerce_store_postcode' );
- if ( WC_Payments_Utils::is_valid_us_zip_code( $store_postcode ) ) {
- $level3_data['shipping_from_zip'] = $store_postcode;
- }
-
- return $level3_data;
+ return wcpay_get_container()->get( Level3Service::class )->get_data_from_order( $order->get_id() );
}
/**
@@ -2993,7 +2948,8 @@ public function update_order_status() {
if ( $amount > 0 ) {
// An exception is thrown if an intent can't be found for the given intent ID.
$request = Get_Intention::create( $intent_id );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$status = $intent->get_status();
$charge = $intent->get_charge();
@@ -3006,7 +2962,7 @@ public function update_order_status() {
// For $0 orders, fetch the Setup Intent instead.
$setup_intent_request = Get_Setup_Intention::create( $intent_id );
/** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $intent = $setup_intent_request->send( 'wcpay_get_setup_intent_request' );
+ $intent = $setup_intent_request->send();
$status = $intent->get_status();
$charge_id = '';
}
@@ -3114,7 +3070,7 @@ public function add_payment_method() {
$setup_intent_request = Get_Setup_Intention::create( $setup_intent_id );
/** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $setup_intent = $setup_intent_request->send( 'wcpay_get_setup_intent_request' );
+ $setup_intent = $setup_intent_request->send();
if ( Intent_Status::SUCCEEDED !== $setup_intent->get_status() ) {
throw new Add_Payment_Method_Exception(
@@ -3214,7 +3170,8 @@ public function create_intent( WC_Order $order, array $payment_methods, string $
$request->set_metadata( $metadata );
$request->set_payment_method_types( $payment_methods );
$request->set_capture_method( $capture_method );
- $intent = $request->send( 'wcpay_create_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
return [
'id' => ! empty( $intent ) ? $intent->get_id() : null,
@@ -3262,7 +3219,9 @@ public function create_and_confirm_setup_intent() {
$request = Create_And_Confirm_Setup_Intention::create();
$request->set_customer( $customer_id );
$request->set_payment_method( $payment_information->get_payment_method() );
- return $request->send( 'wcpay_create_and_confirm_setup_intention_request', $payment_information, $should_save_in_platform_account, false );
+ $request->assign_hook( 'wcpay_create_and_confirm_setup_intention_request' );
+ $request->set_hook_args( $payment_information, $should_save_in_platform_account, false );
+ return $request->send();
}
/**
@@ -3521,36 +3480,6 @@ protected function should_bump_rate_limiter( string $error_code ): bool {
return in_array( $error_code, [ 'card_declined', 'incorrect_number', 'incorrect_cvc' ], true );
}
- /**
- * Returns a bundle of products passed as an argument. Useful when working with Stripe's level 3 data
- *
- * @param array $items The Stripe's level 3 array of items.
- *
- * @return object A bundle of the products passed.
- */
- public function bundle_level3_data_from_items( array $items ) {
- // Total cost is the sum of each product cost * quantity.
- $items_count = count( $items );
- $total_cost = array_sum(
- array_map(
- function( $cost, $qty ) {
- return $cost * $qty;
- },
- array_column( $items, 'unit_cost' ),
- array_column( $items, 'quantity' )
- )
- );
-
- return (object) [
- 'product_code' => (string) substr( uniqid(), 0, 26 ),
- 'product_description' => "{$items_count} more items",
- 'unit_cost' => $total_cost,
- 'quantity' => 1,
- 'tax_amount' => array_sum( array_column( $items, 'tax_amount' ) ),
- 'discount_amount' => array_sum( array_column( $items, 'discount_amount' ) ),
- ];
- }
-
/**
* Move the email field to the top of the Checkout page.
*
diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php
index 10680f70f5d..420ff5750e7 100644
--- a/includes/class-wc-payments-account.php
+++ b/includes/class-wc-payments-account.php
@@ -58,34 +58,56 @@ class WC_Payments_Account {
private $action_scheduler_service;
/**
- * Class constructor
+ * WC_Payments_Session_Service instance for working with session information
*
- * @param WC_Payments_API_Client $payments_api_client Payments API client.
- * @param Database_Cache $database_cache Database cache util.
- * @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Action scheduler service.
+ * @var WC_Payments_Session_Service
*/
- public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache, WC_Payments_Action_Scheduler_Service $action_scheduler_service ) {
+ private $session_service;
+
+ /**
+ * Class constructor
+ *
+ * @param WC_Payments_API_Client $payments_api_client Payments API client.
+ * @param Database_Cache $database_cache Database cache util.
+ * @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Action scheduler service.
+ * @param WC_Payments_Session_Service $session_service Session service.
+ */
+ public function __construct(
+ WC_Payments_API_Client $payments_api_client,
+ Database_Cache $database_cache,
+ WC_Payments_Action_Scheduler_Service $action_scheduler_service,
+ WC_Payments_Session_Service $session_service
+ ) {
$this->payments_api_client = $payments_api_client;
$this->database_cache = $database_cache;
$this->action_scheduler_service = $action_scheduler_service;
+ $this->session_service = $session_service;
+ }
+ /**
+ * Initialise class hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
+ // Add admin init hooks.
add_action( 'admin_init', [ $this, 'maybe_handle_onboarding' ] );
add_action( 'admin_init', [ $this, 'maybe_redirect_to_onboarding' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic.
add_action( 'admin_init', [ $this, 'maybe_redirect_to_wcpay_connect' ], 12 ); // Run this after the redirect to onboarding logic.
+ add_action( 'admin_init', [ $this, 'maybe_redirect_to_capital_offer' ] );
+ add_action( 'admin_init', [ $this, 'maybe_redirect_to_server_link' ] );
+ add_action( 'admin_init', [ $this, 'maybe_activate_woopay' ] );
+
+ // Add handlers for inbox notes and reminders.
add_action( 'woocommerce_payments_account_refreshed', [ $this, 'handle_instant_deposits_inbox_note' ] );
add_action( 'woocommerce_payments_account_refreshed', [ $this, 'handle_loan_approved_inbox_note' ] );
add_action( self::INSTANT_DEPOSITS_REMINDER_ACTION, [ $this, 'handle_instant_deposits_inbox_reminder' ] );
+
+ // Add all other hooks.
add_filter( 'allowed_redirect_hosts', [ $this, 'allowed_redirect_hosts' ] );
add_action( 'jetpack_site_registered', [ $this, 'clear_cache' ] );
add_action( 'updated_option', [ $this, 'possibly_update_wcpay_account_locale' ], 10, 3 );
add_action( 'woocommerce_woocommerce_payments_updated', [ $this, 'clear_cache' ] );
-
- // Add capital offer redirection.
- add_action( 'admin_init', [ $this, 'maybe_redirect_to_capital_offer' ] );
-
- // Add server links handler.
- add_action( 'admin_init', [ $this, 'maybe_redirect_to_server_link' ] );
- add_action( 'admin_init', [ $this, 'maybe_activate_woopay' ] );
}
/**
@@ -506,8 +528,8 @@ public function get_progressive_onboarding_details(): array {
*/
public function is_progressive_onboarding_in_progress(): bool {
$account = $this->get_cached_account_data();
- return $account['progressive_onboarding']['is_enabled'] ?? false
- && ! $account['progressive_onboarding']['is_complete'] ?? false;
+ return ( $account['progressive_onboarding']['is_enabled'] ?? false )
+ && ! ( $account['progressive_onboarding']['is_complete'] ?? false );
}
/**
@@ -606,7 +628,7 @@ public function maybe_redirect_to_capital_offer() {
$request->set_return_url( $return_url );
$request->set_refresh_url( $refresh_url );
- $capital_link = $request->send( 'wcpay_get_account_capital_link' );
+ $capital_link = $request->send();
$this->redirect_to( $capital_link['url'] );
} catch ( Exception $e ) {
$error_url = add_query_arg(
@@ -1118,7 +1140,7 @@ private function redirect_to_login() {
$request = Get_Account_Login_Data::create();
$request->set_redirect_url( $redirect_url );
- $response = $request->send( 'wpcay_get_account_login_data' );
+ $response = $request->send();
$login_data = $response->to_array();
wp_safe_redirect( $login_data['url'] );
exit;
@@ -1185,8 +1207,7 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args =
// Clear persisted onboarding flow state.
WC_Payments_Onboarding_Service::clear_onboarding_flow_state();
- $current_user = wp_get_current_user();
- $return_url = $this->get_onboarding_return_url( $wcpay_connect_from );
+ $return_url = $this->get_onboarding_return_url( $wcpay_connect_from );
if ( ! empty( $additional_args ) ) {
$return_url = add_query_arg( $additional_args, $return_url );
}
@@ -1259,14 +1280,19 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args =
$account_data = [];
}
+ $site_data = [
+ 'site_username' => wp_get_current_user()->user_login,
+ 'site_locale' => get_locale(),
+ ];
+
+ $user_data = $this->get_onboarding_user_data();
+
$onboarding_data = $this->payments_api_client->get_onboarding_data(
$return_url,
- [
- 'site_username' => $current_user->user_login,
- 'site_locale' => get_locale(),
- ],
- $this->get_actioned_notes(),
+ $site_data,
+ array_filter( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped.
array_filter( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped.
+ $this->get_actioned_notes(),
$progressive,
$collect_payout_requirements
);
@@ -1386,7 +1412,7 @@ function () {
delete_transient( self::ON_BOARDING_DISABLED_TRANSIENT );
$request = Get_Account::create();
- $response = $request->send( 'wcpay_get_account' );
+ $response = $request->send();
$account = $response->to_array();
} catch ( API_Exception $e ) {
@@ -1504,7 +1530,9 @@ public function is_valid_cached_account( $account ) {
*
* @param array $stripe_account_settings Settings to update.
*
- * @return null|string Error message if update failed.
+ * @return null|WP_Error Account update result.
+ *
+ * @throws Exception
*/
public function update_stripe_account( $stripe_account_settings ) {
try {
@@ -1514,13 +1542,14 @@ public function update_stripe_account( $stripe_account_settings ) {
}
$request = Update_Account::from_account_settings( $stripe_account_settings );
- $response = $request->send( 'wcpay_update_account_settings' );
+ $response = $request->send();
$updated_account = $response->to_array();
$this->database_cache->add( Database_Cache::ACCOUNT_KEY, $updated_account );
} catch ( Exception $e ) {
Logger::error( 'Failed to update Stripe account ' . $e );
- return $e->getMessage();
+
+ return new WP_Error( 'wcpay_failed_to_update_stripe_account', $e->getMessage() );
}
}
@@ -1558,7 +1587,7 @@ public function possibly_update_wcpay_account_locale( $option_name, $old_value,
];
$request = Update_Account::from_account_settings( $account_settings );
- $response = $request->send( 'wcpay_update_account_settings' );
+ $response = $request->send();
$updated_account = $response->to_array();
$this->database_cache->add( Database_Cache::ACCOUNT_KEY, $updated_account );
@@ -1698,8 +1727,9 @@ public function handle_loan_approved_inbox_note( $account ) {
// Get the loan summary.
try {
- $request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/active_loan_summary' );
- $loan_details = $request->send( 'wcpay_get_active_loan_summary_request' );
+ $request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/active_loan_summary' );
+ $request->assign_hook( 'wcpay_get_active_loan_summary_request' );
+ $loan_details = $request->send();
} catch ( API_Exception $ex ) {
return;
@@ -1801,4 +1831,23 @@ private function tracks_event( string $name, array $properties = [] ) {
Logger::info( 'Tracks event: ' . $name . ' with data: ' . wp_json_encode( WC_Payments_Utils::redact_array( $properties, [ 'woo_country_code' ] ) ) );
}
+
+ /**
+ * Get user data to send to the onboarding flow.
+ *
+ * @return array The user data.
+ */
+ private function get_onboarding_user_data(): array {
+ return [
+ 'user_id' => get_current_user_id(),
+ 'sift_session_id' => $this->session_service->get_sift_session_id(),
+ 'ip_address' => \WC_Geolocation::get_ip_address(),
+ 'browser' => [
+ 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
+ 'accept_language' => isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '',
+ 'content_language' => empty( get_user_locale() ) ? 'en-US' : str_replace( '_', '-', get_user_locale() ),
+ ],
+ 'referer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
+ ];
+ }
}
diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php
index d6f098116b4..2b158f92707 100644
--- a/includes/class-wc-payments-checkout.php
+++ b/includes/class-wc-payments-checkout.php
@@ -188,6 +188,7 @@ public function get_payment_fields_js_config() {
'isSavedCardsEnabled' => $this->gateway->is_saved_cards_enabled(),
'isWooPayEnabled' => $this->woopay_util->should_enable_woopay( $this->gateway ) && $this->woopay_util->should_enable_woopay_on_cart_or_checkout(),
'isWoopayExpressCheckoutEnabled' => $this->woopay_util->is_woopay_express_checkout_enabled(),
+ 'isWoopayFirstPartyAuthEnabled' => $this->woopay_util->is_woopay_first_party_auth_enabled(),
'isClientEncryptionEnabled' => WC_Payments_Features::is_client_secret_encryption_enabled(),
'woopayHost' => WooPay_Utilities::get_woopay_url(),
'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ),
diff --git a/includes/class-wc-payments-customer-service.php b/includes/class-wc-payments-customer-service.php
index 26c89f13003..72c2b7ff37b 100644
--- a/includes/class-wc-payments-customer-service.php
+++ b/includes/class-wc-payments-customer-service.php
@@ -147,6 +147,29 @@ public function create_customer_for_user( WP_User $user, array $customer_data ):
return $customer_id;
}
+ /**
+ * Manages customer details held on WCPay server for WordPress user associated with an order.
+ *
+ * @param int $user_id ID of the WP user to associate with the customer.
+ * @param WC_Order $order Woo Order.
+ * @return string WooPayments customer ID.
+ */
+ public function get_or_create_customer_id_from_order( int $user_id, WC_Order $order ): string {
+ // Determine the customer making the payment, create one if we don't have one already.
+ $customer_id = $this->get_customer_id_by_user_id( $user_id );
+
+ if ( null !== $customer_id ) {
+ // @todo: We need to update the customer here.
+ return $customer_id;
+ }
+
+ $customer_data = self::map_customer_data( $order, new WC_Customer( $user_id ) );
+ $user = get_user_by( 'id', $user_id );
+ $customer_id = $this->create_customer_for_user( $user, $customer_data );
+
+ return $customer_id;
+ }
+
/**
* Update the customer details held on the WCPay server associated with the given WordPress user.
*
diff --git a/includes/class-wc-payments-dependency-service.php b/includes/class-wc-payments-dependency-service.php
index 0eb66a09faa..c952c20113b 100644
--- a/includes/class-wc-payments-dependency-service.php
+++ b/includes/class-wc-payments-dependency-service.php
@@ -25,10 +25,11 @@ class WC_Payments_Dependency_Service {
const DEV_ASSETS_NOT_BUILT = 'dev_assets_not_built';
/**
- * Constructor.
+ * Initializes this class's WP hooks.
+ *
+ * @return void
*/
- public function __construct() {
-
+ public function init_hooks() {
add_filter( 'admin_notices', [ $this, 'display_admin_notices' ] );
}
diff --git a/includes/class-wc-payments-express-checkout-button-display-handler.php b/includes/class-wc-payments-express-checkout-button-display-handler.php
index b5cc5b1b55b..cc48bf282b7 100644
--- a/includes/class-wc-payments-express-checkout-button-display-handler.php
+++ b/includes/class-wc-payments-express-checkout-button-display-handler.php
@@ -59,8 +59,8 @@ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Paym
add_action( 'woocommerce_checkout_before_customer_details', [ $this, 'display_express_checkout_buttons' ], 1 );
}
- if ( class_exists( '\Automattic\WooCommerce\Blocks\Package' ) && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '10.8.0', '>=' ) ) {
- add_action( 'before_woocommerce_pay_form', [ $this, 'add_pay_for_order_params_to_js_config' ] );
+ if ( WC_Payments_Features::is_pay_for_order_flow_enabled() && class_exists( '\Automattic\WooCommerce\Blocks\Package' ) && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '11.1.0', '>=' ) ) {
+ add_action( 'wp_enqueue_scripts', [ $this, 'add_pay_for_order_params_to_js_config' ], 5 );
add_action( 'woocommerce_pay_order_before_payment', [ $this, 'display_express_checkout_buttons' ], 1 );
}
}
@@ -114,12 +114,13 @@ public function is_woopay_enabled() {
/**
* Add the Pay for order params to the JS config.
- *
- * @param WC_Order $order The pay-for-order order.
*/
- public function add_pay_for_order_params_to_js_config( $order ) {
+ public function add_pay_for_order_params_to_js_config() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) ) {
+ global $wp;
+ $order_id = $wp->query_vars['order-pay'];
+ $order = wc_get_order( $order_id );
add_filter(
'wcpay_payment_fields_js_config',
function( $js_config ) use ( $order ) {
diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php
index 08485ac5e26..cc4fd8ee8e3 100644
--- a/includes/class-wc-payments-features.php
+++ b/includes/class-wc-payments-features.php
@@ -19,9 +19,12 @@ class WC_Payments_Features {
const WCPAY_SUBSCRIPTIONS_FLAG_NAME = '_wcpay_feature_subscriptions';
const STRIPE_BILLING_FLAG_NAME = '_wcpay_feature_stripe_billing';
const WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME = '_wcpay_feature_woopay_express_checkout';
+ const WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME = '_wcpay_feature_woopay_first_party_auth';
const AUTH_AND_CAPTURE_FLAG_NAME = '_wcpay_feature_auth_and_capture';
const PROGRESSIVE_ONBOARDING_FLAG_NAME = '_wcpay_feature_progressive_onboarding';
- const DISPUTE_ON_TRANSACTION_PAGE = '_wcpay_feature_dispute_on_transaction_page';
+ const PAY_FOR_ORDER_FLOW = '_wcpay_feature_pay_for_order_flow';
+ const DEFERRED_UPE_SERVER_FLAG_NAME = 'is_deferred_intent_creation_upe_enabled';
+ const DISPUTE_ISSUER_EVIDENCE = '_wcpay_feature_dispute_issuer_evidence';
/**
* Checks whether any UPE gateway is enabled.
@@ -38,7 +41,26 @@ public static function is_upe_enabled() {
* @return string
*/
public static function get_enabled_upe_type() {
- if ( self::is_upe_split_enabled() || self::is_upe_deferred_intent_enabled() ) {
+ // New stores created in 6.4.0 or 6.5.0, and legacy card stores that self-migrated to dUPE in 6.4.0 or 6.5.0.
+ $has_store_enabled_dupe_from_previous_plugin_version = self::has_store_enabled_dupe_from_previous_plugin_version();
+ $has_store_enabled_upe_and_dupe_server_flag = self::has_store_enabled_upe_and_dupe_server_flag();
+
+ // Stores which first self-migrated and then had deferred intent creation UPE enabled by default.
+ if ( $has_store_enabled_dupe_from_previous_plugin_version && $has_store_enabled_upe_and_dupe_server_flag ) {
+ return 'deferred_intent_upe_without_fallback';
+ }
+
+ // Stores which have deferred intent creation UPE enabled by default.
+ if ( $has_store_enabled_upe_and_dupe_server_flag ) {
+ return 'deferred_intent_upe_without_fallback';
+ }
+
+ // Stores which self-migrated to dUPE (e.g. legacy card stores from 6.4.0 or 6.5.0).
+ if ( $has_store_enabled_dupe_from_previous_plugin_version ) {
+ return 'deferred_intent_upe_with_fallback';
+ }
+
+ if ( self::is_upe_split_enabled() ) {
return 'split';
}
@@ -55,31 +77,55 @@ public static function get_enabled_upe_type() {
* @return bool
*/
public static function is_upe_legacy_enabled() {
- $upe_flag_value = '1' === get_option( self::UPE_FLAG_NAME, '0' );
- if ( $upe_flag_value ) {
- return true;
- }
-
- $upe_split_flag_value = '1' === get_option( self::UPE_SPLIT_FLAG_NAME, '0' );
- $upe_deferred_flag_value = '1' === get_option( self::UPE_DEFERRED_INTENT_FLAG_NAME, '0' );
-
- // if the merchant is not eligible for the Split UPE, but they have the flag enabled, fallback to the "legacy" UPE (for now).
- return ( $upe_split_flag_value || $upe_deferred_flag_value )
- && ! self::is_upe_split_eligible();
+ return '1' === get_option( self::UPE_FLAG_NAME, '0' );
}
/**
* Checks whether the Split-UPE gateway is enabled
*/
public static function is_upe_split_enabled() {
- return '1' === get_option( self::UPE_SPLIT_FLAG_NAME, '0' ) && self::is_upe_split_eligible();
+ return '1' === get_option( self::UPE_SPLIT_FLAG_NAME, '0' );
}
/**
- * Checks whether the Split UPE with deferred intent is enabled
+ * Checks whether the Split UPE with deferred intent creation is enabled
*/
public static function is_upe_deferred_intent_enabled() {
- return ( '1' === get_option( self::UPE_DEFERRED_INTENT_FLAG_NAME, '0' ) && self::is_upe_split_eligible() ) || self::is_upe_split_enabled();
+ // Support new stores created in 6.4.0 or 6.5.0, and legacy card stores that migrated to deferred UPE in 6.4.0 or 6.5.0.
+ $has_store_enabled_dupe_from_previous_plugin_version = self::has_store_enabled_dupe_from_previous_plugin_version();
+ $has_store_enabled_upe_and_dupe_server_flag = self::has_store_enabled_upe_and_dupe_server_flag();
+
+ return $has_store_enabled_dupe_from_previous_plugin_version || $has_store_enabled_upe_and_dupe_server_flag;
+ }
+
+ /**
+ * Checks if the store has deferred intent creation UPE enabled from a previous version.
+ * This is applicable to:
+ * * legacy card stores that self-migrated to deferred UPE in 6.4.0 or 6.5.0
+ * * new stores starting 6.4.0
+ */
+ private static function has_store_enabled_dupe_from_previous_plugin_version() {
+ return '1' === get_option( self::UPE_DEFERRED_INTENT_FLAG_NAME, '0' );
+ }
+
+ /**
+ * Checks if the store has UPE enabled and the server-side feature flag is enabled.
+ * This is applicable to:
+ * * Split UPE stores starting 6.4.0
+ * * Legacy UPE stores starting 6.6.0
+ */
+ private static function has_store_enabled_upe_and_dupe_server_flag() {
+ return ( self::is_upe_split_enabled() || self::is_upe_legacy_enabled() ) && self::is_deferred_upe_server_flag_enabled();
+ }
+
+
+ /**
+ * Checks if the Deferred UPE server-side feature flag is enabled.
+ * The flag should be always returned, and if it's not present, server assumes it's enabled.
+ */
+ private static function is_deferred_upe_server_flag_enabled() {
+ $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true );
+ return is_array( $account ) && ( $account[ self::DEFERRED_UPE_SERVER_FLAG_NAME ] ?? false );
}
/**
@@ -188,26 +234,72 @@ public static function is_wcpay_subscriptions_enabled() {
}
/**
- * Returns whether WCPay Subscriptions is eligible, based on the stores base country.
+ * Returns whether the store is eligible to use WCPay Subscriptions (the free subscriptions bundled in WooPayments)
+ *
+ * Stores are eligible for the WCPay Subscriptions feature if:
+ * 1. The store has existing WCPay Subscriptions, or
+ * 2. The store has Stripe Billing product metadata on at least 1 product subscription product.
*
* @return bool
*/
public static function is_wcpay_subscriptions_eligible() {
- if ( ! function_exists( 'wc_get_base_location' ) ) {
- return false;
+ /**
+ * Check if they have at least 1 WCPay Subscription.
+ *
+ * Note: this is only possible if WCPay Subscriptions is enabled, otherwise the wcs_get_subscriptions function wouldn't exist.
+ */
+ if ( function_exists( 'wcs_get_subscriptions' ) ) {
+ $wcpay_subscriptions = wcs_get_subscriptions(
+ [
+ 'subscriptions_per_page' => 1,
+ 'subscription_status' => 'any',
+ 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ [
+ 'key' => '_wcpay_subscription_id',
+ 'compare' => 'EXISTS',
+ ],
+ ],
+ ]
+ );
+
+ if ( count( $wcpay_subscriptions ) > 0 ) {
+ return true;
+ }
}
- $store_base_location = wc_get_base_location();
- return ! empty( $store_base_location['country'] ) && 'US' === $store_base_location['country'];
- }
+ /**
+ * Check if they have at least 1 Stripe Billing enabled product.
+ */
+ $stripe_billing_meta_query_handler = function ( $query, $query_vars ) {
+ if ( ! empty( $query_vars['stripe_billing_product'] ) ) {
+ $query['meta_query'][] = [
+ 'key' => '_wcpay_product_hash',
+ 'compare' => 'EXISTS',
+ ];
+ }
- /**
- * Checks whether Deposits details UI on Transaction Details page is enabled. Disabled by default.
- *
- * @return bool
- */
- public static function is_dispute_on_transaction_page_enabled(): bool {
- return '1' === get_option( self::DISPUTE_ON_TRANSACTION_PAGE, '0' );
+ return $query;
+ };
+
+ add_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 );
+
+ $subscription_products = wc_get_products(
+ [
+ 'limit' => 1,
+ 'type' => [ 'subscription', 'variable-subscription' ],
+ 'status' => 'publish',
+ 'return' => 'ids',
+ 'stripe_billing_product' => 'true',
+ ]
+ );
+
+ remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 );
+
+ if ( count( $subscription_products ) > 0 ) {
+ return true;
+ }
+
+ return false;
}
/**
@@ -279,6 +371,15 @@ public static function is_woopay_express_checkout_enabled() {
return '1' === get_option( self::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ) && self::is_woopay_eligible();
}
+ /**
+ * Checks whether WooPay First Party Auth is enabled.
+ *
+ * @return bool
+ */
+ public static function is_woopay_first_party_auth_enabled() {
+ return '1' === get_option( self::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' ) && self::is_woopay_express_checkout_enabled();
+ }
+
/**
* Checks whether Auth & Capture (uncaptured transactions tab, capture from payment details page) is enabled.
*
@@ -368,6 +469,24 @@ public static function should_use_stripe_billing() {
return false;
}
+ /**
+ * Checks whether the pay for order flow is enabled
+ *
+ * @return bool
+ */
+ public static function is_pay_for_order_flow_enabled() {
+ return '1' === get_option( self::PAY_FOR_ORDER_FLOW, '0' );
+ }
+
+ /**
+ * Checks whether Dispute issuer evidence feature should be enabled. Disabled by default.
+ *
+ * @return bool
+ */
+ public static function is_dispute_issuer_evidence_enabled(): bool {
+ return '1' === get_option( self::DISPUTE_ISSUER_EVIDENCE, '0' );
+ }
+
/**
* Returns feature flags as an array suitable for display on the front-end.
*
@@ -376,18 +495,19 @@ public static function should_use_stripe_billing() {
public static function to_array() {
return array_filter(
[
- 'upe' => self::is_upe_enabled(),
- 'upeSplit' => self::is_upe_split_enabled(),
- 'upeDeferred' => self::is_upe_deferred_intent_enabled(),
- 'upeSettingsPreview' => self::is_upe_settings_preview_enabled(),
- 'multiCurrency' => self::is_customer_multi_currency_enabled(),
- 'woopay' => self::is_woopay_eligible(),
- 'documents' => self::is_documents_section_enabled(),
- 'clientSecretEncryption' => self::is_client_secret_encryption_enabled(),
- 'woopayExpressCheckout' => self::is_woopay_express_checkout_enabled(),
- 'isAuthAndCaptureEnabled' => self::is_auth_and_capture_enabled(),
- 'progressiveOnboarding' => self::is_progressive_onboarding_enabled(),
- 'isDisputeOnTransactionPageEnabled' => self::is_dispute_on_transaction_page_enabled(),
+ 'upe' => self::is_upe_enabled(),
+ 'upeSplit' => self::is_upe_split_enabled(),
+ 'upeDeferred' => self::is_upe_deferred_intent_enabled(),
+ 'upeSettingsPreview' => self::is_upe_settings_preview_enabled(),
+ 'multiCurrency' => self::is_customer_multi_currency_enabled(),
+ 'woopay' => self::is_woopay_eligible(),
+ 'documents' => self::is_documents_section_enabled(),
+ 'clientSecretEncryption' => self::is_client_secret_encryption_enabled(),
+ 'woopayExpressCheckout' => self::is_woopay_express_checkout_enabled(),
+ 'isAuthAndCaptureEnabled' => self::is_auth_and_capture_enabled(),
+ 'progressiveOnboarding' => self::is_progressive_onboarding_enabled(),
+ 'isPayForOrderFlowEnabled' => self::is_pay_for_order_flow_enabled(),
+ 'isDisputeIssuerEvidenceEnabled' => self::is_dispute_issuer_evidence_enabled(),
]
);
}
diff --git a/includes/class-wc-payments-fraud-service.php b/includes/class-wc-payments-fraud-service.php
index cc97581494d..a4da74a1ba1 100644
--- a/includes/class-wc-payments-fraud-service.php
+++ b/includes/class-wc-payments-fraud-service.php
@@ -35,22 +35,39 @@ class WC_Payments_Fraud_Service {
*/
private $customer_service;
+ /**
+ * WC_Payments_Session_Service instance for working with session information
+ *
+ * @var WC_Payments_Session_Service
+ */
+ private $session_service;
+
/**
* Constructor for WC_Payments_Fraud_Service.
*
* @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client.
* @param WC_Payments_Customer_Service $customer_service - Customer class instance.
* @param WC_Payments_Account $account - Account class instance.
+ * @param WC_Payments_Session_Service $session_service - Session Service class instance.
*/
public function __construct(
WC_Payments_API_Client $payments_api_client,
WC_Payments_Customer_Service $customer_service,
- WC_Payments_Account $account
+ WC_Payments_Account $account,
+ WC_Payments_Session_Service $session_service
) {
$this->payments_api_client = $payments_api_client;
$this->customer_service = $customer_service;
$this->account = $account;
+ $this->session_service = $session_service;
+ }
+ /**
+ * Initializes this class's hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_filter( 'wcpay_prepare_fraud_config', [ $this, 'prepare_fraud_config' ], 10, 2 );
add_action( 'init', [ $this, 'link_session_if_user_just_logged_in' ] );
add_action( 'admin_print_footer_scripts', [ $this, 'add_sift_js_tracker' ] );
@@ -105,43 +122,11 @@ private function prepare_sift_config( $config ) {
}
}
- if ( $this->check_if_user_just_logged_in() ) {
- $config['session_id'] = $this->get_cookie_session_id();
- } else {
- if ( is_a( WC()->session, 'WC_Session' ) ) {
- $config['session_id'] = $wpcom_blog_id . '_' . WC()->session->get_customer_id();
- } else {
- return null; // we do not have a valid session for the current process.
- }
- }
+ $config['session_id'] = $this->session_service->get_sift_session_id();
return $config;
}
- /**
- * Called after the WooCommerce session has been initialized. Check if the current user has just logged in,
- * and sends that information to the server to link the current browser session with the user.
- *
- * @return boolean True if the user has just logged in, false in any other case.
- */
- public function check_if_user_just_logged_in() {
- if ( ! get_current_user_id() ) {
- return false;
- }
- WC()->initialize_session();
- $session_handler = WC()->session;
- // The Store API SessionHandler (used by WooPay) doesn't provide this method.
- if ( ! method_exists( $session_handler, 'get_session_cookie' ) ) {
- return false;
- }
- $cookie = $session_handler->get_session_cookie();
- if ( ! $cookie ) {
- return false;
- }
- $cookie_customer_id = $cookie[0];
- return $session_handler->get_customer_id() !== $cookie_customer_id;
- }
-
/**
* Called after the WooCommerce session has been initialized. Check if the current user has just logged in,
* and sends that information to the server to link the current browser session with the user.
@@ -153,7 +138,7 @@ public function link_session_if_user_just_logged_in() {
return;
}
- if ( ! $this->check_if_user_just_logged_in() ) {
+ if ( ! $this->session_service->user_just_logged_in() ) {
return;
}
@@ -171,39 +156,12 @@ public function link_session_if_user_just_logged_in() {
}
try {
- $this->payments_api_client->link_session_to_customer( $this->get_cookie_session_id(), $customer_id );
+ $this->session_service->link_current_session_to_customer( $customer_id );
} catch ( API_Exception $e ) {
Logger::log( '[Tracking] Error when linking session with user: ' . $e->getMessage() );
}
}
- /**
- * Get the session ID used until now for the current browsing session.
- *
- * @return string|NULL Session ID, or NULL if unknown.
- */
- private function get_cookie_session_id() {
- $wpcom_blog_id = $this->payments_api_client->get_blog_id();
- if ( ! $wpcom_blog_id ) {
- return null;
- }
-
- $session_handler = WC()->session;
- if ( ! $session_handler ) {
- return null;
- }
- // The Store API SessionHandler (used by WooPay) doesn't provide this method.
- if ( ! method_exists( $session_handler, 'get_session_cookie' ) ) {
- return null;
- }
- $cookie = $session_handler->get_session_cookie();
- if ( ! $cookie ) {
- return null;
- }
- $cookie_customer_id = $cookie[0];
- return $wpcom_blog_id . '_' . $cookie_customer_id;
- }
-
/**
* Adds the Sift JS page tracker if needed. See the comments for the detailed logic.
*
diff --git a/includes/class-wc-payments-incentives-service.php b/includes/class-wc-payments-incentives-service.php
index 240bbcf8782..77fc841e523 100644
--- a/includes/class-wc-payments-incentives-service.php
+++ b/includes/class-wc-payments-incentives-service.php
@@ -30,7 +30,14 @@ class WC_Payments_Incentives_Service {
*/
public function __construct( Database_Cache $database_cache ) {
$this->database_cache = $database_cache;
+ }
+ /**
+ * Initialise class hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_action( 'admin_menu', [ $this, 'add_payments_menu_badge' ] );
add_filter( 'woocommerce_admin_allowed_promo_notes', [ $this, 'allowed_promo_notes' ] );
add_filter( 'woocommerce_admin_woopayments_onboarding_task_badge', [ $this, 'onboarding_task_badge' ] );
diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php
index f4782d3b852..904462c37ef 100644
--- a/includes/class-wc-payments-onboarding-service.php
+++ b/includes/class-wc-payments-onboarding-service.php
@@ -43,7 +43,14 @@ class WC_Payments_Onboarding_Service {
public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache ) {
$this->payments_api_client = $payments_api_client;
$this->database_cache = $database_cache;
+ }
+ /**
+ * Initialise class hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_filter( 'wcpay_dev_mode', [ $this, 'maybe_enable_dev_mode' ], 100 );
add_filter( 'admin_body_class', [ $this, 'add_admin_body_classes' ] );
}
diff --git a/includes/class-wc-payments-order-service.php b/includes/class-wc-payments-order-service.php
index 95c8a0b963f..3cf3d3cbdea 100644
--- a/includes/class-wc-payments-order-service.php
+++ b/includes/class-wc-payments-order-service.php
@@ -277,19 +277,19 @@ public function mark_order_blocked_for_fraud( $order, $intent_id, $intent_status
* Updates the order to on-hold status and adds a note about the dispute.
*
* @param WC_Order $order Order object.
- * @param string $dispute_id The ID of the dispute associated with this order.
+ * @param string $charge_id The ID of the disputed charge associated with this order.
* @param string $amount The disputed amount – formatted currency value.
* @param string $reason The reason for the dispute – human-readable text.
* @param string $due_by The deadline for responding to the dispute - formatted date string.
*
* @return void
*/
- public function mark_payment_dispute_created( $order, $dispute_id, $amount, $reason, $due_by ) {
+ public function mark_payment_dispute_created( $order, $charge_id, $amount, $reason, $due_by ) {
if ( ! is_a( $order, 'WC_Order' ) ) {
return;
}
- $note = $this->generate_dispute_created_note( $dispute_id, $amount, $reason, $due_by );
+ $note = $this->generate_dispute_created_note( $charge_id, $amount, $reason, $due_by );
if ( $this->order_note_exists( $order, $note ) ) {
return;
}
@@ -303,17 +303,17 @@ public function mark_payment_dispute_created( $order, $dispute_id, $amount, $rea
* Updates the order status based on dispute status and adds a note about the dispute.
*
* @param WC_Order $order Order object.
- * @param string $dispute_id The ID of the dispute associated with this order.
+ * @param string $charge_id The ID of the disputed charge associated with this order.
* @param string $status The status of the dispute.
*
* @return void
*/
- public function mark_payment_dispute_closed( $order, $dispute_id, $status ) {
+ public function mark_payment_dispute_closed( $order, $charge_id, $status ) {
if ( ! is_a( $order, 'WC_Order' ) ) {
return;
}
- $note = $this->generate_dispute_closed_note( $dispute_id, $status );
+ $note = $this->generate_dispute_closed_note( $charge_id, $status );
if ( $this->order_note_exists( $order, $note ) ) {
return;
@@ -1218,15 +1218,15 @@ private function generate_fraud_blocked_note( $order ): string {
/**
* Get content for the dispute created order note.
*
- * @param string $dispute_id The ID of the dispute associated with this order.
+ * @param string $charge_id The ID of the disputes charge associated with this order.
* @param string $amount The disputed amount – formatted currency value.
* @param string $reason The reason for the dispute – human-readable text.
* @param string $due_by The deadline for responding to the dispute - formatted date string.
*
* @return string Note content.
*/
- private function generate_dispute_created_note( $dispute_id, $amount, $reason, $due_by ) {
- $dispute_url = $this->compose_dispute_url( $dispute_id );
+ private function generate_dispute_created_note( $charge_id, $amount, $reason, $due_by ) {
+ $dispute_url = $this->compose_dispute_url( $charge_id );
// Get merchant-friendly dispute reason description.
$reason = WC_Payments_Utils::get_dispute_reason_description( $reason );
@@ -1248,13 +1248,13 @@ private function generate_dispute_created_note( $dispute_id, $amount, $reason, $
/**
* Get content for the dispute closed order note.
*
- * @param string $dispute_id The ID of the dispute associated with this order.
- * @param string $status The status of the dispute.
+ * @param string $charge_id The ID of the disputed charge associated with this order.
+ * @param string $status The status of the dispute.
*
* @return string Note content.
*/
- private function generate_dispute_closed_note( $dispute_id, $status ) {
- $dispute_url = $this->compose_dispute_url( $dispute_id );
+ private function generate_dispute_closed_note( $charge_id, $status ) {
+ $dispute_url = $this->compose_dispute_url( $charge_id );
return sprintf(
WC_Payments_Utils::esc_interpolated_html(
/* translators: %1: the dispute status */
@@ -1270,16 +1270,16 @@ private function generate_dispute_closed_note( $dispute_id, $status ) {
/**
* Composes url for dispute details page.
*
- * @param string $dispute_id Dispute id.
+ * @param string $charge_id The disputed charge ID.
*
- * @return string Dispute details page url.
+ * @return string Transaction details page url.
*/
- private function compose_dispute_url( $dispute_id ) {
+ private function compose_dispute_url( $charge_id ) {
return add_query_arg(
[
'page' => 'wc-admin',
- 'path' => '/payments/disputes/details',
- 'id' => $dispute_id,
+ 'path' => '/payments/transactions/details',
+ 'id' => $charge_id,
],
admin_url( 'admin.php' )
);
diff --git a/includes/class-wc-payments-session-service.php b/includes/class-wc-payments-session-service.php
new file mode 100644
index 00000000000..0289011a454
--- /dev/null
+++ b/includes/class-wc-payments-session-service.php
@@ -0,0 +1,125 @@
+payments_api_client = $payments_api_client;
+ }
+
+ /**
+ * Called after the WooCommerce session has been initialized. Check if the current user has just logged in,
+ * and sends that information to the server to link the current browser session with the user.
+ *
+ * @return boolean True if the user has just logged in, false in any other case.
+ */
+ public function user_just_logged_in(): bool {
+ if ( ! get_current_user_id() ) {
+ return false;
+ }
+
+ WC()->initialize_session();
+ $session_handler = WC()->session;
+ // The Store API SessionHandler (used by WooPay) doesn't provide this method.
+ if ( ! method_exists( $session_handler, 'get_session_cookie' ) ) {
+ return false;
+ }
+ $cookie = $session_handler->get_session_cookie();
+ if ( ! $cookie ) {
+ return false;
+ }
+ $cookie_customer_id = $cookie[0];
+
+ return $session_handler->get_customer_id() !== $cookie_customer_id;
+ }
+
+ /**
+ * Get the Sift session ID for the current browsing session.
+ *
+ * @return string|null The Sift session ID or null if it can't be determined.
+ */
+ public function get_sift_session_id(): ?string {
+ $wpcom_blog_id = $this->payments_api_client->get_blog_id();
+ if ( ! $wpcom_blog_id ) {
+ // No session ID if Jetpack hasn't been connected yet.
+ return null;
+ }
+
+ if ( $this->user_just_logged_in() ) {
+ return $this->get_cookie_session_id();
+ }
+
+ if ( is_a( WC()->session, 'WC_Session' ) ) {
+ return $wpcom_blog_id . '_' . WC()->session->get_customer_id();
+ }
+
+ return null; // We do not have a valid session for the current process.
+ }
+
+ /**
+ * Get the session ID used until now for the current browsing session.
+ *
+ * @return string|null Session ID, or null if unknown.
+ */
+ public function get_cookie_session_id(): ?string {
+ $wpcom_blog_id = $this->payments_api_client->get_blog_id();
+ if ( ! $wpcom_blog_id ) {
+ return null;
+ }
+
+ $session_handler = WC()->session;
+ if ( ! $session_handler ) {
+ return null;
+ }
+ // The Store API SessionHandler (used by WooPay) doesn't provide this method.
+ if ( ! method_exists( $session_handler, 'get_session_cookie' ) ) {
+ return null;
+ }
+ $cookie = $session_handler->get_session_cookie();
+ if ( ! $cookie ) {
+ return null;
+ }
+ $cookie_customer_id = $cookie[0];
+ return $wpcom_blog_id . '_' . $cookie_customer_id;
+ }
+
+ /**
+ * Link a customer with the current browsing session, for tracking purposes.
+ *
+ * @param string $customer_id Stripe customer ID.
+ *
+ * @return array An array, containing a `success` flag.
+ *
+ * @throws API_Exception If an error occurs.
+ */
+ public function link_current_session_to_customer( string $customer_id ): array {
+ return $this->payments_api_client->link_session_to_customer( $this->get_cookie_session_id(), $customer_id );
+ }
+}
diff --git a/includes/class-wc-payments-status.php b/includes/class-wc-payments-status.php
index ed1e54fd0cf..02f4977df63 100644
--- a/includes/class-wc-payments-status.php
+++ b/includes/class-wc-payments-status.php
@@ -13,6 +13,13 @@
* Hooks into Woo Status pages to provide extra tooling and information about WCPay.
*/
class WC_Payments_Status {
+ /**
+ * Instance of WC_Payment_Gateway_WCPay
+ *
+ * @var WC_Payment_Gateway_WCPay
+ */
+ private $gateway;
+
/**
* Instance of WC_Payments_Http_Interface
*
@@ -30,14 +37,23 @@ class WC_Payments_Status {
/**
* WC_Payments_Status constructor.
*
+ * @param WC_Payment_Gateway_WCPay $gateway The main gateway instance.
* @param WC_Payments_Http_Interface $http A class implementing WC_Payments_Http_Interface.
* @param WC_Payments_Account $account The account service.
*/
- public function __construct( $http, $account ) {
+ public function __construct( $gateway, $http, $account ) {
+ $this->gateway = $gateway;
$this->http = $http;
$this->account = $account;
+ }
- add_action( 'woocommerce_system_status_report', [ $this, 'render_status_report_section' ] );
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
+ add_action( 'woocommerce_system_status_report', [ $this, 'render_status_report_section' ], 1 );
add_filter( 'woocommerce_debug_tools', [ $this, 'debug_tools' ] );
}
@@ -67,39 +83,193 @@ public function debug_tools( $tools ) {
/**
* Renders WCPay information on the status page.
*/
- public function render_status_report_section() {
- ?>
-
-
+ public function render_status_report_section() { ?>
+
+
+
+
+ WooPayments
+
+
+
+
+
+ :
+
+
+
+
+
+
+ :
+
+
+
+ http->is_connected() ? esc_html__( 'Yes', 'woocommerce-payments' ) : ' ' . esc_html__( 'No', 'woocommerce-payments' ) . ' '; ?>
+
+ http->is_connected() ) : ?>
+
+ :
+
+ http->is_connected() ? $this->http->get_blog_id() : '-' ); ?>
+
+
+ :
+
+ gateway->is_connected() ? esc_html( $this->account->get_stripe_account_id() ?? '-' ) : ' ' . esc_html__( 'Not connected', 'woocommerce-payments' ) . ' '; ?>
+
+ gateway->is_connected() ) :
+ ?>
-
- WooPayments
-
+ :
+
+ gateway->needs_setup() ? ' ' . esc_html__( 'Needs setup', 'woocommerce-payments' ) . ' ' : ( $this->gateway->is_enabled() ? esc_html__( 'Enabled', 'woocommerce-payments' ) : esc_html__( 'Disabled', 'woocommerce-payments' ) ); ?>
-
-
- :
-
-
+ :
+
+ is_test() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?>
+
+
+ :
+
+
+
+
+
+ :
+
+ gateway->get_upe_enabled_payment_method_ids() ) ); ?>
+
+
+
+
+
+ :
+
+
+
+
+
+
+
+ :
+
+
+ gateway->get_option( 'platform_checkout_button_locations', [] );
+ $woopay_enabled_locations = empty( $woopay_enabled_locations ) ? 'no locations enabled' : implode( ',', $woopay_enabled_locations );
+ echo esc_html( WC_Payments_Features::is_woopay_enabled() ? __( 'Enabled', 'woocommerce-payments' ) . ' (' . $woopay_enabled_locations . ')' : __( 'Disabled', 'woocommerce-payments' ) );
+ ?>
+
+
+
+ :
+
+
+
+
+
+
+ :
+
+
+ gateway->get_option( 'payment_request' );
+ $payment_request_enabled_locations = $this->gateway->get_option( 'payment_request_button_locations', [] );
+ $payment_request_enabled_locations = empty( $payment_request_enabled_locations ) ? 'no locations enabled' : implode( ',', $payment_request_enabled_locations );
+ echo esc_html( $payment_request_enabled ? __( 'Enabled', 'woocommerce-payments' ) . ' (' . $payment_request_enabled_locations . ')' : __( 'Disabled', 'woocommerce-payments' ) );
+ ?>
+
+
+
+ :
+
+ gateway->get_option( 'current_protection_level' ) ); ?>
+
+ gateway->get_option( 'current_protection_level' ) === 'advanced' ) : ?>
+
+ :
+
+
+ gateway->get_option( 'advanced_fraud_protection_settings' ) ), true );
+ $list = array_filter(
+ array_map(
+ function( $rule ) {
+ if ( empty( $rule['key'] ) ) {
+ return null;
+ }
+
+ switch ( $rule['key'] ) {
+ case 'avs_verification':
+ return 'AVS Verification';
+ case 'international_ip_address':
+ return 'International IP Address';
+ case 'ip_address_mismatch':
+ return 'IP Address Mismatch';
+ case 'address_mismatch':
+ return 'Address Mismatch';
+ case 'purchase_price_threshold':
+ return 'Purchase Price Threshold';
+ case 'order_items_threshold':
+ return 'Order Items Threshold';
+ default:
+ // Ignore all others.
+ return null;
+ }
+ },
+ $adv_fraud_settings
+ )
+ );
+
+ echo empty( $list ) ? '-' : esc_html( implode( ',', $list ) );
+ ?>
+
+
+
+
+
+ :
+
+
- :
-
- http->is_connected() ? esc_html_e( 'Yes', 'woocommerce-payments' ) : esc_html_e( 'No', 'woocommerce-payments' ); ?>
+ :
+
+
- :
-
- http->is_connected() ? $this->http->get_blog_id() : '-' ); ?>
+ :
+
+
- :
-
- account->get_stripe_account_id() ?? '-' ); ?>
+ :
+
+
-
-
+
+
+
+ :
+
+
+
+
+
get_meta( '_intent_id' );
$request = Get_Intention::create( $intent_id );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$intent_status = $intent->get_status();
@@ -520,10 +521,8 @@ private function process_webhook_payment_intent_succeeded( $event_body ) {
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
*/
private function process_webhook_dispute_created( $event_body ) {
- $event_type = $this->read_webhook_property( $event_body, 'type' );
$event_data = $this->read_webhook_property( $event_body, 'data' );
$event_object = $this->read_webhook_property( $event_data, 'object' );
- $dispute_id = $this->read_webhook_property( $event_object, 'id' );
$charge_id = $this->read_webhook_property( $event_object, 'charge' );
$reason = $this->read_webhook_property( $event_object, 'reason' );
$amount_raw = $this->read_webhook_property( $event_object, 'amount' );
@@ -551,7 +550,7 @@ private function process_webhook_dispute_created( $event_body ) {
);
}
- $this->order_service->mark_payment_dispute_created( $order, $dispute_id, $amount, $reason, $due_by );
+ $this->order_service->mark_payment_dispute_created( $order, $charge_id, $amount, $reason, $due_by );
// Clear dispute caches to trigger a fetch of new data.
$this->database_cache->delete( DATABASE_CACHE::DISPUTE_STATUS_COUNTS_KEY );
@@ -566,10 +565,8 @@ private function process_webhook_dispute_created( $event_body ) {
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
*/
private function process_webhook_dispute_closed( $event_body ) {
- $event_type = $this->read_webhook_property( $event_body, 'type' );
$event_data = $this->read_webhook_property( $event_body, 'data' );
$event_object = $this->read_webhook_property( $event_data, 'object' );
- $dispute_id = $this->read_webhook_property( $event_object, 'id' );
$charge_id = $this->read_webhook_property( $event_object, 'charge' );
$status = $this->read_webhook_property( $event_object, 'status' );
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
@@ -584,7 +581,7 @@ private function process_webhook_dispute_closed( $event_body ) {
);
}
- $this->order_service->mark_payment_dispute_closed( $order, $dispute_id, $status );
+ $this->order_service->mark_payment_dispute_closed( $order, $charge_id, $status );
// Clear dispute caches to trigger a fetch of new data.
$this->database_cache->delete( DATABASE_CACHE::DISPUTE_STATUS_COUNTS_KEY );
@@ -602,7 +599,6 @@ private function process_webhook_dispute_updated( $event_body ) {
$event_type = $this->read_webhook_property( $event_body, 'type' );
$event_data = $this->read_webhook_property( $event_body, 'data' );
$event_object = $this->read_webhook_property( $event_data, 'object' );
- $dispute_id = $this->read_webhook_property( $event_object, 'id' );
$charge_id = $this->read_webhook_property( $event_object, 'charge' );
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
@@ -632,8 +628,8 @@ private function process_webhook_dispute_updated( $event_body ) {
__( '%1$s. See dispute overview for more details.', 'woocommerce-payments' ),
$message,
add_query_arg(
- [ 'id' => $dispute_id ],
- admin_url( 'admin.php?page=wc-admin&path=/payments/disputes/details' )
+ [ 'id' => $charge_id ],
+ admin_url( 'admin.php?page=wc-admin&path=/payments/transactions/details' )
)
);
diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php
index eabaa7d1cac..a33aba7a3e0 100644
--- a/includes/class-wc-payments-woopay-button-handler.php
+++ b/includes/class-wc-payments-woopay-button-handler.php
@@ -217,24 +217,39 @@ public function ajax_add_to_cart() {
WC()->shipping->reset_shipping();
$product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false;
- $qty = ! isset( $_POST['qty'] ) ? 1 : absint( $_POST['qty'] );
+ $quantity = ! isset( $_POST['quantity'] ) ? 1 : absint( $_POST['quantity'] );
$product = wc_get_product( $product_id );
$product_type = $product->get_type();
// First empty the cart to prevent wrong calculation.
WC()->cart->empty_cart();
+ $is_add_to_cart_valid = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );
+
+ if ( ! $is_add_to_cart_valid ) {
+ // Some extensions error messages needs to be
+ // submitted to show error messages.
+ wp_send_json(
+ [
+ 'error' => true,
+ 'submit' => true,
+ ],
+ 400
+ );
+ return;
+ }
+
if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) {
$attributes = wc_clean( wp_unslash( $_POST['attributes'] ) );
$data_store = WC_Data_Store::load( 'product' );
$variation_id = $data_store->find_matching_product_variation( $product, $attributes );
- WC()->cart->add_to_cart( $product->get_id(), $qty, $variation_id, $attributes );
+ WC()->cart->add_to_cart( $product->get_id(), $quantity, $variation_id, $attributes );
}
- if ( 'simple' === $product_type || 'subscription' === $product_type ) {
- WC()->cart->add_to_cart( $product->get_id(), $qty );
+ if ( in_array( $product_type, [ 'simple', 'subscription', 'bundle', 'mix-and-match' ], true ) ) {
+ WC()->cart->add_to_cart( $product->get_id(), $quantity );
}
WC()->cart->calculate_totals();
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 36ff9512279..68a46ce4cfc 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -17,13 +17,13 @@
use WCPay\Payment_Methods\Bancontact_Payment_Method;
use WCPay\Payment_Methods\Becs_Payment_Method;
use WCPay\Payment_Methods\Giropay_Payment_Method;
+use WCPay\Payment_Methods\Klarna_Payment_Method;
use WCPay\Payment_Methods\P24_Payment_Method;
use WCPay\Payment_Methods\Sepa_Payment_Method;
use WCPay\Payment_Methods\Sofort_Payment_Method;
use WCPay\Payment_Methods\UPE_Payment_Gateway;
use WCPay\Payment_Methods\UPE_Split_Payment_Gateway;
use WCPay\Payment_Methods\Ideal_Payment_Method;
-use WCPay\Payment_Methods\JCB_Payment_Method;
use WCPay\Payment_Methods\Eps_Payment_Method;
use WCPay\Payment_Methods\UPE_Payment_Method;
use WCPay\WooPay_Tracker;
@@ -40,6 +40,8 @@
use WCPay\Core\WC_Payments_Customer_Service_API;
use WCPay\Constants\Payment_Method;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Internal\Service\Level3Service;
+use WCPay\Internal\Service\OrderService;
use WCPay\WooPay\WooPay_Scheduler;
use WCPay\WooPay\WooPay_Session;
@@ -90,6 +92,13 @@ class WC_Payments {
*/
private static $account;
+ /**
+ * Instance of WC_Payments_Session_Service, created in init function.
+ *
+ * @var WC_Payments_Session_Service
+ */
+ private static $session_service;
+
/**
* Instance of WC_Payments_Customer_Service, created in init function.
*
@@ -297,10 +306,12 @@ public static function init() {
include_once __DIR__ . '/class-database-cache.php';
self::$database_cache = new Database_Cache();
+ self::$database_cache->init_hooks();
include_once __DIR__ . '/class-wc-payments-dependency-service.php';
self::$dependency_service = new WC_Payments_Dependency_Service();
+ self::$dependency_service->init_hooks();
if ( false === self::$dependency_service->has_valid_dependencies() ) {
return;
@@ -372,6 +383,7 @@ public static function init() {
include_once __DIR__ . '/compat/subscriptions/trait-wc-payments-subscriptions-utilities.php';
include_once __DIR__ . '/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php';
+ include_once __DIR__ . '/class-wc-payments-session-service.php';
include_once __DIR__ . '/class-wc-payments-account.php';
include_once __DIR__ . '/class-wc-payments-customer-service.php';
include_once __DIR__ . '/class-logger.php';
@@ -395,7 +407,7 @@ public static function init() {
include_once __DIR__ . '/payment-methods/class-link-payment-method.php';
include_once __DIR__ . '/payment-methods/class-affirm-payment-method.php';
include_once __DIR__ . '/payment-methods/class-afterpay-payment-method.php';
- include_once __DIR__ . '/payment-methods/class-jcb-payment-method.php';
+ include_once __DIR__ . '/payment-methods/class-klarna-payment-method.php';
include_once __DIR__ . '/class-wc-payment-token-wcpay-sepa.php';
include_once __DIR__ . '/class-wc-payments-status.php';
include_once __DIR__ . '/class-wc-payments-token-service.php';
@@ -449,11 +461,7 @@ public static function init() {
include_once __DIR__ . '/core/service/class-wc-payments-customer-service-api.php';
include_once __DIR__ . '/class-duplicate-payment-prevention-service.php';
include_once __DIR__ . '/class-wc-payments-incentives-service.php';
-
- // Load customer multi-currency if feature is enabled.
- if ( WC_Payments_Features::is_customer_multi_currency_enabled() ) {
- include_once __DIR__ . '/multi-currency/wc-payments-multi-currency.php';
- }
+ include_once __DIR__ . '/multi-currency/wc-payments-multi-currency.php';
self::$woopay_checkout_service = new Checkout_Service();
self::$woopay_checkout_service->init();
@@ -472,11 +480,12 @@ public static function init() {
self::$order_service = new WC_Payments_Order_Service( self::$api_client );
self::$action_scheduler_service = new WC_Payments_Action_Scheduler_Service( self::$api_client, self::$order_service );
- self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service );
+ self::$session_service = new WC_Payments_Session_Service( self::$api_client );
+ self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$session_service );
self::$customer_service = new WC_Payments_Customer_Service( self::$api_client, self::$account, self::$database_cache );
self::$token_service = new WC_Payments_Token_Service( self::$api_client, self::$customer_service );
self::$remote_note_service = new WC_Payments_Remote_Note_Service( WC_Data_Store::load( 'admin-note' ) );
- self::$fraud_service = new WC_Payments_Fraud_Service( self::$api_client, self::$customer_service, self::$account );
+ self::$fraud_service = new WC_Payments_Fraud_Service( self::$api_client, self::$customer_service, self::$account, self::$session_service );
self::$in_person_payments_receipts_service = new WC_Payments_In_Person_Payments_Receipts_Service();
self::$localization_service = new WC_Payments_Localization_Service();
self::$failed_transaction_rate_limiter = new Session_Rate_Limiter( Session_Rate_Limiter::SESSION_KEY_DECLINED_CARD_REGISTRY, 5, 10 * MINUTE_IN_SECONDS );
@@ -489,6 +498,12 @@ public static function init() {
( new WooPay_Scheduler( self::$api_client ) )->init();
+ // Initialise hooks.
+ self::$account->init_hooks();
+ self::$fraud_service->init_hooks();
+ self::$onboarding_service->init_hooks();
+ self::$incentives_service->init_hooks();
+
self::$legacy_card_gateway = new CC_Payment_Gateway( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service );
$payment_method_classes = [
@@ -504,7 +519,7 @@ public static function init() {
Link_Payment_Method::class,
Affirm_Payment_Method::class,
Afterpay_Payment_Method::class,
- JCB_Payment_Method::class,
+ Klarna_Payment_Method::class,
];
if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) {
$payment_methods = [];
@@ -601,7 +616,7 @@ public static function init() {
}
if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) {
- new WC_Payments_Admin(
+ $admin = new WC_Payments_Admin(
self::$api_client,
self::get_gateway(),
self::$account,
@@ -610,18 +625,23 @@ public static function init() {
self::$incentives_service,
self::$database_cache
);
+ $admin->init_hooks();
- new WC_Payments_Admin_Settings( self::get_gateway() );
+ $admin_settings = new WC_Payments_Admin_Settings( self::get_gateway() );
+ $admin_settings->init_hooks();
// Use tracks loader only in admin screens because it relies on WC_Tracks loaded by WC_Admin.
include_once WCPAY_ABSPATH . 'includes/admin/tracks/tracks-loader.php';
include_once __DIR__ . '/admin/class-wc-payments-admin-sections-overwrite.php';
- new WC_Payments_Admin_Sections_Overwrite( self::get_account_service() );
+ $admin_sections_overwrite = new WC_Payments_Admin_Sections_Overwrite( self::get_account_service() );
+ $admin_sections_overwrite->init_hooks();
- new WC_Payments_Status( self::get_wc_payments_http(), self::get_account_service() );
+ $wcpay_status = new WC_Payments_Status( self::get_gateway(), self::get_wc_payments_http(), self::get_account_service() );
+ $wcpay_status->init_hooks();
- new WCPay\Fraud_Prevention\Order_Fraud_And_Risk_Meta_Box( self::$order_service );
+ $wcpay_order_frt_meta_box = new WCPay\Fraud_Prevention\Order_Fraud_And_Risk_Meta_Box( self::$order_service );
+ $wcpay_order_frt_meta_box->init_hooks();
}
// Load Stripe Billing subscription integration.
@@ -634,6 +654,8 @@ public static function init() {
add_action( 'woocommerce_onboarding_profile_data_updated', 'WC_Payments_Features::maybe_enable_wcpay_subscriptions_after_onboarding', 10, 2 );
}
+ add_action( 'woocommerce_woocommerce_payments_updated', [ __CLASS__, 'maybe_disable_wcpay_subscriptions_on_update' ] );
+
add_action( 'rest_api_init', [ __CLASS__, 'init_rest_api' ] );
add_action( 'woocommerce_woocommerce_payments_updated', [ __CLASS__, 'set_plugin_activation_timestamp' ] );
@@ -945,6 +967,7 @@ private static function get_wc_payments_http() {
if ( ! $http_class instanceof WC_Payments_Http_Interface ) {
$http_class = new WC_Payments_Http( new Automattic\Jetpack\Connection\Manager( 'woocommerce-payments' ) );
+ $http_class->init_hooks();
}
return $http_class;
@@ -1025,6 +1048,10 @@ public static function init_rest_api() {
$onboarding_controller = new WC_REST_Payments_Onboarding_Controller( self::$api_client, self::$onboarding_service );
$onboarding_controller->register_routes();
+ include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-customer-controller.php';
+ $customer_controller = new WC_REST_Payments_Customer_Controller( self::$api_client, self::$customer_service );
+ $customer_controller->register_routes();
+
if ( WC_Payments_Features::is_upe_settings_preview_enabled() ) {
include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-upe-flag-toggle-controller.php';
$upe_flag_toggle_controller = new WC_REST_UPE_Flag_Toggle_Controller( self::get_gateway() );
@@ -1049,6 +1076,15 @@ public static function init_rest_api() {
$payment_intents_controller = new WC_REST_Payments_Payment_Intents_Controller( self::$api_client );
$payment_intents_controller->register_routes();
+ include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-payment-intents-create-controller.php';
+ $payment_intents_create_controller = new WC_REST_Payments_Payment_Intents_Create_Controller(
+ self::$api_client,
+ self::get_gateway(),
+ wcpay_get_container()->get( OrderService::class ),
+ wcpay_get_container()->get( Level3Service::class )
+ );
+ $payment_intents_create_controller->register_routes();
+
include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-authorizations-controller.php';
$authorizations_controller = new WC_REST_Payments_Authorizations_Controller( self::$api_client );
$authorizations_controller->register_routes();
@@ -1257,6 +1293,15 @@ public static function get_customer_service_api(): WC_Payments_Customer_Service_
return self::$customer_service_api;
}
+ /**
+ * Returns the order service instance.
+ *
+ * @return WC_Payments_Order_Service
+ */
+ public static function get_order_service(): WC_Payments_Order_Service {
+ return self::$order_service;
+ }
+
/**
* Sets the customer service instance. This is needed only for tests.
*
@@ -1641,7 +1686,7 @@ public static function create_request( $class_name, $id = null ) {
/**
* Inject an inline script with WCPay assets properties.
- * window.wcpayAssets.url – Dist URL, required to properly load chunks on sites with JS concatenation enabled.
+ * window.wcpayAssets.url – Dist URL, required to properly load chunks on sites with JS concatenation enabled.
*
* @return void
*/
@@ -1741,4 +1786,15 @@ private static function should_load_stripe_billing_integration() {
)
);
}
+
+ /**
+ * Disable the WCPay Subscriptions feature on WooPayments plugin update if it's enabled and the store is no longer eligible.
+ *
+ * @see WC_Payments_Features::is_wcpay_subscriptions_eligible() for eligibility criteria.
+ */
+ public static function maybe_disable_wcpay_subscriptions_on_update() {
+ if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() && ( class_exists( 'WC_Subscriptions' ) || ! WC_Payments_Features::is_wcpay_subscriptions_eligible() ) ) {
+ update_option( WC_Payments_Features::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '0' );
+ }
+ }
}
diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php
index 555143461b4..b6c8e15a621 100644
--- a/includes/class-woopay-tracker.php
+++ b/includes/class-woopay-tracker.php
@@ -170,6 +170,12 @@ public function maybe_record_admin_event( $event, $data = [] ) {
*/
public function should_enable_tracking( $is_admin_event = false, $track_on_all_stores = false ) {
+ // Don't track if the gateway is not enabled.
+ $gateway = \WC_Payments::get_gateway();
+ if ( ! $gateway->is_enabled() ) {
+ return false;
+ }
+
// Don't track if the account is not connected.
$account = WC_Payments::get_account_service();
if ( is_null( $account ) || ! $account->is_stripe_connected() ) {
@@ -208,7 +214,6 @@ public function should_enable_tracking( $is_admin_event = false, $track_on_all_s
}
// For the remaining events, don't track when woopay is disabled.
- $gateway = \WC_Payments::get_gateway();
$is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag.
$is_woopay_enabled = 'yes' === $gateway->get_option( 'platform_checkout', 'no' );
if ( ! ( $is_woopay_eligible && $is_woopay_enabled ) ) {
diff --git a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
index a96963d8022..e4bbb2d3458 100644
--- a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
+++ b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
@@ -256,7 +256,8 @@ public function prepare_intent_for_order_pay_page(): bool {
$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
$request = Get_Intention::create( $order->get_transaction_id() );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
if ( ! $intent || Intent_Status::REQUIRES_ACTION !== $intent->get_status() ) {
return false;
diff --git a/includes/constants/class-payment-method.php b/includes/constants/class-payment-method.php
index 4114885de00..ee498864ab5 100644
--- a/includes/constants/class-payment-method.php
+++ b/includes/constants/class-payment-method.php
@@ -31,10 +31,10 @@ class Payment_Method extends Base_Constant {
const P24 = 'p24';
const SEPA = 'sepa_debit';
const SOFORT = 'sofort';
- const JCB = 'jcb';
const US_BANK_ACCOUNT = 'us_bank_account';
const AFFIRM = 'affirm';
const AFTERPAY = 'afterpay_clearpay';
+ const KLARNA = 'klarna';
const IPP_ALLOWED_PAYMENT_METHODS = [
self::CARD_PRESENT,
@@ -44,5 +44,6 @@ class Payment_Method extends Base_Constant {
const BNPL_PAYMENT_METHODS = [
self::AFFIRM,
self::AFTERPAY,
+ self::KLARNA,
];
}
diff --git a/includes/core/server/CONTRIBUTING.md b/includes/core/server/CONTRIBUTING.md
index 6bc4536f57e..33dd5e20e70 100644
--- a/includes/core/server/CONTRIBUTING.md
+++ b/includes/core/server/CONTRIBUTING.md
@@ -112,7 +112,8 @@ add_filter( 'wcpay_my_request', function ( $request ) {
$request = My_Request::create();
$request->set_name( 'John' );
-$request->send( 'wcpay_my_request );
+$request->assign_hook( 'wcpay_my_request' );
+$request->send();
```
### Validators
@@ -151,8 +152,9 @@ Request classes can be extended as any other PHP class. Let's use the existing `
```php
$request = Create_And_Confirm_Intention::create();
+$request->set_hook_args( $payment_information );
// Call all necessary setters...
-$intent = $request->send( 'wcpay_create_intention_request', $payment_information );
+$intent = $request->send();
```
### 2. Extend the class
diff --git a/includes/core/server/README.md b/includes/core/server/README.md
index 465f9410397..cde03991363 100644
--- a/includes/core/server/README.md
+++ b/includes/core/server/README.md
@@ -35,13 +35,14 @@ use WCPay\Core\Server\Request\Get_Intention;
$intention_id = $this->order_service->get_intent_id_for_order( $order );
$request = Get_Intention::create( $intention_id );
+$request->assign_hook( 'my_get_intention_request', $order );
/**
* Sends a request to retrieve an intention.
*
* @param WC_Order $order The order, which the intent is associated with.
*/
-$intention = $request->send( 'my_get_intention_request', $order );
+$intention = $request->send();
```
Highlights from this example:
@@ -62,14 +63,7 @@ $request = Create_Intention::create();
$request->set_amount( WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) );
$request->set_currency( $order->get_currency() );
$request->set_payment_method( $payment_information->get_payment_method() );
-
-/**
- * Sending a request to create and confirm a payment intention.
- *
- * @param WC_Order $order The order which the intention belongs to.
- * @param Payment_Information $payment_information Prepared payment information from the gateway.
- */
-$intention = $request->send( 'wcpay_create_intention_request', $order, $payment_information );
+$intention = $request->send();
```
### Generic requests
@@ -88,7 +82,7 @@ $request = new Generic(
'amount' => 300
]
);
-$intention = $request->send( 'custom_create_intention_request' );
+$intention = $request->send();
```
However, once the rest of the related development is finished, please create a new request class before merging.
@@ -116,7 +110,9 @@ Here is a **good example**:
* @param WC_Order $order The order which the intention belongs to.
* @param Payment_Information $payment_information Prepared payment information from the gateway.
*/
-$intention = $request->send( 'wcpay_update_intention_request', $order, $payment_information );
+$request->assign_hook( 'wcpay_update_intention_request' );
+$request->set_hook_args( $order, $payment_information )
+$intention = $request->send();
```
- This example includes everything needed in the `$order` and `$payment_information` parameters, and they are documented.
@@ -125,7 +121,9 @@ $intention = $request->send( 'wcpay_update_intention_request', $order, $payment_
Here is a **bad example**:
```php
-$intention = $request->send( 'update_request', $intention_id );
+$request->assign_hook('update_request', $intention_id );
+
+$intention = $request->send();
```
- This example uses `update_request`, which could appear in other hooks.
diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php
index 8ac0f70d398..6d7f38c0269 100644
--- a/includes/core/server/class-request.php
+++ b/includes/core/server/class-request.php
@@ -93,12 +93,26 @@ abstract class Request {
*/
protected $id;
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = '';
+
+ /**
+ * Used to set the arguments for the request class WordPress hook. Make sure to add hook name first.
+ *
+ * @var array
+ */
+ protected $hook_args = [];
+
/**
* Creates a new request, loading dependencies in there.
*
* @param mixed $id The identifier for various update/get/delete requests.
*
- * @indexof $this->>routeList
+ * @indexof $this->routeList
*
* @return static
*/
@@ -301,17 +315,15 @@ final public function get_param( $key ) {
/**
* Allows the request to be modified, and then sends it.
*
- * @param string $hook The filter to use.
- * @param mixed ...$args Other parameters for the hook.
* @return mixed Either the response array, or the correct object.
*
* @throws Extend_Request_Exception
* @throws Immutable_Parameter_Exception
* @throws Invalid_Request_Parameter_Exception
*/
- final public function send( $hook, ...$args ) {
+ final public function send() {
return $this->format_response(
- $this->api_client->send_request( $this->apply_filters( $hook, ...$args ) )
+ $this->api_client->send_request( $this->apply_filters( $this->hook, ...$this->hook_args ) )
);
}
@@ -319,17 +331,15 @@ final public function send( $hook, ...$args ) {
* This is mimic of send method, but where API execption is handled.
* The reason behind this is that sometimes API request can fail for valid reasons and instead of handling this exception on every request, you could use this function.
*
- * @param string $hook The filter to use.
- * @param mixed ...$args Other parameters for the hook.
* @return mixed Either the response array, or the correct object.
*
* @throws Extend_Request_Exception
* @throws Immutable_Parameter_Exception
* @throws Invalid_Request_Parameter_Exception
*/
- final public function handle_rest_request( $hook, ...$args ) {
+ final public function handle_rest_request() {
try {
- $data = $this->send( $hook, ...$args );
+ $data = $this->send();
// Make sure to return array if $data is instance or has parent as a Response class.
if ( is_a( $data, Response::class ) ) {
return $data->to_array();
@@ -369,6 +379,24 @@ protected function set_request_route_id_parameter( $id ) {
}
}
}
+ /**
+ * Assign the WordPress hook and the arguments specific to the previously assigned hook.
+ *
+ * @param string $hook WordPress hook name.
+ */
+ public function assign_hook( string $hook ) {
+ $this->hook = $hook;
+ }
+
+ /**
+ * Set hook arguments. Used when hook is predefined in the request class, but you want to pass hook args.
+ *
+ * @param mixed ...$args Arguments for the hook.
+ * @return void
+ */
+ public function set_hook_args( ...$args ) {
+ $this->hook_args = $args;
+ }
/**
* Stores a parameter within the internal props.
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 411f6aae5b1..5b97bee0993 100644
--- a/includes/core/server/request/class-add-account-tos-agreement.md
+++ b/includes/core/server/request/class-add-account-tos-agreement.md
@@ -15,8 +15,6 @@ The `WCPay\Core\Server\Request\Add_Account_Tos_Agreement` class is used to const
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_add_account_tos_agreement`
- Arguments: None.
@@ -26,5 +24,5 @@ When using this request, provide the following filter and arguments:
$request = Add_Account_Tos_Agreement::create();
$request->set_source( 'settings-popup' );
$request->set_user_name( 'current_username' );
-$request->send( 'wcpay_add_account_tos_agreement' );
+$request->send();
```
diff --git a/includes/core/server/request/class-add-account-tos-agreement.php b/includes/core/server/request/class-add-account-tos-agreement.php
index d205d088bb4..11cec241f85 100644
--- a/includes/core/server/request/class-add-account-tos-agreement.php
+++ b/includes/core/server/request/class-add-account-tos-agreement.php
@@ -21,6 +21,13 @@ class Add_Account_Tos_Agreement extends Request {
'user_name',
];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_add_account_tos_agreement';
+
/**
* Returns the request's API.
*
diff --git a/includes/core/server/request/class-cancel-intention.md b/includes/core/server/request/class-cancel-intention.md
index 54c01e4b251..0f22a295fae 100644
--- a/includes/core/server/request/class-cancel-intention.md
+++ b/includes/core/server/request/class-cancel-intention.md
@@ -13,8 +13,6 @@ There are no additional parameters for this request.
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_cancel_intent_request`
- Arguments: `WC_Order $order`
@@ -22,5 +20,6 @@ When using this request, provide the following filter and arguments:
```php
$request = Cancel_Intention::create( $id );
-$request->send( 'wcpay_cancel_intent_request', $order );
+$request->set_hook_args( $order )
+$request->send();
```
diff --git a/includes/core/server/request/class-cancel-intention.php b/includes/core/server/request/class-cancel-intention.php
index 8da5ab9d3bc..4500e6ac15d 100644
--- a/includes/core/server/request/class-cancel-intention.php
+++ b/includes/core/server/request/class-cancel-intention.php
@@ -23,6 +23,13 @@ class Cancel_Intention extends Request {
const REQUIRED_PARAMS = [];
const DEFAULT_PARAMS = [];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_cancel_intent_request';
+
/**
* Sets the intent ID, which will be used in the request URL.
*
diff --git a/includes/core/server/request/class-capture-intention.md b/includes/core/server/request/class-capture-intention.md
index 36d5fe50a1c..785b9012bd9 100644
--- a/includes/core/server/request/class-capture-intention.md
+++ b/includes/core/server/request/class-capture-intention.md
@@ -26,5 +26,6 @@ When using this request, provide the following filter and arguments:
```php
$request = Capture_Intention::create( $id );
$request->set_amount_to_capture( $amount );
-$request->send( 'wcpay_capture_intent_request', $order );
+$request->set_hook_args( $order );
+$request->send();
```
diff --git a/includes/core/server/request/class-capture-intention.php b/includes/core/server/request/class-capture-intention.php
index 6f7dcfb500d..cb5074fbd25 100644
--- a/includes/core/server/request/class-capture-intention.php
+++ b/includes/core/server/request/class-capture-intention.php
@@ -25,6 +25,13 @@ class Capture_Intention extends Request {
'level3' => [],
];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_capture_intent_request';
+
/**
* Sets the intent ID, which will be used in the request URL.
*
@@ -76,6 +83,15 @@ public function set_level3( $level3 ) {
$this->set_param( 'level3', $this->fix_level3_data( $level3 ) );
}
+ /**
+ * Setter for intent metadata.
+ *
+ * @param array $metadata Intent metadata that includes stuff like order details, card reader specifics, etc..
+ */
+ public function set_metadata( array $metadata ): void {
+ $this->set_param( 'metadata', $metadata );
+ }
+
/**
* Formats the response from the server.
*
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 ed5edfb4b68..e6e2c390f00 100644
--- a/includes/core/server/request/class-create-and-confirm-intention.md
+++ b/includes/core/server/request/class-create-and-confirm-intention.md
@@ -50,5 +50,6 @@ $request->set_payment_method( $payment_method_id );
$request->set_payment_method_types( $payment_methods );
$request->set_payment_methods( $payment_methods );
$request->setup_future_usage();
-$request->send( 'wcpay_create_and_confirm_intent_request', $payment_information );
+$request->set_hook_args( $payment_information );
+$request->send();
```
diff --git a/includes/core/server/request/class-create-and-confirm-intention.php b/includes/core/server/request/class-create-and-confirm-intention.php
index 3313caa052b..905354045d1 100644
--- a/includes/core/server/request/class-create-and-confirm-intention.php
+++ b/includes/core/server/request/class-create-and-confirm-intention.php
@@ -36,6 +36,13 @@ class Create_And_Confirm_Intention extends Create_Intention {
'capture_method' => 'automatic',
];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_create_and_confirm_intent_request';
+
/**
* Returns the request's API.
*
@@ -80,14 +87,6 @@ public function set_off_session( bool $off_session = true ) {
* @throws Invalid_Request_Parameter_Exception When there are no payment methods provided.
*/
public function set_payment_methods( array $payment_methods ) {
- // Hard to validate without hardcoding a list here.
- if ( empty( $payment_methods ) ) {
- throw new Invalid_Request_Parameter_Exception(
- __( 'Intentions require at least one payment method', 'woocommerce-payments' ),
- 'wcpay_core_invalid_request_parameter_missing_payment_method_types'
- );
- }
-
$this->set_param( 'payment_method_types', $payment_methods );
}
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 001891fa283..e50bf1fd471 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
@@ -20,8 +20,6 @@ The `WCPay\Core\Server\Request\Create_and_Confirm_Setup_Intention` class is used
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_create_and_confirm_setup_intent_request`
- Arguments:
- `WCPay\Payment_Information $payment_information`
@@ -37,5 +35,7 @@ $request->set_metadata( $metadata );
$request->set_payment_method( $payment_method_id );
$request->set_payment_method_types( $payment_method_types );
$request->set_mandate_data( $mandate_data );
-$request->send( 'wcpay_create_and_confirm_setup_intent_request', $payment_information, $save_in_platform_account, $save_payment_method_to_platform );
+$request->assign_hook( 'wcpay_create_and_confirm_setup_intent_request' );
+$request->set_hook_args( $payment_information, $save_in_platform_account, $save_payment_method_to_platform );
+$request->send();
```
diff --git a/includes/core/server/request/class-create-and-confirm-setup-intention.php b/includes/core/server/request/class-create-and-confirm-setup-intention.php
index 24769b9e6d2..bf8831e293a 100644
--- a/includes/core/server/request/class-create-and-confirm-setup-intention.php
+++ b/includes/core/server/request/class-create-and-confirm-setup-intention.php
@@ -27,6 +27,13 @@ class Create_And_Confirm_Setup_Intention extends Request {
'metadata' => [],
];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_create_and_confirm_setup_intention_request';
+
/**
* Returns the request's API.
*
diff --git a/includes/core/server/request/class-create-intention.md b/includes/core/server/request/class-create-intention.md
index faa3168b0ac..0ac4f7c397b 100644
--- a/includes/core/server/request/class-create-intention.md
+++ b/includes/core/server/request/class-create-intention.md
@@ -25,8 +25,6 @@ The `WCPay\Core\Server\Request\Create_Intention` class is used to construct the
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_create_intent_request`
- Arguments: `WC_Order $order`
@@ -44,5 +42,5 @@ $request->set_mandate_data( $mandate_data );
$request->set_metadata( $metadata );
$request->set_payment_method( $payment_method_id );
$request->set_payment_method_types( $payment_methods );
-$request->send( 'wcpay_create_intent_request', $order );
+$request->send();
```
diff --git a/includes/core/server/request/class-create-intention.php b/includes/core/server/request/class-create-intention.php
index c3ce3bd37d3..917089eb5d7 100644
--- a/includes/core/server/request/class-create-intention.php
+++ b/includes/core/server/request/class-create-intention.php
@@ -22,6 +22,13 @@ class Create_Intention extends Request {
const IMMUTABLE_PARAMS = [ 'amount' ];
const REQUIRED_PARAMS = [ 'amount', 'currency' ];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_create_intent_request';
+
/**
* Returns the request's API.
*
diff --git a/includes/core/server/request/class-create-setup-intention.md b/includes/core/server/request/class-create-setup-intention.md
index 0e7edf0342d..877ec791a65 100644
--- a/includes/core/server/request/class-create-setup-intention.md
+++ b/includes/core/server/request/class-create-setup-intention.md
@@ -17,8 +17,6 @@ The `WCPay\Core\Server\Request\Create_Setup_Intention` class is used to construc
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_create_setup_intent_request`
- Arguments: None
@@ -28,5 +26,5 @@ When using this request, provide the following filter and arguments:
$request = Create_Setup_Intention::create();
$request->set_customer( $customer_id );
$request->set_payment_method_types( $payment_methods );
-$request->send( 'wcpay_create_setup_intent_request' );
+$request->send();
```
diff --git a/includes/core/server/request/class-create-setup-intention.php b/includes/core/server/request/class-create-setup-intention.php
index 2bf7c12ea1f..9614d8fd265 100644
--- a/includes/core/server/request/class-create-setup-intention.php
+++ b/includes/core/server/request/class-create-setup-intention.php
@@ -22,6 +22,13 @@ class Create_Setup_Intention extends Request {
const IMMUTABLE_PARAMS = [ 'customer', 'confirm' ];
const REQUIRED_PARAMS = [ 'customer', 'payment_method_types' ];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_create_setup_intention_request';
+
const DEFAULT_PARAMS = [
'confirm' => 'false',
];
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 efbe473b554..2360ba3a0a9 100644
--- a/includes/core/server/request/class-get-account-capital-link.md
+++ b/includes/core/server/request/class-get-account-capital-link.md
@@ -17,9 +17,6 @@ Expected response is an account link object with create, expires_at, and url fie
## Filter
-
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_get_account_capital_link`
- Arguments: None.
@@ -30,5 +27,5 @@ $request = Get_Account_Capital_Link::create();
$request->set_type( 'capital_financing_offer' );
$request->set_return_url( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' );
$request->set_refresh_url( 'http://example.org/wp-admin/admin.php?wcpay-loan-offer' );
-$request->send( 'wcpay_get_account_capital_link' );
+$request->send();
```
diff --git a/includes/core/server/request/class-get-account-capital-link.php b/includes/core/server/request/class-get-account-capital-link.php
index 46372a09fcd..01e4ead7e00 100644
--- a/includes/core/server/request/class-get-account-capital-link.php
+++ b/includes/core/server/request/class-get-account-capital-link.php
@@ -22,6 +22,13 @@ class Get_Account_Capital_Link extends Request {
'refresh_url',
];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_get_account_capital_link';
+
/**
* Returns the request's API.
*
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 a7dd6667847..4ea6a6f37e9 100644
--- a/includes/core/server/request/class-get-account-login-data.md
+++ b/includes/core/server/request/class-get-account-login-data.md
@@ -15,8 +15,6 @@ Note that this request sends the test_mode flag only when the site is in the dev
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wpcay_get_account_login_data`
- Arguments: None.
@@ -25,5 +23,5 @@ When using this request, provide the following filter and arguments:
```php
$request = Get_Account_Login_Data::create();
$request->set_redirect_url( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' );
-$request->send( 'wpcay_get_account_login_data' );
+$request->send();
```
diff --git a/includes/core/server/request/class-get-account-login-data.php b/includes/core/server/request/class-get-account-login-data.php
index 0ce0c8dcbcd..97df3210677 100644
--- a/includes/core/server/request/class-get-account-login-data.php
+++ b/includes/core/server/request/class-get-account-login-data.php
@@ -21,6 +21,13 @@ class Get_Account_Login_Data extends Request {
'redirect_url',
];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wpcay_get_account_login_data';
+
/**
* Returns the request's API.
*
diff --git a/includes/core/server/request/class-get-account.md b/includes/core/server/request/class-get-account.md
index 60dcd901d51..7cdd3cd4330 100644
--- a/includes/core/server/request/class-get-account.md
+++ b/includes/core/server/request/class-get-account.md
@@ -21,5 +21,5 @@ When using this request, provide the following filter:
```php
$request = Get_Account::create();
-$request->send( 'wcpay_get_account' );
+$request->send();
```
diff --git a/includes/core/server/request/class-get-account.php b/includes/core/server/request/class-get-account.php
index 4f9710c512b..144cf26f35e 100644
--- a/includes/core/server/request/class-get-account.php
+++ b/includes/core/server/request/class-get-account.php
@@ -16,6 +16,13 @@
class Get_Account extends Request {
use Use_Test_Mode_Only_When_Dev_Mode;
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_get_account';
+
/**
* Returns the request's API.
*
diff --git a/includes/core/server/request/class-get-charge.md b/includes/core/server/request/class-get-charge.md
index eb8cad9867d..bc9173a41e1 100644
--- a/includes/core/server/request/class-get-charge.md
+++ b/includes/core/server/request/class-get-charge.md
@@ -16,11 +16,11 @@ There are no additional parameters for this request.
When using this request, provide the following filter and arguments:
- Name: `wcpay_get_charge_request`
-- Arguments: None
+- Arguments: None or charge id.
## Example:
```php
$request = Get_Charge::create( $id );
-$request->send( 'wcpay_get_charge_request' );
+$request->send();
```
diff --git a/includes/core/server/request/class-get-charge.php b/includes/core/server/request/class-get-charge.php
index 42e6afe63a8..b2a4667b6ea 100644
--- a/includes/core/server/request/class-get-charge.php
+++ b/includes/core/server/request/class-get-charge.php
@@ -16,6 +16,13 @@
* Request class for getting intents.
*/
class Get_Charge extends Request {
+
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_get_charge_request';
/**
* Sets the intent ID, which will be used in the request URL.
*
diff --git a/includes/core/server/request/class-get-intention.md b/includes/core/server/request/class-get-intention.md
index 0459603d29d..f7b71e8c091 100644
--- a/includes/core/server/request/class-get-intention.md
+++ b/includes/core/server/request/class-get-intention.md
@@ -22,5 +22,6 @@ When using this request, provide the following filter and arguments:
```php
$request = Get_Intention::create( $id );
-$request->send( 'wcpay_get_intent_request', $order );
+$request->set_hook_args( $order )
+$request->send();
```
diff --git a/includes/core/server/request/class-get-intention.php b/includes/core/server/request/class-get-intention.php
index dadac29bb75..641ec25139e 100644
--- a/includes/core/server/request/class-get-intention.php
+++ b/includes/core/server/request/class-get-intention.php
@@ -24,6 +24,13 @@ class Get_Intention extends Request {
*/
private $intent_id;
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_get_intent_request';
+
/**
* Class constructor.
*
diff --git a/includes/core/server/request/class-get-request.md b/includes/core/server/request/class-get-request.md
index 15d6f38422c..b591cb98cba 100644
--- a/includes/core/server/request/class-get-request.md
+++ b/includes/core/server/request/class-get-request.md
@@ -23,17 +23,20 @@ When creating `Get_Request` requests, the ID parameter could be passed to the `:
```php
$request = Request::get( ('deposits', $id );
-$request->send( 'wcpay_get_deposit_request' );
+$request->assign_hook( 'wcpay_get_deposit_request' );
+$request->send();
```
```php
$request = Request::get( ('deposits' ); // Without ID argument.
-$request->send( 'wcpay_get_deposits_request' );
+$request->assign_hook( 'wcpay_get_deposits_request' );
+$request->send();
```
You could also create request with the following code, but it is not recommended.
```php
$request = Get_Request::create( $id );
$request->set_method ('deposits')
-$request->send( 'wcpay_get_deposit_request' );
+$request->assign_hook( 'wcpay_get_deposit_request' );
+$request->send();
```
diff --git a/includes/core/server/request/class-get-setup-intention.md b/includes/core/server/request/class-get-setup-intention.md
index 75fc5f818b9..5b42c8ad4fe 100644
--- a/includes/core/server/request/class-get-setup-intention.md
+++ b/includes/core/server/request/class-get-setup-intention.md
@@ -13,13 +13,11 @@ There are no additional parameters for this request.
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_get_setup_intent_request`
## Example:
```php
$request = Get_Setup_Intention::create( $id );
-$request->send( 'wcpay_get_setup_intent_request' );
+$request->send();
```
diff --git a/includes/core/server/request/class-get-setup-intention.php b/includes/core/server/request/class-get-setup-intention.php
index 695055dc4e8..3453ae14090 100644
--- a/includes/core/server/request/class-get-setup-intention.php
+++ b/includes/core/server/request/class-get-setup-intention.php
@@ -16,6 +16,14 @@
* Request class for getting setup intents.
*/
class Get_Setup_Intention extends Request {
+
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_get_setup_intent_request';
+
/**
* Sets the intent ID, which will be used in the request URL.
*
diff --git a/includes/core/server/request/class-list-authorizations.md b/includes/core/server/request/class-list-authorizations.md
index 340bd3e1ad3..ba1b070ef65 100644
--- a/includes/core/server/request/class-list-authorizations.md
+++ b/includes/core/server/request/class-list-authorizations.md
@@ -32,5 +32,5 @@ $request->set_page( $page );
$request->set_page_size( $page_size );
$request->set_sort_by( $sort );
$request->set_sort_direction( $direction );
-$request->send( 'wcpay_list_authorizations_request', $request );
+$request->send();
```
diff --git a/includes/core/server/request/class-list-authorizations.php b/includes/core/server/request/class-list-authorizations.php
index e45ee1f55ef..354e3f15850 100644
--- a/includes/core/server/request/class-list-authorizations.php
+++ b/includes/core/server/request/class-list-authorizations.php
@@ -14,6 +14,12 @@
*/
class List_Authorizations extends Paginated {
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_list_authorizations_request';
/**
* Returns the request's API.
diff --git a/includes/core/server/request/class-list-charge-refunds.md b/includes/core/server/request/class-list-charge-refunds.md
index 34fdd8a4753..485f808d4af 100644
--- a/includes/core/server/request/class-list-charge-refunds.md
+++ b/includes/core/server/request/class-list-charge-refunds.md
@@ -17,8 +17,6 @@ The `WCPay\Core\Server\Request\List_Charge_Refunds` class is used to construct t
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_list_charge_refunds_request`
- Arguments: None
@@ -28,5 +26,5 @@ When using this request, provide the following filter and arguments:
$request = List_Charge_Refunds::create();
$request->set_charge( 'ch_id' );
$request->set_limit( 100 ); // It is not required. You can also skip this setter.
-$request->send( 'wcpay_refund_charge_request' );
+$request->send();
```
diff --git a/includes/core/server/request/class-list-charge-refunds.php b/includes/core/server/request/class-list-charge-refunds.php
index 83fdbb68949..e35980f76b7 100644
--- a/includes/core/server/request/class-list-charge-refunds.php
+++ b/includes/core/server/request/class-list-charge-refunds.php
@@ -24,6 +24,13 @@ class List_Charge_Refunds extends Request {
const REQUIRED_PARAMS = [ 'charge' ];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_list_charge_refunds_request';
+
/**
* Sets the charge ID, which will be used in the request URL.
*
diff --git a/includes/core/server/request/class-list-deposits.md b/includes/core/server/request/class-list-deposits.md
index 44cdd65842c..9e25eacc72c 100644
--- a/includes/core/server/request/class-list-deposits.md
+++ b/includes/core/server/request/class-list-deposits.md
@@ -40,5 +40,5 @@ $request->set_sort_direction( $direction );
$request->set_status_is( $status_is );
$request->set_status_is_not( $status_is_not );
$request->set_store_currency_is( $store_currency_is );
-$request->send( 'wcpay_list_deposits_request', $request );
+$request->send();
```
diff --git a/includes/core/server/request/class-list-deposits.php b/includes/core/server/request/class-list-deposits.php
index 2e47b9a18e3..4fea0e930e4 100644
--- a/includes/core/server/request/class-list-deposits.php
+++ b/includes/core/server/request/class-list-deposits.php
@@ -17,6 +17,13 @@ class List_Deposits extends Paginated {
use Date_Parameters;
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_list_deposits_request';
+
/**
* Returns the request's API.
*
diff --git a/includes/core/server/request/class-list-disputes.md b/includes/core/server/request/class-list-disputes.md
index 11eeb1a8a3f..83e720fda2a 100644
--- a/includes/core/server/request/class-list-disputes.md
+++ b/includes/core/server/request/class-list-disputes.md
@@ -27,8 +27,6 @@ The `WCPay\Core\Server\Request\List_Disputes` class is used to construct the req
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_list_disputes_request`
- Arguments: `$request`
@@ -48,5 +46,5 @@ $request->set_sort_by( $sort );
$request->set_sort_direction( $direction );
$request->set_status_is( $status_is );
$request->set_status_is_not( $status_is_not );
-$request->send( 'wcpay_list_disputes_request', $request );
+$request->send();
```
diff --git a/includes/core/server/request/class-list-disputes.php b/includes/core/server/request/class-list-disputes.php
index e347d922c6b..721e35bac65 100644
--- a/includes/core/server/request/class-list-disputes.php
+++ b/includes/core/server/request/class-list-disputes.php
@@ -22,6 +22,13 @@ class List_Disputes extends Paginated {
use Order_Info;
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_list_disputes_request';
+
/**
* Get api URI.
*
diff --git a/includes/core/server/request/class-list-documents.md b/includes/core/server/request/class-list-documents.md
index 3387117cb70..207a2eb00fd 100644
--- a/includes/core/server/request/class-list-documents.md
+++ b/includes/core/server/request/class-list-documents.md
@@ -38,5 +38,5 @@ $request->set_sort_by( $sort );
$request->set_sort_direction( $direction );
$request->set_type_is( $type_is );
$request->set_type_is_not( $type_is_not );
-$request->send( 'wcpay_list_documents_request', $request );
+$request->send();
```
diff --git a/includes/core/server/request/class-list-documents.php b/includes/core/server/request/class-list-documents.php
index 881379e3849..2b602359ab7 100644
--- a/includes/core/server/request/class-list-documents.php
+++ b/includes/core/server/request/class-list-documents.php
@@ -17,6 +17,13 @@ class List_Documents extends Paginated {
use Date_Parameters;
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_list_documents_request';
+
/**
* Returns the request's API.
*
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 d7e5b985710..7e7b48d49f0 100644
--- a/includes/core/server/request/class-list-fraud-outcome-transactions.md
+++ b/includes/core/server/request/class-list-fraud-outcome-transactions.md
@@ -39,5 +39,5 @@ $request->set_search( $search );
$request->set_search_term( $search_term );
$request->set_sort_by( $sort );
$request->set_sort_direction( $direction );
-$request->send( 'wcpay_list_transactions_request', $request );
+$request->send();
```
diff --git a/includes/core/server/request/class-list-transactions.md b/includes/core/server/request/class-list-transactions.md
index 73f60f12bc3..e3f0fb595b1 100644
--- a/includes/core/server/request/class-list-transactions.md
+++ b/includes/core/server/request/class-list-transactions.md
@@ -30,8 +30,6 @@ The `WCPay\Core\Server\Request\List_Transactions` class is used to construct the
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_list_transactions_request`
- Arguments: `$request`
@@ -54,5 +52,5 @@ $request->set_type_is( $type_is );
$request->set_type_is_not( $type_is_not );
$request->set_source_device_is( $source_device_is );
$request->set_source_device_is_not( $source_device_is_not );
-$request->send( 'wcpay_list_transactions_request', $request );
+$request->send();
```
diff --git a/includes/core/server/request/class-list-transactions.php b/includes/core/server/request/class-list-transactions.php
index 88607206c4a..ef340c37d9f 100644
--- a/includes/core/server/request/class-list-transactions.php
+++ b/includes/core/server/request/class-list-transactions.php
@@ -27,6 +27,13 @@ class List_Transactions extends Paginated {
'direction' => 'desc',
];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_list_transactions_request';
+
/**
* Set deposit id.
*
diff --git a/includes/core/server/request/class-refund-charge.md b/includes/core/server/request/class-refund-charge.md
index 0a25fa419aa..7ea32059739 100644
--- a/includes/core/server/request/class-refund-charge.md
+++ b/includes/core/server/request/class-refund-charge.md
@@ -17,8 +17,6 @@ The `WCPay\Core\Server\Request\Refund_Charge` class is used to construct the req
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_refund_charge_request`
- Arguments: None
@@ -28,5 +26,5 @@ When using this request, provide the following filter and arguments:
$request = Refund_Charge::create( );
$request->set_charge( 'ch_xxxxx' );
$request->set_amount( 100 ); // It is not required. You can also skip this setter.
-$request->send( 'wcpay_refund_charge_request' );
+$request->send();
```
diff --git a/includes/core/server/request/class-refund-charge.php b/includes/core/server/request/class-refund-charge.php
index 4d82557a5ee..d792c942e53 100644
--- a/includes/core/server/request/class-refund-charge.php
+++ b/includes/core/server/request/class-refund-charge.php
@@ -25,6 +25,13 @@ class Refund_Charge extends Request {
const REQUIRED_PARAMS = [ 'charge' ];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_refund_charge_request';
+
/**
* Sets the charge ID, which will be used in the request URL.
*
diff --git a/includes/core/server/request/class-update-account.md b/includes/core/server/request/class-update-account.md
index 8d58568285f..759fed59df2 100644
--- a/includes/core/server/request/class-update-account.md
+++ b/includes/core/server/request/class-update-account.md
@@ -52,5 +52,5 @@ $request->set_deposit_schedule_weekly_anchor( $deposit_schedule_weekly_anchor );
$request->set_deposit_schedule_monthly_anchor( $deposit_schedule_monthly_anchor );
$request->set_locale( $locale );
-$request->send( 'wcpay_update_account_settings' );
+$request->send();
```
diff --git a/includes/core/server/request/class-update-account.php b/includes/core/server/request/class-update-account.php
index db63a5eda24..8a0eef57b7b 100644
--- a/includes/core/server/request/class-update-account.php
+++ b/includes/core/server/request/class-update-account.php
@@ -15,6 +15,13 @@
* Request class for updating account.
*/
class Update_Account extends Request {
+
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_update_account_settings';
/**
* Returns the request's API.
*
diff --git a/includes/core/server/request/class-update-intention.md b/includes/core/server/request/class-update-intention.md
index 3fffa1c1f4e..cdc37f87aca 100644
--- a/includes/core/server/request/class-update-intention.md
+++ b/includes/core/server/request/class-update-intention.md
@@ -24,8 +24,6 @@ When creating `Update_Intention` requests, the item ID must be provided to the `
## Filter
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_update_intention_request`
- Arguments: `WC_Order $order`
@@ -42,5 +40,6 @@ $request->set_payment_country( $payment_country );
$request->set_payment_method_options( $payment_method_options );
$request->set_payment_method_types( $payment_methods );
$request->setup_future_usage();
-$request->send( 'wcpay_update_intention_request', $order );
+$request->set_hook_args( $order );
+$request->send();
```
diff --git a/includes/core/server/request/class-update-intention.php b/includes/core/server/request/class-update-intention.php
index 9a817e0e85e..55807b29b3c 100644
--- a/includes/core/server/request/class-update-intention.php
+++ b/includes/core/server/request/class-update-intention.php
@@ -25,6 +25,13 @@ class Update_Intention extends Request {
'metadata' => [],
];
+ /**
+ * Specifies the WordPress hook name that will be triggered upon calling the send() method.
+ *
+ * @var string
+ */
+ protected $hook = 'wcpay_update_intention_request';
+
/**
* Sets the intent ID, which will be used in the request URL.
*
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 11e17217697..225cf21e01c 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
@@ -54,5 +54,6 @@ $request->set_payment_method( $payment_method_id );
$request->set_payment_method_types( $payment_methods );
$request->set_payment_methods( $payment_methods );
$request->setup_future_usage();
-$request->send( 'wcpay_create_and_confirm_intent_request', $payment_information );
+$request->set_hook_args( $payment_information );
+$request->send();
```
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 b70c8da472e..04904419e1a 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
@@ -20,9 +20,6 @@ The `WCPay\Core\Server\Request\WooPay_Create_and_Confirm_Setup_Intention` class
## Filter
-
-When using this request, provide the following filter and arguments:
-
- Name: `wcpay_create_and_confirm_setup_intent_request`
- Arguments:
- `WCPay\Payment_Information $payment_information`
@@ -39,5 +36,7 @@ $request->set_metadata( $metadata );
$request->set_payment_method( $payment_method_id );
$request->set_save_in_platform_account( $save );
$request->set_save_payment_method_to_platform( $save );
-$request->send( 'wcpay_create_and_confirm_setup_intent_request', $payment_information, $save_in_platform_account, $save_payment_method_to_platform );
+$request->assign_hook( 'wcpay_create_and_confirm_setup_intent_request' );
+$request->set_hook_args( $payment_information, $save_in_platform_account, $save_payment_method_to_platform );
+$request->send();
```
diff --git a/includes/core/server/request/class-woopay-create-intent.md b/includes/core/server/request/class-woopay-create-intent.md
index f1629c556d0..0ae42ef421f 100644
--- a/includes/core/server/request/class-woopay-create-intent.md
+++ b/includes/core/server/request/class-woopay-create-intent.md
@@ -44,5 +44,6 @@ $request->set_metadata( $metadata );
$request->set_payment_method( $payment_method_id );
$request->set_payment_method_types( $payment_methods );
$request->set_save_payment_method_to_platform( $toggle );
-$request->send( 'wcpay_create_intent_request', $order );
+$request->set_hook_args( $order );
+$request->send();
```
diff --git a/includes/fraud-prevention/class-fraud-risk-tools.php b/includes/fraud-prevention/class-fraud-risk-tools.php
index bc363da284b..133ecf04cbe 100644
--- a/includes/fraud-prevention/class-fraud-risk-tools.php
+++ b/includes/fraud-prevention/class-fraud-risk-tools.php
@@ -48,6 +48,7 @@ class Fraud_Risk_Tools {
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self( WC_Payments::get_account_service() );
+ self::$instance->init_hooks();
}
return self::$instance;
}
@@ -66,6 +67,14 @@ public static function instance() {
*/
public function __construct( WC_Payments_Account $payments_account ) {
$this->payments_account = $payments_account;
+ }
+
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) {
add_action( 'admin_menu', [ $this, 'init_advanced_settings_page' ] );
}
diff --git a/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php b/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php
index 28dd3e0d3d2..5b498ec90c8 100644
--- a/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php
+++ b/includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php
@@ -30,11 +30,18 @@ class Order_Fraud_And_Risk_Meta_Box {
* @param WC_Payments_Order_Service $order_service The order service.
*/
public function __construct( WC_Payments_Order_Service $order_service ) {
- add_action( 'add_meta_boxes', [ $this, 'maybe_add_meta_box' ] );
-
$this->order_service = $order_service;
}
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
+ add_action( 'add_meta_boxes', [ $this, 'maybe_add_meta_box' ] );
+ }
+
/**
* Maybe add the meta box.
*/
diff --git a/includes/multi-currency/AdminNotices.php b/includes/multi-currency/AdminNotices.php
index 49612a450f4..cd9055fa2fe 100644
--- a/includes/multi-currency/AdminNotices.php
+++ b/includes/multi-currency/AdminNotices.php
@@ -21,9 +21,11 @@ class AdminNotices {
private $notices = [];
/**
- * Constructor
+ * Initializes this class' WP hooks.
+ *
+ * @return void
*/
- public function __construct() {
+ public function init_hooks() {
add_action( 'admin_notices', [ $this, 'admin_notices' ] );
add_action( 'wp_loaded', [ $this, 'hide_notices' ] );
}
diff --git a/includes/multi-currency/Analytics.php b/includes/multi-currency/Analytics.php
index 949c2cd0632..d2c05dbd2df 100644
--- a/includes/multi-currency/Analytics.php
+++ b/includes/multi-currency/Analytics.php
@@ -28,7 +28,6 @@ class Analytics {
const SUPPORTED_CONTEXTS = [ 'orders', 'products', 'variations', 'categories', 'coupons', 'taxes' ];
-
/**
* SQL string replacements made by the analytics Multi-Currency extension.
*
@@ -120,6 +119,7 @@ public function register_customer_currencies() {
$currency_options = [];
$default_currency = $this->multi_currency->get_default_currency();
+
// Add default currency to the list if it does not exist.
if ( ! in_array( $default_currency->get_code(), $currencies, true ) ) {
$currencies[] = $default_currency->get_code();
@@ -136,10 +136,8 @@ public function register_customer_currencies() {
'value' => $currency_details->get_code(),
];
}
- $data_registry = Package::container()->get(
- AssetDataRegistry::class
- );
+ $data_registry = Package::container()->get( AssetDataRegistry::class );
$data_registry->add( 'customerCurrencies', $currency_options, true );
}
diff --git a/includes/multi-currency/BackendCurrencies.php b/includes/multi-currency/BackendCurrencies.php
index ccdf1636841..9ea009ac4c4 100644
--- a/includes/multi-currency/BackendCurrencies.php
+++ b/includes/multi-currency/BackendCurrencies.php
@@ -45,7 +45,14 @@ class BackendCurrencies {
public function __construct( MultiCurrency $multi_currency, WC_Payments_Localization_Service $localization_service ) {
$this->multi_currency = $multi_currency;
$this->localization_service = $localization_service;
+ }
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
// Skip if no additional currencies are enabled.
if ( ! $this->multi_currency->has_additional_currencies_enabled() ) {
return;
@@ -60,7 +67,6 @@ public function __construct( MultiCurrency $multi_currency, WC_Payments_Localiza
// Currency hooks. Be aware that this should not run after Explicit Price hook, its priority should be less
// than explicit price hooks to run before them.
add_filter( 'wc_price_args', [ $this, 'build_wc_price_args' ], 50 );
-
}
}
diff --git a/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php b/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
index 3ddf98206c0..c716e985acc 100644
--- a/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
+++ b/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
@@ -33,6 +33,10 @@ protected function init() {
add_action( 'woocommerce_add_cart_item_data', [ $this, 'add_initial_currency' ], 20, 3 );
add_filter( 'woocommerce_get_cart_item_from_session', [ $this, 'convert_cart_currency' ], 20, 2 );
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 );
+
+ // Convert cart editing price.
+ add_filter( 'wc_nyp_edit_in_cart_args', [ $this, 'edit_in_cart_args' ], 10, 2 );
+ add_filter( 'wc_nyp_get_initial_price', [ $this, 'get_initial_price' ], 10, 3 );
}
}
@@ -92,20 +96,13 @@ public function convert_cart_currency( $cart_item, $values ) {
$cart_item['nyp'] = $cart_item['nyp_original'];
} else {
- $nyp_currency = $this->multi_currency->get_enabled_currencies()[ $cart_item['nyp_currency'] ] ?? null;
-
- // Convert entered price back to default currency.
- $converted_price = ( (float) $cart_item['nyp_original'] ) / $nyp_currency->get_rate();
+ $from_currency = $cart_item['nyp_currency'];
+ $raw_price = $cart_item['nyp_original'];
- if ( ! $selected_currency->get_is_default() ) {
- $converted_price = $this->multi_currency->get_price( $converted_price, 'product' );
- }
-
- $cart_item['nyp'] = $converted_price;
+ $cart_item['nyp'] = $this->multi_currency->get_raw_conversion( $raw_price, $selected_currency->get_code(), $from_currency );
}
$cart_item = WC_Name_Your_Price()->cart->set_cart_item( $cart_item );
-
}
return $cart_item;
@@ -140,4 +137,43 @@ public function should_convert_product_price( bool $return, $product ): bool {
return $return;
}
+
+ /**
+ * Add currency to cart edit link.
+ *
+ * @param array $args The cart args.
+ * @param array $cart_item The current cart item.
+ *
+ * @return array
+ */
+ public function edit_in_cart_args( $args, $cart_item ) {
+ $args['nyp_currency'] = $this->multi_currency->get_selected_currency()->get_code();
+ return $args;
+ }
+
+ /**
+ * Maybe convert any prices being edited from the cart
+ *
+ * @param string $initial_price The initial price.
+ * @param mixed $product The product being queried.
+ * @param string $suffix The suffix needed for composites and bundles.
+ *
+ * @return float|string
+ */
+ public function get_initial_price( $initial_price, $product, $suffix ) {
+
+ if ( isset( $_REQUEST[ 'nyp_raw' . $suffix ] ) && isset( $_REQUEST['nyp_currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ $from_currency = wc_clean( wp_unslash( $_REQUEST['nyp_currency'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $raw_price = (float) wc_clean( wp_unslash( $_REQUEST[ 'nyp_raw' . $suffix ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ $selected_currency = $this->multi_currency->get_selected_currency();
+
+ if ( $from_currency !== $selected_currency->get_code() ) {
+ $initial_price = $this->multi_currency->get_raw_conversion( $raw_price, $selected_currency->get_code(), $from_currency );
+ }
+ }
+
+ return $initial_price;
+ }
}
diff --git a/includes/multi-currency/CurrencySwitcherBlock.php b/includes/multi-currency/CurrencySwitcherBlock.php
index 3b498fe7e58..2dd7029386f 100644
--- a/includes/multi-currency/CurrencySwitcherBlock.php
+++ b/includes/multi-currency/CurrencySwitcherBlock.php
@@ -42,7 +42,14 @@ class CurrencySwitcherBlock {
public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) {
$this->multi_currency = $multi_currency;
$this->compatibility = $compatibility;
+ }
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_action( 'init', [ $this, 'init_block_widget' ] );
}
diff --git a/includes/multi-currency/FrontendCurrencies.php b/includes/multi-currency/FrontendCurrencies.php
index 13b86f22b7f..7a75254d854 100644
--- a/includes/multi-currency/FrontendCurrencies.php
+++ b/includes/multi-currency/FrontendCurrencies.php
@@ -99,7 +99,14 @@ public function __construct( MultiCurrency $multi_currency, WC_Payments_Localiza
$this->localization_service = $localization_service;
$this->utils = $utils;
$this->compatibility = $compatibility;
+ }
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
if ( ! is_admin() && ! defined( 'DOING_CRON' ) && ! Utils::is_admin_api_request() ) {
// Currency hooks.
add_filter( 'woocommerce_currency', [ $this, 'get_woocommerce_currency' ], 900 );
diff --git a/includes/multi-currency/FrontendPrices.php b/includes/multi-currency/FrontendPrices.php
index 0c9dbefa950..f1975387d42 100644
--- a/includes/multi-currency/FrontendPrices.php
+++ b/includes/multi-currency/FrontendPrices.php
@@ -38,7 +38,14 @@ class FrontendPrices {
public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) {
$this->multi_currency = $multi_currency;
$this->compatibility = $compatibility;
+ }
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
if ( ! is_admin() && ! defined( 'DOING_CRON' ) && ! Utils::is_admin_api_request() ) {
// Simple product price hooks.
add_filter( 'woocommerce_product_get_price', [ $this, 'get_product_price_string' ], 99, 2 );
diff --git a/includes/multi-currency/Helpers/OrderMetaHelper.php b/includes/multi-currency/Helpers/OrderMetaHelper.php
index 02ecc812e00..64e53e3c80a 100644
--- a/includes/multi-currency/Helpers/OrderMetaHelper.php
+++ b/includes/multi-currency/Helpers/OrderMetaHelper.php
@@ -39,7 +39,18 @@ class OrderMetaHelper {
*/
public function __construct( WC_Payments_API_Client $payments_api_client ) {
$this->payments_api_client = $payments_api_client;
- $this->add_actions();
+ }
+
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
+ add_action( 'add_meta_boxes', [ $this, 'maybe_add_meta_box' ], 10, 2 );
+ add_action( 'save_post', [ $this, 'maybe_update_exchange_rate' ] );
+ add_action( 'admin_notices', [ $this, 'maybe_output_errors' ] );
+ add_filter( 'get_edit_post_link', [ $this, 'maybe_update_edit_post_link' ] );
}
/**
@@ -325,18 +336,6 @@ public function maybe_update_edit_post_link( $url ): string {
return $url;
}
- /**
- * Adds our actions and hooks.
- *
- * @return void
- */
- private function add_actions() {
- add_action( 'add_meta_boxes', [ $this, 'maybe_add_meta_box' ], 10, 2 );
- add_action( 'save_post', [ $this, 'maybe_update_exchange_rate' ] );
- add_action( 'admin_notices', [ $this, 'maybe_output_errors' ] );
- add_filter( 'get_edit_post_link', [ $this, 'maybe_update_edit_post_link' ] );
- }
-
/**
* Checks to see if the feature is enabled by the request parameter.
*
diff --git a/includes/multi-currency/MultiCurrency.php b/includes/multi-currency/MultiCurrency.php
index d55158c4d80..c94c09a73fc 100644
--- a/includes/multi-currency/MultiCurrency.php
+++ b/includes/multi-currency/MultiCurrency.php
@@ -27,9 +27,10 @@
*/
class MultiCurrency {
- const CURRENCY_SESSION_KEY = 'wcpay_currency';
- const CURRENCY_META_KEY = 'wcpay_currency';
- const FILTER_PREFIX = 'wcpay_multi_currency_';
+ const CURRENCY_SESSION_KEY = 'wcpay_currency';
+ const CURRENCY_META_KEY = 'wcpay_currency';
+ const FILTER_PREFIX = 'wcpay_multi_currency_';
+ const CUSTOMER_CURRENCIES_KEY = 'wcpay_multi_currency_stored_customer_currencies';
/**
* The plugin's ID.
@@ -196,6 +197,7 @@ class MultiCurrency {
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self( WC_Payments::get_payments_api_client(), WC_Payments::get_account_service(), WC_Payments::get_localization_service(), WC_Payments::get_database_cache() );
+ self::$instance->init_hooks();
}
return self::$instance;
}
@@ -219,7 +221,14 @@ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Pay
$this->geolocation = new Geolocation( $this->localization_service );
$this->compatibility = new Compatibility( $this, $this->utils );
$this->currency_switcher_block = new CurrencySwitcherBlock( $this, $this->compatibility );
+ }
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) {
add_filter( 'woocommerce_get_settings_pages', [ $this, 'init_settings_pages' ] );
// Enqueue the scripts after the main WC_Payments_Admin does.
@@ -240,6 +249,8 @@ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Pay
add_action( 'init', [ $this, 'possible_simulation_activation' ], 13 );
add_action( 'woocommerce_created_customer', [ $this, 'set_new_customer_currency_meta' ] );
}
+
+ $this->currency_switcher_block->init_hooks();
}
/**
@@ -265,9 +276,9 @@ public function init() {
$this->update_manual_rate_currencies_notice_option();
}
- new PaymentMethodsCompatibility( $this, WC_Payments::get_gateway() );
- new AdminNotices();
- new UserSettings( $this );
+ $payment_method_compat = new PaymentMethodsCompatibility( $this, WC_Payments::get_gateway() );
+ $admin_notices = new AdminNotices();
+ $user_settings = new UserSettings( $this );
new Analytics( $this );
$this->frontend_prices = new FrontendPrices( $this, $this->compatibility );
@@ -276,6 +287,16 @@ public function init() {
$this->tracking = new Tracking( $this );
$this->order_meta_helper = new OrderMetaHelper( $this->payments_api_client );
+ // Init all of the hooks.
+ $payment_method_compat->init_hooks();
+ $admin_notices->init_hooks();
+ $user_settings->init_hooks();
+ $this->frontend_prices->init_hooks();
+ $this->frontend_currencies->init_hooks();
+ $this->backend_currencies->init_hooks();
+ $this->tracking->init_hooks();
+ $this->order_meta_helper->init_hooks();
+
add_action( 'woocommerce_order_refunded', [ $this, 'add_order_meta_on_refund' ], 50, 2 );
// Check to make sure there are enabled currencies, then for Storefront being active, and then load the integration.
@@ -288,6 +309,9 @@ public function init() {
add_action( 'admin_init', [ __CLASS__, 'add_woo_admin_notes' ] );
}
+ // Update the customer currencies option after an order status change.
+ add_action( 'woocommerce_order_status_changed', [ $this, 'maybe_update_customer_currencies_option' ] );
+
static::$is_initialized = true;
}
@@ -327,9 +351,15 @@ public function init_settings_pages( $settings_pages ): array {
}
if ( $this->payments_account->is_stripe_connected() ) {
- $settings_pages[] = new Settings( $this );
+ $settings = new Settings( $this );
+ $settings->init_hooks();
+
+ $settings_pages[] = $settings;
} else {
- $settings_pages[] = new SettingsOnboardCta( $this );
+ $settings_onboard_cta = new SettingsOnboardCta( $this );
+ $settings_onboard_cta->init_hooks();
+
+ $settings_pages[] = $settings_onboard_cta;
}
return $settings_pages;
@@ -491,9 +521,7 @@ public function get_store_currencies(): array {
public function get_single_currency_settings( string $currency_code ): array {
// Confirm the currency code is valid before trying to get the settings.
if ( ! array_key_exists( strtoupper( $currency_code ), $this->get_available_currencies() ) ) {
- $message = 'Invalid currency passed to get_single_currency_settings: ' . $currency_code;
- Logger::error( $message );
- throw new InvalidCurrencyException( $message, 'wcpay_multi_currency_invalid_currency', 500 );
+ $this->log_and_throw_invalid_currency_exception( __FUNCTION__, $currency_code );
}
$currency_code = strtolower( $currency_code );
@@ -522,9 +550,7 @@ public function get_single_currency_settings( string $currency_code ): array {
public function update_single_currency_settings( string $currency_code, string $exchange_rate_type, float $price_rounding, float $price_charm, $manual_rate = null ) {
// Confirm the currency code is valid before trying to update the settings.
if ( ! array_key_exists( strtoupper( $currency_code ), $this->get_available_currencies() ) ) {
- $message = 'Invalid currency passed to update_single_currency_settings: ' . $currency_code;
- Logger::error( $message );
- throw new InvalidCurrencyException( $message, 'wcpay_multi_currency_invalid_currency', 500 );
+ $this->log_and_throw_invalid_currency_exception( __FUNCTION__, $currency_code );
}
$currency_code = strtolower( $currency_code );
@@ -545,6 +571,32 @@ public function update_single_currency_settings( string $currency_code, string $
}
}
+ /**
+ * Updates the customer currencies option.
+ *
+ * @param int $order_id The order ID.
+ *
+ * @return void
+ */
+ public function maybe_update_customer_currencies_option( $order_id ) {
+ $order = wc_get_order( $order_id );
+
+ if ( ! $order ) {
+ return;
+ }
+
+ $currency = strtoupper( $order->get_currency() );
+ $currencies = self::get_all_customer_currencies();
+
+ // Skip if the currency is already in the list.
+ if ( in_array( $currency, $currencies, true ) ) {
+ return;
+ }
+
+ $currencies[] = $currency;
+ update_option( self::CUSTOMER_CURRENCIES_KEY, $currencies );
+ }
+
/**
* Sets up the available currencies, which are alphabetical by name.
*
@@ -690,9 +742,7 @@ public function set_enabled_currencies( $currencies = [] ) {
// Confirm the currencies submitted are available/valid currencies.
$invalid_currencies = array_diff( $currencies, array_keys( $this->get_available_currencies() ) );
if ( 0 < count( $invalid_currencies ) ) {
- $message = 'Invalid currency/currencies passed to set_enabled_currencies: ' . implode( ', ', $invalid_currencies );
- Logger::error( $message );
- throw new InvalidCurrencyException( $message, 'wcpay_multi_currency_invalid_currency', 500 );
+ $this->log_and_throw_invalid_currency_exception( __FUNCTION__, implode( ', ', $invalid_currencies ) );
}
// Get the currencies that were removed before they are updated.
@@ -845,6 +895,52 @@ public function get_price( $price, string $type ): float {
return $this->get_adjusted_price( $converted_price, $apply_charm_pricing, $currency );
}
+ /**
+ * Gets a raw converted amount based on the amount and currency codes passed.
+ * This is a helper method for external conversions, if needed.
+ *
+ * @param float $amount The amount to be converted.
+ * @param string $to_currency The 3 letter currency code to convert the amount to.
+ * @param string $from_currency The 3 letter currency code to convert the amount from.
+ *
+ * @return float The converted amount.
+ *
+ * @throws InvalidCurrencyException
+ * @throws InvalidCurrencyRateException
+ */
+ public function get_raw_conversion( float $amount, string $to_currency, string $from_currency = '' ): float {
+ $enabled_currencies = $this->get_enabled_currencies();
+
+ // If the from_currency is not set, use the store currency.
+ if ( '' === $from_currency ) {
+ $from_currency = $this->get_default_currency()->get_code();
+ }
+
+ // We throw an exception if either of the currencies are not enabled.
+ $to_currency = strtoupper( $to_currency );
+ $from_currency = strtoupper( $from_currency );
+ foreach ( [ $to_currency, $from_currency ] as $code ) {
+ if ( ! isset( $enabled_currencies[ $code ] ) ) {
+ $this->log_and_throw_invalid_currency_exception( __FUNCTION__, $code );
+ }
+ }
+
+ // Get the rates.
+ $to_currency_rate = $enabled_currencies[ $to_currency ]->get_rate();
+ $from_currency_rate = $enabled_currencies[ $from_currency ]->get_rate();
+
+ // Throw an exception in case from_currency_rate is less than or equal to 0.
+ if ( 0 >= $from_currency_rate ) {
+ $message = 'Invalid rate for from_currency in get_raw_conversion: ' . $from_currency_rate;
+ Logger::error( $message );
+ throw new InvalidCurrencyRateException( $message, 'wcpay_multi_currency_invalid_currency_rate', 500 );
+ }
+
+ $amount = $amount * ( $to_currency_rate / $from_currency_rate );
+
+ return (float) $amount;
+ }
+
/**
* Recalculates WooCommerce cart totals.
*
@@ -1447,13 +1543,19 @@ public function is_multi_currency_settings_page(): bool {
}
/**
- * Function used to compute the customer used currencies, used as internal callable for get_all_customer_currencies function.
+ * Get all the currencies that have been used in the store.
*
* @return array
*/
- public function callable_get_customer_currencies() {
+ public function get_all_customer_currencies(): array {
global $wpdb;
+ $currencies = get_option( self::CUSTOMER_CURRENCIES_KEY );
+
+ if ( self::is_customer_currencies_data_valid( $currencies ) ) {
+ return array_map( 'strtoupper', $currencies );
+ }
+
$currencies = $this->get_available_currencies();
$query_union = [];
@@ -1481,32 +1583,9 @@ public function callable_get_customer_currencies() {
$query = "SELECT currency_code FROM ( $sub_query ) as subquery WHERE subquery.exists_in_orders=1 ORDER BY currency_code ASC";
$currencies = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
- return [
- 'currencies' => $currencies,
- 'updated' => time(),
- ];
- }
-
- /**
- * Get all the currencies that have been used in the store.
- *
- * @return array
- */
- public function get_all_customer_currencies(): array {
-
- $data = $this->database_cache->get_or_add(
- Database_Cache::CUSTOMER_CURRENCIES_KEY,
- [ $this, 'callable_get_customer_currencies' ],
- function ( $data ) {
- // Return true if the data looks valid and was updated an hour or less ago.
- return is_array( $data ) &&
- isset( $data['currencies'], $data['updated'] ) &&
- $data['updated'] >= ( time() - ( 5 * MINUTE_IN_SECONDS ) );
- }
- );
-
- if ( ! empty( $data['currencies'] ) && is_array( $data['currencies'] ) ) {
- return $data['currencies'];
+ if ( self::is_customer_currencies_data_valid( $currencies ) ) {
+ update_option( self::CUSTOMER_CURRENCIES_KEY, $currencies );
+ return array_map( 'strtoupper', $currencies );
}
return [];
@@ -1530,4 +1609,30 @@ public function has_additional_currencies_enabled(): bool {
public static function is_initialized() : bool {
return static::$is_initialized;
}
+
+ /**
+ * Logs a message and throws InvalidCurrencyException.
+ *
+ * @param string $method The method that's actually throwing the exception.
+ * @param string $currency_code The currency code that was invalid.
+ * @param int $code The exception code.
+ *
+ * @throws InvalidCurrencyException
+ */
+ private function log_and_throw_invalid_currency_exception( $method, $currency_code, $code = 500 ) {
+ $message = 'Invalid currency passed to ' . $method . ': ' . $currency_code;
+ Logger::error( $message );
+ throw new InvalidCurrencyException( $message, 'wcpay_multi_currency_invalid_currency', $code );
+ }
+
+ /**
+ * Checks if the customer currencies data is valid.
+ *
+ * @param mixed $currencies The currencies to check.
+ *
+ * @return bool
+ */
+ private function is_customer_currencies_data_valid( $currencies ) {
+ return ! empty( $currencies ) && is_array( $currencies );
+ }
}
diff --git a/includes/multi-currency/PaymentMethodsCompatibility.php b/includes/multi-currency/PaymentMethodsCompatibility.php
index 864e533c600..7b75d81c4c7 100644
--- a/includes/multi-currency/PaymentMethodsCompatibility.php
+++ b/includes/multi-currency/PaymentMethodsCompatibility.php
@@ -40,7 +40,14 @@ class PaymentMethodsCompatibility {
public function __construct( MultiCurrency $multi_currency, WC_Payment_Gateway_WCPay $gateway ) {
$this->multi_currency = $multi_currency;
$this->gateway = $gateway;
+ }
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_action(
'update_option_woocommerce_woocommerce_payments_settings',
[ $this, 'add_missing_currencies' ]
diff --git a/includes/multi-currency/Settings.php b/includes/multi-currency/Settings.php
index e4309c8690e..11c74d56619 100644
--- a/includes/multi-currency/Settings.php
+++ b/includes/multi-currency/Settings.php
@@ -45,11 +45,18 @@ public function __construct( MultiCurrency $multi_currency ) {
$this->id = $this->multi_currency->id;
$this->label = _x( 'Multi-currency', 'Settings tab label', 'woocommerce-payments' );
+ parent::__construct();
+ }
+
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
// TODO: Only register emoji script in settings page. Until WC Admin decide if they will enable it too: https://github.com/woocommerce/woocommerce-admin/issues/6388.
add_action( 'admin_print_scripts', [ $this, 'maybe_add_print_emoji_detection_script' ] );
add_action( 'woocommerce_admin_field_wcpay_multi_currency_settings_page', [ $this, 'wcpay_multi_currency_settings_page' ] );
-
- parent::__construct();
}
/**
diff --git a/includes/multi-currency/SettingsOnboardCta.php b/includes/multi-currency/SettingsOnboardCta.php
index dbe7fa15fb7..574cd8fdd7e 100644
--- a/includes/multi-currency/SettingsOnboardCta.php
+++ b/includes/multi-currency/SettingsOnboardCta.php
@@ -37,11 +37,18 @@ public function __construct( MultiCurrency $multi_currency ) {
$this->id = $this->multi_currency->id;
$this->label = _x( 'Multi-currency', 'Settings tab label', 'woocommerce-payments' );
- add_action( 'woocommerce_admin_field_wcpay_currencies_settings_onboarding_cta', [ $this, 'currencies_settings_onboarding_cta' ] );
-
parent::__construct();
}
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
+ add_action( 'woocommerce_admin_field_wcpay_currencies_settings_onboarding_cta', [ $this, 'currencies_settings_onboarding_cta' ] );
+ }
+
/**
* Output the call to action button if needing to onboard.
*/
@@ -76,6 +83,10 @@ public function currencies_settings_onboarding_cta() {
* @return array
*/
public function get_settings( $current_section = '' ) {
+ // Hide the save button because there are no settings to save.
+ global $hide_save_button;
+ $hide_save_button = true;
+
return [
[
'title' => __( 'Enabled currencies', 'woocommerce-payments' ),
@@ -90,6 +101,10 @@ public function get_settings( $current_section = '' ) {
[
'type' => 'wcpay_currencies_settings_onboarding_cta',
],
+ [
+ 'type' => 'sectionend',
+ 'id' => $this->id . '_enabled_currencies',
+ ],
];
}
}
diff --git a/includes/multi-currency/Tracking.php b/includes/multi-currency/Tracking.php
index d90f64ca913..c34d3ffc47e 100644
--- a/includes/multi-currency/Tracking.php
+++ b/includes/multi-currency/Tracking.php
@@ -27,7 +27,14 @@ class Tracking {
*/
public function __construct( MultiCurrency $multi_currency ) {
$this->multi_currency = $multi_currency;
+ }
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_filter( 'woocommerce_tracker_data', [ $this, 'add_tracker_data' ], 50 );
}
diff --git a/includes/multi-currency/UserSettings.php b/includes/multi-currency/UserSettings.php
index be33c2d3b71..185c715e5b3 100644
--- a/includes/multi-currency/UserSettings.php
+++ b/includes/multi-currency/UserSettings.php
@@ -28,7 +28,14 @@ class UserSettings {
*/
public function __construct( MultiCurrency $multi_currency ) {
$this->multi_currency = $multi_currency;
+ }
+ /**
+ * Initializes this class' WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
// Only show currency selector if more than one currency is enabled.
if ( 1 < count( $this->multi_currency->get_enabled_currencies() ) ) {
add_action( 'woocommerce_edit_account_form', [ $this, 'add_presentment_currency_switch' ] );
diff --git a/includes/multi-currency/wc-payments-multi-currency.php b/includes/multi-currency/wc-payments-multi-currency.php
index 10f5852d086..31017f9ff35 100644
--- a/includes/multi-currency/wc-payments-multi-currency.php
+++ b/includes/multi-currency/wc-payments-multi-currency.php
@@ -7,6 +7,25 @@
defined( 'ABSPATH' ) || exit;
+/**
+ * Load customer multi-currency if feature is enabled or if it is the setup page.
+ */
+function wcpay_multi_currency_onboarding_check() {
+ $is_setup_page = false;
+
+ // Skip checking the HTTP referer if it is a cron job.
+ if ( ! defined( 'DOING_CRON' ) ) {
+ $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) );
+ $is_setup_page = 0 < strpos( $http_referer, 'multi-currency-setup' );
+ }
+
+ return $is_setup_page;
+}
+
+if ( ! WC_Payments_Features::is_customer_multi_currency_enabled() && ! wcpay_multi_currency_onboarding_check() ) {
+ return;
+}
+
/**
* Returns the main instance of MultiCurrency.
*
diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php
index 1d6381d5bbb..aa8dec6f743 100644
--- a/includes/payment-methods/class-afterpay-payment-method.php
+++ b/includes/payment-methods/class-afterpay-payment-method.php
@@ -45,8 +45,8 @@ public function __construct( $token_service ) {
], // Represents NZD 1 - 2,000 NZD.
'GBP' => [
'min' => 100,
- 'max' => 100000,
- ], // Represents GBP 1 - 1,000 GBP.
+ 'max' => 120000,
+ ], // Represents GBP 1 - 1,200 GBP.
'USD' => [
'min' => 100,
'max' => 400000,
diff --git a/includes/payment-methods/class-jcb-payment-method.php b/includes/payment-methods/class-jcb-payment-method.php
deleted file mode 100644
index 7a70a6c8a2c..00000000000
--- a/includes/payment-methods/class-jcb-payment-method.php
+++ /dev/null
@@ -1,42 +0,0 @@
-stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
- $this->title = 'JCB';
- $this->is_reusable = false;
- $this->currencies = [ 'JPY' ];
- $this->icon_url = plugins_url( 'assets/images/payment-methods/sofort.svg', WCPAY_PLUGIN_FILE );
- }
-
- /**
- * Returns testing credentials to be printed at checkout in test mode.
- *
- * @return string
- */
- public function get_testing_instructions() {
- return '';
- }
-}
diff --git a/includes/payment-methods/class-klarna-payment-method.php b/includes/payment-methods/class-klarna-payment-method.php
new file mode 100644
index 00000000000..5e7823960df
--- /dev/null
+++ b/includes/payment-methods/class-klarna-payment-method.php
@@ -0,0 +1,59 @@
+stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
+ $this->title = __( 'Klarna', 'woocommerce-payments' );
+ $this->is_reusable = false;
+ $this->icon_url = plugins_url( 'assets/images/payment-methods/klarna.svg', WCPAY_PLUGIN_FILE );
+ $this->currencies = [ 'USD', 'GBP', 'EUR' ];
+ $this->accept_only_domestic_payment = true;
+ $this->countries = [ 'US', 'GB', 'AT', 'DE', 'NL', 'BE', 'ES', 'IT' ];
+ $this->limits_per_currency = [
+ 'USD' => [
+ 'min' => 1000,
+ 'max' => 500000,
+ ], // Represents USD 10 - 5,000 AUD.
+ 'GBP' => [
+ 'min' => 1000,
+ 'max' => 500000,
+ ], // Represents GBP 10 - 5,000 AUD.
+ 'EUR' => [
+ 'min' => 1000,
+ 'max' => 500000,
+ ], // Represents EUR 10 - 5,000 AUD.
+ ];
+ }
+
+ /**
+ * Returns testing credentials to be printed at checkout in test mode.
+ *
+ * @return string
+ */
+ public function get_testing_instructions() {
+ return '';
+ }
+}
diff --git a/includes/payment-methods/class-upe-payment-gateway.php b/includes/payment-methods/class-upe-payment-gateway.php
index 339a90f91b1..42fc2deafe1 100644
--- a/includes/payment-methods/class-upe-payment-gateway.php
+++ b/includes/payment-methods/class-upe-payment-gateway.php
@@ -240,6 +240,7 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null
$request->set_level3( $this->get_level3_data_from_order( $order ) );
$request->set_payment_method_types( $payment_methods );
$request->set_fingerprint( $fingerprint );
+ $request->set_hook_args( $order, $payment_intent_id );
if ( $payment_country ) {
$request->set_payment_country( $payment_country );
}
@@ -250,7 +251,7 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null
$request->set_customer( $customer_id );
}
- $updated_payment_intent = $request->send( 'wcpay_update_intention_request', $order, $payment_intent_id );
+ $updated_payment_intent = $request->send();
// Attach the intent and exchange info to the order before doing the redirect,
// so that when processing redirect, the up-to-date intent information is available.
@@ -357,7 +358,8 @@ public function create_payment_intent( $displayed_payment_methods, $order_id = n
$request->set_metadata( $metadata );
$request->set_capture_method( $manual_capture );
$request->set_fingerprint( $fingerprint );
- $payment_intent = $request->send( 'wcpay_create_intent_request', $order );
+ $request->set_hook_args( $order );
+ $payment_intent = $request->send();
} catch ( Amount_Too_Small_Exception $e ) {
$minimum_amount = $e->get_minimum_amount();
@@ -371,7 +373,7 @@ public function create_payment_intent( $displayed_payment_methods, $order_id = n
* amount for the API.
*/
$request->set_amount( $minimum_amount );
- $payment_intent = $request->send( 'wcpay_create_intent_request', $order );
+ $payment_intent = $request->send();
}
return [
@@ -438,7 +440,7 @@ public function create_setup_intent( $displayed_payment_methods ) {
$request->set_customer( $customer_id );
$request->set_payment_method_types( array_values( $displayed_payment_methods ) );
/** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $setup_intent = $request->send( 'wcpay_create_setup_intention_request' );
+ $setup_intent = $request->send();
return [
'id' => $setup_intent->get_id(),
@@ -546,6 +548,7 @@ public function process_payment( $order_id ) {
$request->set_metadata( $this->get_metadata_from_order( $order, $payment_type ) );
$request->set_level3( $this->get_level3_data_from_order( $order ) );
$request->set_payment_method_types( $payment_methods );
+ $request->set_hook_args( $order, $payment_intent_id );
if ( $payment_country ) {
$request->set_payment_country( $payment_country );
}
@@ -560,7 +563,7 @@ public function process_payment( $order_id ) {
$request->setup_future_usage();
$request->set_payment_method_options( $payment_method_options );
}
- $updated_payment_intent = $request->send( 'wcpay_update_intention_request', $order, $payment_intent_id );
+ $updated_payment_intent = $request->send();
} catch ( Amount_Too_Small_Exception $e ) {
// This code would only be reached if the cache has already expired.
throw new Exception( WC_Payments_Utils::get_filtered_error_message( $e ) );
@@ -757,8 +760,9 @@ public function process_redirect_payment( $order, $intent_id, $save_payment_meth
// Get payment intent to confirm status.
if ( $payment_needed ) {
$request = Get_Intention::create( $intent_id );
+ $request->set_hook_args( $order );
/** @var WC_Payments_API_Payment_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $intent = $request->send();
$client_secret = $intent->get_client_secret();
$status = $intent->get_status();
$charge = $intent->get_charge();
@@ -777,7 +781,7 @@ public function process_redirect_payment( $order, $intent_id, $save_payment_meth
} else {
$request = Get_Setup_Intention::create( $intent_id );
/** @var WC_Payments_API_Setup_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $intent = $request->send( 'wcpay_get_setup_intent_request' );
+ $intent = $request->send();
$client_secret = $intent->get_client_secret();
$status = $intent->get_status();
$charge_id = '';
@@ -933,7 +937,7 @@ public function create_token_from_setup_intent( $setup_intent_id, $user ) {
try {
$setup_intent_request = Get_Setup_Intention::create( $setup_intent_id );
/** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $setup_intent = $setup_intent_request->send( 'wcpay_get_setup_intent_request' );
+ $setup_intent = $setup_intent_request->send();
$payment_method_id = $setup_intent->get_payment_method_id();
// TODO: When adding SEPA and Sofort, we will need a new API call to get the payment method and from there get the type.
@@ -989,6 +993,7 @@ public function get_payment_method_ids_enabled_at_checkout( $order_id = null, $f
$enabled_payment_methods = [];
$active_payment_methods = $this->get_upe_enabled_payment_method_statuses();
+
foreach ( $upe_enabled_payment_methods as $payment_method_id ) {
$payment_method_capability_key = $this->payment_method_capability_key_map[ $payment_method_id ] ?? 'undefined_capability_key';
if ( isset( $this->payment_methods[ $payment_method_id ] ) ) {
@@ -1064,11 +1069,7 @@ public function get_upe_available_payment_methods() {
$available_methods[] = Link_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Affirm_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods[] = Afterpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
-
- // List JCB method for Japanese account only.
- if ( 'JP' === $this->account->get_account_country() ) {
- $available_methods[] = JCB_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
- }
+ $available_methods[] = Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
$available_methods = array_values(
apply_filters(
@@ -1077,21 +1078,9 @@ public function get_upe_available_payment_methods() {
)
);
- $methods_with_fees = array_keys( $this->account->get_fees() );
- $available_methods_with_fees = array_intersect( $available_methods, $methods_with_fees );
-
- // As the JCB is not a real payment method it doesn't have fees.
- // If JCB is still available after the filter, bypass it to the available payment methods with fees.
- if ( in_array(
- JCB_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
- $available_methods,
- true
- )
- ) {
- $available_methods_with_fees[] = JCB_Payment_Method::PAYMENT_METHOD_STRIPE_ID;
- }
+ $methods_with_fees = array_keys( $this->account->get_fees() );
- return array_values( $available_methods_with_fees );
+ return array_values( array_intersect( $available_methods, $methods_with_fees ) );
}
/**
@@ -1278,8 +1267,9 @@ public function log_payment_error_ajax() {
}
// Get charge data from WCPay Server.
- $request = Get_Charge::create( $charge_id );
- $charge_data = $request->send( 'wcpay_get_charge_request', $charge_id );
+ $request = Get_Charge::create( $charge_id );
+ $request->set_hook_args( $charge_id );
+ $charge_data = $request->send();
$order_id = $charge_data['metadata']['order_id'];
// Validate Order ID and proceed with logging errors and updating order status.
@@ -1291,7 +1281,8 @@ public function log_payment_error_ajax() {
$intent_id = $charge_data['payment_intent'] ?? $order->get_meta( '_intent_id' );
$request = Get_Intention::create( $intent_id );
- $intent = $request->send( 'wcpay_get_intent_request', $order );
+ $request->set_hook_args( $order );
+ $intent = $request->send();
$intent_status = $intent->get_status();
$error_message = esc_html( rtrim( $charge_data['failure_message'], '.' ) );
diff --git a/includes/payment-methods/class-upe-split-payment-gateway.php b/includes/payment-methods/class-upe-split-payment-gateway.php
index dc5308fc829..3e7b3090a7a 100644
--- a/includes/payment-methods/class-upe-split-payment-gateway.php
+++ b/includes/payment-methods/class-upe-split-payment-gateway.php
@@ -362,7 +362,7 @@ public function create_token_from_setup_intent( $setup_intent_id, $user ) {
try {
$setup_intent_request = Get_Setup_Intention::create( $setup_intent_id );
/** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- $setup_intent = $setup_intent_request->send( 'wcpay_get_setup_intent_request' );
+ $setup_intent = $setup_intent_request->send();
$payment_method_id = $setup_intent->get_payment_method_id();
// TODO: When adding SEPA and Sofort, we will need a new API call to get the payment method and from there get the type.
@@ -377,6 +377,20 @@ public function create_token_from_setup_intent( $setup_intent_id, $user ) {
}
}
+ /**
+ * Mandate must be shown and acknowledged by customer before deferred intent UPE payment can be processed.
+ * This applies to SEPA and Link payment methods.
+ * https://stripe.com/docs/payments/finalize-payments-on-the-server
+ *
+ * @return boolean True if mandate must be shown and acknowledged by customer before deferred intent UPE payment can be processed, false otherwise.
+ */
+ public function is_mandate_data_required() {
+ $is_stripe_link_enabled = Payment_Method::CARD === $this->get_selected_stripe_payment_type_id() && in_array( Payment_Method::LINK, $this->get_upe_enabled_payment_method_ids(), true );
+ $is_sepa_debit_payment = Payment_Method::SEPA === $this->get_selected_stripe_payment_type_id();
+
+ return $is_stripe_link_enabled || $is_sepa_debit_payment;
+ }
+
/**
* Returns the Stripe payment type of the selected payment method.
*
diff --git a/includes/reports/class-wc-rest-payments-reports-transactions-controller.php b/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
index 8772de15c2d..d59c259fe2e 100644
--- a/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
+++ b/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
@@ -70,7 +70,7 @@ public function get_transactions( $request ) {
];
$wcpay_request->set_filters( $filters );
- $response = $wcpay_request->handle_rest_request( 'wcpay_list_transactions_request' );
+ $response = $wcpay_request->handle_rest_request();
if ( is_wp_error( $response ) ) {
return $response;
}
@@ -96,7 +96,7 @@ public function get_transaction( $request ) {
$wcpay_request->set_sort_by( 'date' ); // Default sort.
$wcpay_request->set_page_size( 1 ); // Set page size to limit to only one record.
- $response = $wcpay_request->handle_rest_request( 'wcpay_list_transactions_request' );
+ $response = $wcpay_request->handle_rest_request();
if ( is_wp_error( $response ) ) {
return $response;
}
diff --git a/includes/subscriptions/assets/css/plugin-page.css b/includes/subscriptions/assets/css/plugin-page.css
index 05add31edb6..b7d109e036c 100644
--- a/includes/subscriptions/assets/css/plugin-page.css
+++ b/includes/subscriptions/assets/css/plugin-page.css
@@ -1,4 +1,4 @@
-#wcpay-subscriptions-plugin-warning-notice .button {
+.woopayments-plugin-warning-modal .button {
height: 36px;
align-items: center;
box-sizing: border-box;
@@ -7,10 +7,10 @@
line-height: normal;
cursor: pointer;
}
-#wcpay-subscriptions-plugin-warning-notice .button-secondary {
+.woopayments-plugin-warning-modal .button-secondary {
background: transparent;
}
-#wcpay-subscriptions-plugin-warning-notice .button.busy {
+.woopayments-plugin-warning-modal .button.busy {
animation: wcpay-plugin-notice-submit-button-busy 3000ms infinite linear;
background-image: linear-gradient(
-45deg,
@@ -21,59 +21,56 @@
);
background-size: 100px 100%;
}
-#wcpay-subscriptions-plugin-warning-notice
- footer
- .inner
- > *:not( :first-child ) {
+.woopayments-plugin-warning-modal footer .inner > *:not( :first-child ) {
margin-left: 16px;
}
-#wcpay-subscriptions-plugin-warning-notice h1 {
+.woopayments-plugin-warning-modal h1 {
font-size: 1rem;
font-weight: 600;
line-height: 1;
}
-#wcpay-subscriptions-plugin-warning-notice header,
-#wcpay-subscriptions-plugin-warning-notice footer {
+.woopayments-plugin-warning-modal header,
+.woopayments-plugin-warning-modal footer {
background: initial;
box-shadow: initial;
}
-#wcpay-subscriptions-plugin-warning-notice header {
+.woopayments-plugin-warning-modal header {
margin: 0 -24px;
padding-top: 24px;
padding-left: 24px;
height: 60px;
border-bottom: 1px solid #dcdcde;
}
-#wcpay-subscriptions-plugin-warning-notice footer {
+.woopayments-plugin-warning-modal footer {
padding: 24px;
border-top: 1px solid #dcdcde;
}
-#wcpay-subscriptions-plugin-warning-notice .wc-backbone-modal-content {
+.woopayments-plugin-warning-modal .wc-backbone-modal-content {
border-radius: 2px;
padding: 0 24px 24px;
}
-#wcpay-subscriptions-plugin-warning-notice .modal-close-link {
+.woopayments-plugin-warning-modal .modal-close-link {
height: 100%;
width: 60px;
border-left: none;
}
-#wcpay-subscriptions-plugin-warning-notice .modal-close-link:hover {
+.woopayments-plugin-warning-modal .modal-close-link:hover {
background: transparent;
}
-#wcpay-subscriptions-plugin-warning-notice article {
+.woopayments-plugin-warning-modal article {
padding: 1.5em 0;
}
-#wcpay-subscriptions-plugin-warning-notice .wc-backbone-modal-main {
+.woopayments-plugin-warning-modal .wc-backbone-modal-main {
padding-bottom: 60px;
}
-#wcpay-subscriptions-plugin-warning-notice .modal-close-link:hover::before {
+.woopayments-plugin-warning-modal .modal-close-link:hover::before {
color: initial;
}
-#wcpay-subscriptions-plugin-warning-notice ul {
+.woopayments-plugin-warning-modal ul {
padding-left: 24px;
list-style: disc;
}
-#wcpay-subscriptions-plugin-warning-notice ul > li > ul {
+.woopayments-plugin-warning-modal ul > li > ul {
margin-top: 6px;
list-style: circle;
}
diff --git a/includes/subscriptions/assets/js/plugin-page.js b/includes/subscriptions/assets/js/plugin-page.js
index df980ddf166..5123ba5671f 100644
--- a/includes/subscriptions/assets/js/plugin-page.js
+++ b/includes/subscriptions/assets/js/plugin-page.js
@@ -9,13 +9,6 @@ jQuery( function ( $ ) {
// Initialise handlers for WC Pay deactivate warning.
init_deactivate_wcpay_warning() {
- // If the store doesn't have active WCPay (Stripe Billing) subscriptions, no warning needed.
- if (
- ! wcpay_subscriptions_plugin_screen_data.store_has_active_wcpay_subscriptions
- ) {
- return;
- }
-
// Intercept click on WCPay deactivate link to show modal.
$( '#deactivate-woocommerce-payments' ).on(
'click',
@@ -47,10 +40,10 @@ jQuery( function ( $ ) {
);
},
- // Initialise handlers for WC Subscriptions deactivate warning.
+ // Initialise handlers for Woo Subscriptions deactivate warning.
init_deactivate_wc_subscriptions_warning() {
- // Intercept click on WCS deactivate link to show modal.
- $( '#deactivate-woocommerce-subscriptions' ).on(
+ // Intercept click on Woo Subscriptions deactivate link to show modal.
+ $( '#deactivate-' + this.get_woo_subscriptions_plugin_slug() ).on(
'click',
this.display_wcs_warning
);
@@ -77,9 +70,22 @@ jQuery( function ( $ ) {
'busy'
);
- window.location = $( '#deactivate-woocommerce-subscriptions' ).attr(
- 'href'
+ window.location = $(
+ '#deactivate-' +
+ wc_payments_plugin.get_woo_subscriptions_plugin_slug()
+ ).attr( 'href' );
+ },
+ // Gets the Woo Subscriptions plugin slug. When the ite is connected to WooCommerce.com, the slug is different and includes a woocommerce-com- prefix.
+ get_woo_subscriptions_plugin_slug() {
+ const element = document.querySelector(
+ '[data-slug="woocommerce-com-woocommerce-subscriptions"]'
);
+
+ if ( element ) {
+ return 'woocommerce-com-woocommerce-subscriptions';
+ } else {
+ return 'woocommerce-subscriptions';
+ }
},
};
diff --git a/includes/subscriptions/class-wc-payments-invoice-service.php b/includes/subscriptions/class-wc-payments-invoice-service.php
index 66fd2dcc4f4..429f46dda0e 100644
--- a/includes/subscriptions/class-wc-payments-invoice-service.php
+++ b/includes/subscriptions/class-wc-payments-invoice-service.php
@@ -288,8 +288,9 @@ public function set_subscription_invoice_id( WC_Subscription $subscription, stri
*/
public function get_and_attach_intent_info_to_order( $order, $intent_id ) {
try {
- $request = Get_Intention::create( $intent_id );
- $intent_object = $request->send( 'wcpay_get_intent_request', $order );
+ $request = Get_Intention::create( $intent_id );
+ $request->set_hook_args( $order );
+ $intent_object = $request->send();
} catch ( API_Exception $e ) {
$order->add_order_note( __( 'The payment info couldn\'t be added to the order.', 'woocommerce-payments' ) );
diff --git a/includes/subscriptions/class-wc-payments-subscriptions-plugin-notice-manager.php b/includes/subscriptions/class-wc-payments-subscriptions-plugin-notice-manager.php
index 1fb6dd7b523..76ce78eeaaf 100644
--- a/includes/subscriptions/class-wc-payments-subscriptions-plugin-notice-manager.php
+++ b/includes/subscriptions/class-wc-payments-subscriptions-plugin-notice-manager.php
@@ -41,7 +41,7 @@ private function is_admin_plugins_screen() {
* Enqueues the admin scripts needed on the plugins screen.
*/
public function enqueue_scripts_and_styles() {
- if ( ! $this->is_admin_plugins_screen() ) {
+ if ( ! $this->is_admin_plugins_screen() || ! WC_Payments_Subscription_Service::store_has_active_wcpay_subscriptions() ) {
return;
}
@@ -58,12 +58,6 @@ public function enqueue_scripts_and_styles() {
wp_enqueue_script( 'wcpay-subscriptions-plugin' );
- // Enqueue script data - does this store have active WCPay subscriptions?
- $script_data = [
- 'store_has_active_wcpay_subscriptions' => WC_Payments_Subscription_Service::store_has_active_wcpay_subscriptions(),
- ];
- wp_localize_script( 'wcpay-subscriptions-plugin', 'wcpay_subscriptions_plugin_screen_data', $script_data );
-
WC_Payments_Utils::enqueue_style(
'wcpay-subscriptions-plugin-styles',
plugins_url( 'includes/subscriptions/assets/css/plugin-page.css', WCPAY_PLUGIN_FILE ),
@@ -77,8 +71,16 @@ public function enqueue_scripts_and_styles() {
* Enqueues templates for plugin deactivation warnings on the admin plugin screen.
*/
public function output_notice_template() {
- if ( $this->is_admin_plugins_screen() ) {
- wc_get_template( 'html-subscriptions-plugin-notice.php', [], '', dirname( __DIR__ ) . '/subscriptions/templates/' );
+ if ( ! $this->is_admin_plugins_screen() ) {
+ return;
+ }
+
+ wc_get_template( 'html-subscriptions-plugin-notice.php', [], '', dirname( __DIR__ ) . '/subscriptions/templates/' );
+
+ // Load a slightly different notice for folks still using the legacy WCPay Subscriptions functionality.
+ if ( WC_Payments::get_gateway()->is_subscriptions_plugin_active() ) {
+ wc_get_template( 'html-woo-payments-deactivate-warning.php', [], '', dirname( __DIR__ ) . '/subscriptions/templates/' );
+ } else {
wc_get_template( 'html-wcpay-deactivate-warning.php', [], '', dirname( __DIR__ ) . '/subscriptions/templates/' );
}
}
diff --git a/includes/subscriptions/templates/html-subscriptions-plugin-notice.php b/includes/subscriptions/templates/html-subscriptions-plugin-notice.php
index 0a2e8f6fa79..1d4941e5b90 100644
--- a/includes/subscriptions/templates/html-subscriptions-plugin-notice.php
+++ b/includes/subscriptions/templates/html-subscriptions-plugin-notice.php
@@ -7,7 +7,7 @@
?>
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index bdb7e245106..31e2c9b6c7b 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -289,7 +289,9 @@ public function get_transactions_summary( $filters = [], $deposit_id = null ) {
* @return array
*/
public function list_fraud_outcome_transactions( $request ) {
- $fraud_outcomes = $request->send( 'wcpay_list_fraud_outcome_transactions_request' );
+ // TODO: Refactor this.
+ $request->assign_hook( 'wcpay_list_fraud_outcome_transactions_request' );
+ $fraud_outcomes = $request->send();
$page = $request->get_param( 'page' );
$page_size = $request->get_param( 'pagesize' );
@@ -310,7 +312,9 @@ public function list_fraud_outcome_transactions( $request ) {
* @return array
*/
public function list_fraud_outcome_transactions_summary( $request ) {
- $fraud_outcomes = $request->send( 'wcpay_list_fraud_outcome_transactions_summary_request' );
+ // TODO: Refactor this.
+ $request->assign_hook( 'wcpay_list_fraud_outcome_transactions_summary_request' );
+ $fraud_outcomes = $request->send();
$total = 0;
$currencies = [];
@@ -335,7 +339,9 @@ public function list_fraud_outcome_transactions_summary( $request ) {
* @return array|WP_Error Search results.
*/
public function get_fraud_outcome_transactions_search_autocomplete( $request ) {
- $fraud_outcomes = $request->send( 'wcpay_get_fraud_outcome_transactions_search_autocomplete_request' );
+ // TODO: Refactor this.
+ $request->assign_hook( 'wcpay_get_fraud_outcome_transactions_search_autocomplete_request' );
+ $fraud_outcomes = $request->send();
$search_term = $request->get_param( 'search_term' );
@@ -385,7 +391,9 @@ function ( $result ) {
* @return array
*/
public function get_fraud_outcome_transactions_export( $request ) {
- $fraud_outcomes = $request->send( 'wcpay_get_fraud_outcome_transactions_export_request' );
+ // TODO: Refactor this.
+ $request->assign_hook( 'wcpay_get_fraud_outcome_transactions_export_request' );
+ $fraud_outcomes = $request->send();
return [
'data' => $fraud_outcomes,
@@ -837,28 +845,29 @@ public function request_capability( string $capability_id, bool $requested ) {
/**
* Get data needed to initialize the onboarding flow
*
- * @param string $return_url - URL to redirect to at the end of the flow.
- * @param array $site_data - Data to track ToS agreement.
- * @param array $actioned_notes - Actioned WCPay note names to be sent to the on-boarding flow.
- * @param array $account_data - Data to prefill the onboarding.
- * @param bool $progressive - Whether we need to enable progressive onboarding prefill.
+ * @param string $return_url - URL to redirect to at the end of the flow.
+ * @param array $site_data - Data to track ToS agreement.
+ * @param array $user_data - Data about the user doing the onboarding (location and device).
+ * @param array $account_data - Data to prefill the onboarding.
+ * @param array $actioned_notes - Actioned WCPay note names to be sent to the onboarding flow.
+ * @param bool $progressive - Whether we need to enable progressive onboarding prefill.
* @param bool $collect_payout_requirements - Whether we need to redirect user to Stripe KYC to complete their payouts data.
*
- * @return array An array containing the url and state fields.
- *
* @throws API_Exception Exception thrown on request failure.
+ * @return array An array containing the url and state fields.
*/
- public function get_onboarding_data( $return_url, array $site_data = [], array $actioned_notes = [], $account_data = [], bool $progressive = false, $collect_payout_requirements = false ) {
+ public function get_onboarding_data( string $return_url, array $site_data = [], array $user_data = [], array $account_data = [], array $actioned_notes = [], bool $progressive = false, bool $collect_payout_requirements = false ): array {
$request_args = apply_filters(
'wc_payments_get_onboarding_data_args',
[
'return_url' => $return_url,
'site_data' => $site_data,
- 'create_live_account' => ! WC_Payments::mode()->is_dev(),
+ 'user_data' => $user_data,
+ 'account_data' => $account_data,
'actioned_notes' => $actioned_notes,
+ 'create_live_account' => ! WC_Payments::mode()->is_dev(),
'progressive' => $progressive,
'collect_payout_requirements' => $collect_payout_requirements,
- 'account_data' => $account_data,
]
);
diff --git a/includes/wc-payment-api/class-wc-payments-http.php b/includes/wc-payment-api/class-wc-payments-http.php
index 552faad5c8e..a812406070e 100644
--- a/includes/wc-payment-api/class-wc-payments-http.php
+++ b/includes/wc-payment-api/class-wc-payments-http.php
@@ -32,7 +32,14 @@ class WC_Payments_Http implements WC_Payments_Http_Interface {
*/
public function __construct( $connection_manager ) {
$this->connection_manager = $connection_manager;
+ }
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
add_filter( 'allowed_redirect_hosts', [ $this, 'allowed_redirect_hosts' ] );
}
diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php
index 3f6d69b4500..56b5a831636 100644
--- a/includes/woopay/class-woopay-session.php
+++ b/includes/woopay/class-woopay-session.php
@@ -231,6 +231,11 @@ public static function restore_order_customer_id_from_requests_with_verified_ema
* and disable the schedules when plugin is disabled.
*/
public static function run_and_remove_woopay_restore_order_customer_id_schedules() {
+ // WooCommerce is disabled when disabling WCPay.
+ if ( ! function_exists( 'wc_get_orders' ) ) {
+ return;
+ }
+
$args = [
'meta_key' => 'woopay_merchant_customer_id', //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'return' => 'ids',
@@ -345,7 +350,7 @@ private static function get_init_session_request( $order_id = null, $key = null,
$customer_id = WC_Payments::get_customer_service()->create_customer_for_user( $user, $customer_data );
}
- if ( 0 !== $user->ID ) {
+ if ( WC_Payments_Features::is_customer_multi_currency_enabled() && 0 !== $user->ID ) {
// Multicurrency selection is stored on user meta when logged in and WC session when logged out.
// This code just makes sure that currency selection is available on WC session for WooPay.
$currency = get_user_meta( $user->ID, MultiCurrency::CURRENCY_META_KEY, true );
@@ -358,7 +363,14 @@ private static function get_init_session_request( $order_id = null, $key = null,
$account_id = WC_Payments::get_account_service()->get_stripe_account_id();
- $store_logo = WC_Payments::get_gateway()->get_option( 'platform_checkout_store_logo' );
+ $site_logo_id = get_theme_mod( 'custom_logo' );
+ $site_logo_url = $site_logo_id ? ( wp_get_attachment_image_src( $site_logo_id, 'full' )[0] ?? '' ) : '';
+ $woopay_store_logo = WC_Payments::get_gateway()->get_option( 'platform_checkout_store_logo' );
+
+ $store_logo = $site_logo_url;
+ if ( ! empty( $woopay_store_logo ) ) {
+ $store_logo = get_rest_url( null, 'wc/v3/payments/file/' . $woopay_store_logo );
+ }
include_once WCPAY_ABSPATH . 'includes/compat/blocks/class-blocks-data-extractor.php';
$blocks_data_extractor = new Blocks_Data_Extractor();
@@ -381,8 +393,8 @@ private static function get_init_session_request( $order_id = null, $key = null,
'email' => '',
'store_data' => [
'store_name' => get_bloginfo( 'name' ),
- 'store_logo' => ! empty( $store_logo ) ? get_rest_url( null, 'wc/v3/payments/file/' . $store_logo ) : '',
- 'custom_message' => WC_Payments::get_gateway()->get_option( 'platform_checkout_custom_message' ),
+ 'store_logo' => $store_logo,
+ 'custom_message' => self::get_formatted_custom_message(),
'blog_id' => Jetpack_Options::get_option( 'id' ),
'blog_url' => get_site_url(),
'blog_checkout_url' => ! $is_pay_for_order ? wc_get_checkout_url() : $order->get_checkout_payment_url(),
diff --git a/includes/woopay/class-woopay-utilities.php b/includes/woopay/class-woopay-utilities.php
index f074231a371..a9837776291 100644
--- a/includes/woopay/class-woopay-utilities.php
+++ b/includes/woopay/class-woopay-utilities.php
@@ -79,6 +79,14 @@ public function is_woopay_express_checkout_enabled() {
return WC_Payments_Features::is_woopay_express_checkout_enabled() && $this->is_country_available( WC_Payments::get_gateway() ); // Feature flag.
}
+ /**
+ * Check conditions to determine if woopay first party auth is enabled.
+ *
+ * @return bool
+ */
+ public function is_woopay_first_party_auth_enabled() {
+ return WC_Payments_Features::is_woopay_first_party_auth_enabled() && $this->is_country_available( WC_Payments::get_gateway() ); // Feature flag.
+ }
/**
* Generates a hash based on the store's blog token, merchant ID, and the time step window.
*
diff --git a/package-lock.json b/package-lock.json
index 185fe217534..4b620347e41 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "woocommerce-payments",
- "version": "6.5.1",
+ "version": "6.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "woocommerce-payments",
- "version": "6.5.1",
+ "version": "6.6.0",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
diff --git a/package.json b/package.json
index 08ce1bc783b..a49df63fa09 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce-payments",
- "version": "6.5.1",
+ "version": "6.6.0",
"main": "webpack.config.js",
"author": "Automattic",
"license": "GPL-3.0-or-later",
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 9f8efd70ee5..d99908d97b3 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -7,6 +7,7 @@
+ ./dev/*
./dist/*
./release/*
./docker/*
@@ -70,4 +71,7 @@
+
+
+
diff --git a/readme.txt b/readme.txt
index 2a6554048ac..a411d06ab55 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: payment gateway, payment, apple pay, credit card, google pay, woocommerce
Requires at least: 6.0
Tested up to: 6.2
Requires PHP: 7.3
-Stable tag: 6.5.1
+Stable tag: 6.6.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -94,6 +94,84 @@ Please note that our support for the checkout block is still experimental and th
== Changelog ==
+= 6.6.0 - 2023-10-11 =
+* Add - Add a notice on the Settings page to request JCB capability for Japanese customers.
+* Add - Add current user data to the onboarding init request payload. This data is used for fraud prevention.
+* Add - Added API endpoint to fetch customer's saved payment methods.
+* Add - Added docs for cancel_authorization endpoint
+* Add - Added documentation for create payment intent API endpoint.
+* Add - Added documentation for payment methods API endpoint
+* Add - Add functionality to enable WooPay first party auth behind feature flag.
+* Add - Add helper function/method for raw currency amount conversion.
+* Add - Add Klarna payment method
+* Add - Add loading state to WooPay button
+* Add - Add payment intent creation endpoint
+* Add - Add the feature flag check for pay-for-order flow
+* Add - Add WC blocks spinner to the WooPay checkout styles.
+* Add - Behind a feature flag: dispute message added to transactions screen for disputes not needing a response.
+* Add - Display dispute information, recommended resolution steps, and actions directly on the transaction details screen to help merchants with dispute resolution.
+* Add - Display server error messages on Settings save
+* Add - Expand the data points added to the WooCommerce SSR to include all the main WooPayments features.
+* Add - Handle server-side feature flag for new UPE type enablement.
+* Add - Introduce the "Subscription Relationship" column under the Orders list admin page when HPOS is enabled.
+* Add - Show survey for merchants that disable WooPay.
+* Fix - Add Mix and Match Products support on WooPay.
+* Fix - Add multi-currency enablement check in WooPay session handling.
+* Fix - Comment: Behind a feature flag: Update documentation links (new/changed docs content) when notifying merchant that a dispute needs response.
+* Fix - Disable automatic currency switching and switcher widgets on pay_for_order page.
+* Fix - Ensure renewal orders paid via the Block Checkout are correctly linked to their subscription.
+* Fix - Ensure the order needs processing transient is deleted when a subscription order (eg renewal) is created. Fixes issues with renewal orders going straight to a completed status.
+* Fix - fix: save platform checkout info on blocks
+* Fix - Fix Apple Pay and Google Pay if card payments are disabled.
+* Fix - Fix error when disabling WCPay with core disabled.
+* Fix - Fix init WooPay and empty cart error
+* Fix - Fix modal header alignment on safari browser
+* Fix - Fix onboarding section on MultiCurrency settings page.
+* Fix - Fix WooPay express checkout button with product bundles on product page.
+* Fix - Hide tooltip related to Storefront theme in Multi-Currency settings when Storefront is not the active theme
+* Fix - Improved product details script with enhanced price calculation, and fallbacks for potential undefined values.
+* Fix - Improve escaping around attributes.
+* Fix - Load multi-currency class on setup page.
+* Fix - Missing styles on the Edit Subscription page when HPOS is enabled.
+* Fix - Only request WooPay session data once on blocks pages.
+* Fix - Payment method section missing for Affirm and Afterpay on transaction details page
+* Fix - Prevent charging completed or processing orders with a new payment intent ID
+* Fix - Prevent WooPay-related implementation to modify non-WooPay-specific webhooks by changing their data.
+* Fix - Prevent WooPay multiple redirect requests.
+* Fix - Redirect back to the connect page when attempting to access the new onboarding flow without a server connection.
+* Fix - Redirect back to the pay-for-order page when it's pay-for-order order
+* Fix - Resolved an issue that caused paying for failed/pending parent orders that include Product Add-ons to not calculate the correct total.
+* Fix - Speed up capturing terminal and authorized payments.
+* Fix - Store the correct subscription start date in postmeta and ordermeta when HPOS and data syncing is being used.
+* Fix - Tracking conditions
+* Fix - Virtual variable products no longer require shipping details when checking out with Apple Pay and Google Pay
+* Fix - When HPOS is enabled, deleting a customer will now delete their subscriptions.
+* Fix - When HPOS is enabled, make the orders_by_type_query filter box work in the WooCommerce orders screen.
+* Fix - WooPay save my info phone number fallback for virtual products
+* Update - Adapt the PO congratulations card copy for pending account status.
+* Update - Allow deferred intent creation UPE to support SEPA payments.
+* Update - Enhance design of bnpl payment methods status in settings screen
+* Update - Increase GBP transaction limit for Afterpay
+* Update - Only display the WCPay Subscriptions setting to existing users as part of deprecating this feature.
+* Update - Set WooPay First Party Authentication feature flag to default on.
+* Update - Store customer currencies as an option to avoid expensive calculation.
+* Update - Updated Transaction Details summary with added fee breakdown tooltip for disputed transactions.
+* Update - Update links that pointed to the dispute details screen to point to the transaction details screen
+* Update - Update Name Your Price compatibility to use new Compatibility methods.
+* Update - Update the content of modals that are displayed when deactivating the WooPayments or Woo Subscriptions plugins when the store has active Stripe Billing subscriptions.
+* Update - Update URL used to communicate with WooPay from the iFrame in the merchant site.
+* Dev - Added missing API docs links for payment intents and payment methods API endpoints
+* Dev - Capitalize the JCB label on transactions details page.
+* Dev - e2e tests for progressive onboarding
+* Dev - Extracting payment metadata and level 3 data generation into services.
+* Dev - Migrate away from hooking into actions in certain classes
+* Dev - Move fraud related service hooks out of class constructors and into new init_hooks methods.
+* Dev - Move hooks out of MultiCurrency constructor into own init_hooks method.
+* Dev - Refactored request class send() method
+* Dev - Refactor to move hook initialisation out of constructors.
+* Dev - This work is part of a UI improvements to increase disputes response that is behind a feature flag. A changelog entry will be added to represent the work as a whole.
+* Dev - Update subscriptions-core to 6.3.0.
+
= 6.5.1 - 2023-09-26 =
* Fix - fix incorrect payment method title for non-WooPayments gateways
diff --git a/src/Container.php b/src/Container.php
index 3ff2fd1c091..5c43a14584e 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -8,10 +8,13 @@
namespace WCPay;
use Psr\Container\ContainerInterface;
+use WCPay\Vendor\League\Container\Exception\ContainerException;
use WCPay\Internal\DependencyManagement\ExtendedContainer;
use WCPay\Internal\DependencyManagement\ServiceProvider\PaymentsServiceProvider;
use WCPay\Internal\DependencyManagement\DelegateContainer\LegacyContainer;
use WCPay\Internal\DependencyManagement\DelegateContainer\WooContainer;
+use WCPay\Internal\DependencyManagement\ServiceProvider\GenericServiceProvider;
+use WCPay\Internal\DependencyManagement\ServiceProvider\ProxiesServiceProvider;
/**
* WCPay Dependency Injection Container.
@@ -67,12 +70,21 @@ public function __construct(
* @template ID
* @param class-string $id The ID of the class to retrieve.
* @return ID
+ * @throws ContainerException In case the ID could not be resolved or instantiated.
*
* Psalm expects $id to be a string, based on ContainerInterface.
* @psalm-suppress MoreSpecificImplementedParamType
+ *
+ * PSR-11 containers declares to throw an un-throwable interface
+ * (it does not extend Throwable), and Psalm does not accept it.
+ * @psalm-suppress MissingThrowsDocblock
*/
public function get( $id ) {
- return $this->container->get( $id );
+ try {
+ return $this->container->get( $id );
+ } catch ( \Throwable $e ) {
+ throw new ContainerException( $e->getMessage(), $e->getCode(), $e );
+ }
}
/**
@@ -89,6 +101,8 @@ public function has( $id ) {
* Loads all available providers into the container.
*/
private function load_providers() {
+ $this->container->addServiceProvider( new GenericServiceProvider() );
$this->container->addServiceProvider( new PaymentsServiceProvider() );
+ $this->container->addServiceProvider( new ProxiesServiceProvider() );
}
}
diff --git a/src/Internal/DependencyManagement/ContainerException.php b/src/Internal/DependencyManagement/ContainerException.php
deleted file mode 100644
index 8b7b457c870..00000000000
--- a/src/Internal/DependencyManagement/ContainerException.php
+++ /dev/null
@@ -1,17 +0,0 @@
-getContainer();
+
+ $container->addShared( OrderService::class )
+ ->addArgument( WC_Payments_Order_Service::class )
+ ->addArgument( LegacyProxy::class )
+ ->addArgument( WC_Payments_Account::class )
+ ->addArgument( HooksProxy::class );
+
+ $container->addShared( Level3Service::class )
+ ->addArgument( OrderService::class )
+ ->addArgument( WC_Payments_Account::class )
+ ->addArgument( LegacyProxy::class );
+ }
+}
diff --git a/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php b/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php
index 360d5364641..8641fe728dd 100644
--- a/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php
+++ b/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php
@@ -8,13 +8,24 @@
namespace WCPay\Internal\DependencyManagement\ServiceProvider;
use Automattic\WooCommerce\Utilities\PluginUtil;
+use WC_Payments_Customer_Service;
+use WCPay\Container;
use WCPay\Core\Mode;
use WCPay\Database_Cache;
use WCPay\Internal\DependencyManagement\AbstractServiceProvider;
use WCPay\Internal\Payment\Router;
+use WCPay\Internal\Payment\State\CompletedState;
+use WCPay\Internal\Payment\State\InitialState;
+use WCPay\Internal\Payment\State\PaymentErrorState;
+use WCPay\Internal\Payment\State\StateFactory;
+use WCPay\Internal\Payment\State\SystemErrorState;
+use WCPay\Internal\Proxy\LegacyProxy;
use WCPay\Internal\Service\PaymentProcessingService;
use WCPay\Internal\Service\ExampleService;
use WCPay\Internal\Service\ExampleServiceWithDependencies;
+use WCPay\Internal\Service\Level3Service;
+use WCPay\Internal\Service\OrderService;
+use WCPay\Internal\Service\PaymentRequestService;
/**
* WCPay payments service provider.
@@ -28,8 +39,14 @@ class PaymentsServiceProvider extends AbstractServiceProvider {
protected $provides = [
PaymentProcessingService::class,
Router::class,
+ StateFactory::class,
+ InitialState::class,
+ CompletedState::class,
+ SystemErrorState::class,
+ PaymentErrorState::class,
ExampleService::class,
ExampleServiceWithDependencies::class,
+ PaymentRequestService::class,
];
/**
@@ -38,7 +55,30 @@ class PaymentsServiceProvider extends AbstractServiceProvider {
public function register(): void {
$container = $this->getContainer();
- $container->addShared( PaymentProcessingService::class );
+ $container->addShared( StateFactory::class )
+ ->addArgument( Container::class );
+
+ $container->addShared( PaymentProcessingService::class )
+ ->addArgument( StateFactory::class )
+ ->addArgument( LegacyProxy::class );
+
+ $container->addShared( PaymentRequestService::class );
+
+ $container->add( InitialState::class )
+ ->addArgument( StateFactory::class )
+ ->addArgument( OrderService::class )
+ ->addArgument( WC_Payments_Customer_Service::class )
+ ->addArgument( Level3Service::class )
+ ->addArgument( PaymentRequestService::class );
+
+ $container->add( CompletedState::class )
+ ->addArgument( StateFactory::class );
+
+ $container->add( SystemErrorState::class )
+ ->addArgument( StateFactory::class );
+
+ $container->add( PaymentErrorState::class )
+ ->addArgument( StateFactory::class );
$container->addShared( Router::class )
->addArgument( Database_Cache::class );
diff --git a/src/Internal/Payment/Exception/StateTransitionException.php b/src/Internal/Payment/Exception/StateTransitionException.php
new file mode 100644
index 00000000000..69ff81e03e5
--- /dev/null
+++ b/src/Internal/Payment/Exception/StateTransitionException.php
@@ -0,0 +1,17 @@
+order_id = $order_id;
+ }
+
+ /**
+ * Returns the ID of the order requiring payment.
+ *
+ * @return int
+ */
+ public function get_order_id(): ?int {
+ return $this->order_id;
+ }
+
+ /**
+ * Stores an internal value.
+ * Use this method for changes to allow logging in the future.
+ *
+ * @param string $key Property name.
+ * @param mixed $value Value to store.
+ */
+ private function set( string $key, $value ) {
+ $this->data[ $key ] = $value;
+ }
+
+ /**
+ * Retrieves an internal value, if any.
+ *
+ * @param string $key Key of the property.
+ * @return mixed|null Either the stored value, or null if not set.
+ */
+ private function get( string $key ) {
+ return $this->data[ $key ] ?? null;
+ }
+
+ /**
+ * Stores the payment amount.
+ *
+ * @param int $amount Payment amount in cents.
+ */
+ public function set_amount( int $amount ) {
+ $this->set( 'amount', $amount );
+ }
+
+ /**
+ * Returns the payment amount.
+ *
+ * @return int|null Amount in cents.
+ */
+ public function get_amount(): ?int {
+ return $this->get( 'amount' );
+ }
+
+ /**
+ * Stores the payment currency.
+ *
+ * @param string $currency Lowercase payment currency.
+ */
+ public function set_currency( string $currency ) {
+ $this->set( 'currency', $currency );
+ }
+
+ /**
+ * Returns the payment currency in lowercase.
+ *
+ * @return string|null
+ */
+ public function get_currency(): ?string {
+ return $this->get( 'currency' );
+ }
+
+ /**
+ * Controls whether manual capture is enabled.
+ *
+ * @param bool $manual_capture Whether to enable it or not.
+ */
+ public function toggle_manual_capture( bool $manual_capture ) {
+ $this->set( 'manual_capture', $manual_capture );
+ }
+
+ /**
+ * Indicates whether the payment should be captured manually.
+ *
+ * @return bool
+ */
+ public function should_capture_manually(): bool {
+ return $this->get( 'manual_capture' ) ?? false;
+ }
+
+ /**
+ * Stores the order metadata.
+ *
+ * @param array $metadata Metadata to sent to the API.
+ */
+ public function set_metadata( array $metadata ) {
+ $this->set( 'metadata', $metadata );
+ }
+
+ /**
+ * Returns the order level 3 data if set.
+ *
+ * @return array|null
+ */
+ public function get_level3_data(): ?array {
+ return $this->get( 'level3_data' );
+ }
+
+ /**
+ * Stores the order level 3 data.
+ *
+ * @param array $level3_data level3_data to sent to the API.
+ */
+ public function set_level3_data( array $level3_data ) {
+ $this->set( 'level3_data', $level3_data );
+ }
+
+ /**
+ * Returns the order metadata if set.
+ *
+ * @return array|null
+ */
+ public function get_metadata(): ?array {
+ return $this->get( 'metadata' );
+ }
+
+ /**
+ * Stores the CVC confirmation.
+ *
+ * @param string $cvc_confirmation The confirmation.
+ */
+ public function set_cvc_confirmation( string $cvc_confirmation = null ) {
+ $this->set( 'cvc_confirmation', $cvc_confirmation );
+ }
+
+ /**
+ * Returns the CVC confirmation if set.
+ *
+ * @return string|null
+ */
+ public function get_cvc_confirmation(): ?string {
+ return $this->get( 'cvc_confirmation' );
+ }
+
+ /**
+ * Stores a payment's fingerprint.
+ *
+ * @param string $fingerprint The fingerprint.
+ */
+ public function set_fingerprint( string $fingerprint ) {
+ $this->set( 'fingerprint', $fingerprint );
+ }
+
+ /**
+ * Returns a payment's fingerprint if set.
+ *
+ * @return string|null
+ */
+ public function get_fingerprint(): ?string {
+ return $this->get( 'fingerprint' );
+ }
+
+ /**
+ * Stores a payment method within the context.
+ *
+ * @param PaymentMethodInterface $payment_method The payment method to use.
+ */
+ public function set_payment_method( PaymentMethodInterface $payment_method ) {
+ $this->set( 'payment_method', $payment_method );
+ }
+
+ /**
+ * Returns the stored payment method object, if any.
+ *
+ * @return PaymentMethodInterface|null
+ */
+ public function get_payment_method(): ?PaymentMethodInterface {
+ return $this->get( 'payment_method' );
+ }
+
+ /**
+ * Stores the WP user ID, associated with the payment.
+ *
+ * @param int $user_id ID of the user.
+ */
+ public function set_user_id( int $user_id ) {
+ $this->set( 'user_id', $user_id );
+ }
+
+ /**
+ * Returns the ID of the user if any.
+ *
+ * @return int|null
+ */
+ public function get_user_id(): ?int {
+ return $this->get( 'user_id' );
+ }
+
+ /**
+ * Stores the remote customer ID.
+ *
+ * @param string $customer_id ID of the customer.
+ */
+ public function set_customer_id( string $customer_id ) {
+ $this->set( 'customer_id', $customer_id );
+ }
+
+ /**
+ * Returns the remote customer ID.
+ *
+ * @return string|null
+ */
+ public function get_customer_id(): ?string {
+ return $this->get( 'customer_id' );
+ }
+}
diff --git a/src/Internal/Payment/PaymentMethod/SavedPaymentMethod.php b/src/Internal/Payment/PaymentMethod/SavedPaymentMethod.php
index 21d18ad48dd..92479072c84 100644
--- a/src/Internal/Payment/PaymentMethod/SavedPaymentMethod.php
+++ b/src/Internal/Payment/PaymentMethod/SavedPaymentMethod.php
@@ -7,26 +7,33 @@
namespace WCPay\Internal\Payment\PaymentMethod;
-use WC_Payment_Token;
-
/**
- * Representation of a newly entered payment method.
+ * Representation of a saved payment method.
*/
class SavedPaymentMethod implements PaymentMethodInterface {
/**
- * Contains the WooCommerce token.
+ * External ID of the payment method.
+ *
+ * @var string
+ */
+ private $id;
+
+ /**
+ * Contains the WooCommerce token ID.
*
- * @var WC_Payment_Token
+ * @var int
*/
- private $token;
+ private $token_id;
/**
* Class constructor.
*
- * @param WC_Payment_Token $token The WooCommerce token.
+ * @param string $id External ID of the payment method.
+ * @param int $token_id Internal WooCommerce token ID.
*/
- public function __construct( WC_Payment_Token $token ) {
- $this->token = $token;
+ public function __construct( string $id, int $token_id ) {
+ $this->id = $id;
+ $this->token_id = $token_id;
}
/**
@@ -35,7 +42,16 @@ public function __construct( WC_Payment_Token $token ) {
* @return string
*/
public function get_id(): string {
- return $this->token->get_token();
+ return $this->id;
+ }
+
+ /**
+ * Returns the ID of the Woo payment token.
+ *
+ * @return int
+ */
+ public function get_token_id(): int {
+ return $this->token_id;
}
/**
@@ -45,8 +61,9 @@ public function get_id(): string {
*/
public function get_data(): array {
return [
- 'type' => 'saved',
- 'id' => $this->token->get_id(),
+ 'type' => 'saved',
+ 'id' => $this->id,
+ 'token_id' => $this->token_id,
];
}
}
diff --git a/src/Internal/Payment/PaymentRequest.php b/src/Internal/Payment/PaymentRequest.php
index 838ae8bb8e5..806c4341f91 100644
--- a/src/Internal/Payment/PaymentRequest.php
+++ b/src/Internal/Payment/PaymentRequest.php
@@ -134,7 +134,7 @@ public function get_payment_method(): PaymentMethodInterface {
if ( is_null( $token ) ) {
throw new PaymentRequestException( __( 'Invalid saved payment method (token) ID.', 'woocommerce-payments' ) );
}
- return new SavedPaymentMethod( $token );
+ return new SavedPaymentMethod( $token->get_token(), $token->get_id() );
}
if ( ! empty( $request['wcpay-payment-method'] ) ) {
@@ -144,4 +144,42 @@ public function get_payment_method(): PaymentMethodInterface {
throw new PaymentRequestException( __( 'No valid payment method was selected.', 'woocommerce-payments' ) );
}
+
+ /**
+ * Extract the payment CVC confirmation from the request.
+ *
+ * @return string|null
+ */
+ public function get_cvc_confirmation(): ?string {
+ $payment_method = $this->request['payment_method'] ?? null;
+ if ( null === $payment_method ) {
+ return null;
+ }
+
+ $cvc_request_key = 'wc-' . $payment_method . '-payment-cvc-confirmation';
+ if (
+ ! isset( $this->request[ $cvc_request_key ] ) ||
+ 'new' === $this->request[ $cvc_request_key ]
+ ) {
+ return null;
+ }
+
+ return $this->request[ $cvc_request_key ];
+ }
+
+ /**
+ * Extracts the fingerprint data from the request.
+ *
+ * @return string
+ */
+ public function get_fingerprint(): ?string {
+ if ( ! empty( $this->request['wcpay-fingerprint'] ) ) {
+ $normalized = wc_clean( $this->request['wcpay-fingerprint'] );
+ if ( is_string( $normalized ) ) {
+ return $normalized;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/Internal/Payment/State/AbstractPaymentState.php b/src/Internal/Payment/State/AbstractPaymentState.php
new file mode 100644
index 00000000000..021b1e05cb3
--- /dev/null
+++ b/src/Internal/Payment/State/AbstractPaymentState.php
@@ -0,0 +1,124 @@
+state_factory = $state_factory;
+ }
+
+ /**
+ * Stores the payment context.
+ *
+ * @param PaymentContext $context Payment context.
+ */
+ public function set_context( PaymentContext $context ) {
+ $this->context = $context;
+ }
+
+ /**
+ * Returns the payment context.
+ *
+ * @return PaymentContext
+ */
+ public function get_context(): PaymentContext {
+ return $this->context;
+ }
+
+ /**
+ * Creates a new instance of a given payment state class.
+ *
+ * States control the payment flow, and allow transitions to the next state.
+ * This method should only be called whenever the process is ready to transition
+ * to the next state, as each new state will be considered the payment's latest one.
+ *
+ * @param string $state_class The class of the state to crate.
+ * @return AbstractPaymentState
+ * @throws StateTransitionException In case the new state could not be created.
+ * @throws ContainerException When the dependency container cannot instantiate the state.
+ */
+ protected function create_state( string $state_class ) {
+ $state = $this->state_factory->create_state( $state_class, $this->context );
+
+ // This is where logging will be added.
+
+ return $state;
+ }
+
+ /**
+ * State-specific methods might declare a return type, but
+ * their hollow definitions here would only throw an exception.
+ * phpcs:disable Squiz.Commenting.FunctionComment.InvalidNoReturn
+ */
+
+ /**
+ * Initialtes the payment process.
+ *
+ * @param PaymentRequest $request The incoming payment processing request.
+ * @return CompletedState The next state.
+ * @throws StateTransitionException In case the completed state could not be initialized.
+ * @throws ContainerException When the dependency container cannot instantiate the state.
+ * @throws Order_Not_Found_Exception Order could not be found.
+ * @throws PaymentRequestException When data is not available or invalid.
+ * @psalm-suppress InvalidReturnType If this method does not throw, it will return a new state.
+ */
+ public function process( PaymentRequest $request ) {
+ $this->throw_unavailable_method_exception( __METHOD__ );
+ }
+
+ /**
+ * Throws an exception, indicating that a given method is not available.
+ *
+ * @param string $method_name The name of the called method.
+ * @throws StateTransitionException
+ */
+ private function throw_unavailable_method_exception( string $method_name ) {
+ throw new StateTransitionException(
+ sprintf(
+ // translators: %1$s is the name of a method of the payment object, %2$s is its current state.
+ __( 'The %1$s method is not available in the current payment state (%2$s).', 'woocommerce-payments' ),
+ $method_name,
+ get_class( $this )
+ )
+ );
+ }
+}
diff --git a/src/Internal/Payment/State/CompletedState.php b/src/Internal/Payment/State/CompletedState.php
new file mode 100644
index 00000000000..4810d64bf5c
--- /dev/null
+++ b/src/Internal/Payment/State/CompletedState.php
@@ -0,0 +1,15 @@
+order_service = $order_service;
+ $this->customer_service = $customer_service;
+ $this->level3_service = $level3_service;
+ $this->payment_request_service = $payment_request_service;
+ }
+
+ /**
+ * Initialtes the payment process.
+ *
+ * @param PaymentRequest $request The incoming payment processing request.
+ * @return CompletedState The next state.
+ * @throws StateTransitionException In case the completed state could not be initialized.
+ * @throws ContainerException When the dependency container cannot instantiate the state.
+ * @throws Order_Not_Found_Exception Order could not be found.
+ * @throws PaymentRequestException When data is not available or invalid.
+ */
+ public function process( PaymentRequest $request ) {
+ $context = $this->get_context();
+ $order_id = $context->get_order_id();
+
+ // Populate basic details from the request.
+ $this->populate_context_from_request( $request );
+
+ // Populate further details from the order.
+ $this->populate_context_from_order();
+
+ // Payments are currently based on intents, request one from the API.
+ try {
+ $intent = $this->payment_request_service->create_intent( $context );
+ } catch ( Invalid_Request_Parameter_Exception | Extend_Request_Exception | Immutable_Parameter_Exception $e ) {
+ return $this->create_state( SystemErrorState::class );
+ }
+
+ // Intent available, complete processing.
+ $this->order_service->update_order_from_successful_intent( $order_id, $intent, $context );
+
+ // If everything went well, transition to the completed state.
+ return $this->create_state( CompletedState::class );
+ }
+
+ /**
+ * Populates the payment context before processing a payment.
+ *
+ * This method is the link between the payment request, and the payment process.
+ * Use it to make sure that all necessary parameters are provided in advance,
+ * or throw an exception otherwise. Once done, the payment process would rely
+ * on all needed parameters being in place.
+ *
+ * @param PaymentRequest $request The request to use.
+ * @throws PaymentRequestException When data is not available or invalid.
+ */
+ protected function populate_context_from_request( PaymentRequest $request ) {
+ $context = $this->get_context();
+
+ $context->set_payment_method( $request->get_payment_method() );
+
+ $cvc_confirmation = $request->get_cvc_confirmation();
+ if ( ! is_null( $cvc_confirmation ) ) {
+ $context->set_cvc_confirmation( $cvc_confirmation );
+ }
+
+ $fingerprint = $request->get_fingerprint();
+ if ( ! is_null( $fingerprint ) ) {
+ $context->set_fingerprint( $fingerprint );
+ }
+ }
+
+ /**
+ * Populates the context with details, available in the order.
+ * This includes the update/creation of a customer.
+ *
+ * @throws Order_Not_Found_Exception In case the order could not be found.
+ */
+ protected function populate_context_from_order() {
+ $context = $this->get_context();
+ $order_id = $context->get_order_id();
+
+ // Start by setting up all local objects.
+ $this->order_service->import_order_data_to_payment_context( $order_id, $context );
+ $context->set_metadata(
+ array_merge(
+ $this->order_service->get_payment_metadata( $order_id ),
+ [ 'gateway_type' => 'src' ]
+ )
+ );
+ $context->set_level3_data( $this->level3_service->get_data_from_order( $order_id ) );
+
+ // Customer management involves a remote call.
+ $customer_id = $this->customer_service->get_or_create_customer_id_from_order(
+ $context->get_user_id(),
+ $this->order_service->_deprecated_get_order( $order_id )
+ );
+ $context->set_customer_id( $customer_id );
+ }
+}
diff --git a/src/Internal/Payment/State/PaymentErrorState.php b/src/Internal/Payment/State/PaymentErrorState.php
new file mode 100644
index 00000000000..1f713500ecf
--- /dev/null
+++ b/src/Internal/Payment/State/PaymentErrorState.php
@@ -0,0 +1,18 @@
+container = $container;
+ }
+
+ /**
+ * Creates a new state based on class name.
+ *
+ * @param string $state_class Name of the state class.
+ * @param PaymentContext $context Context for the new state.
+ * @return AbstractPaymentState The generated payment state instance.
+ * @throws ContainerException When the dependency container cannot instantiate the state.
+ * @throws StateTransitionException When the class name is not a state.
+ */
+ public function create_state( string $state_class, PaymentContext $context ): AbstractPaymentState {
+ if ( ! is_subclass_of( $state_class, AbstractPaymentState::class ) ) {
+ throw new StateTransitionException(
+ sprintf(
+ // Translators: %1$s is the PHP class for a new payment state, %1$s is the state base class.
+ __( 'The class %1$s is not a subclass of %2$s', 'woocommerce-payments' ),
+ $state_class,
+ AbstractPaymentState::class
+ )
+ );
+ }
+
+ $state = $this->container->get( $state_class );
+ $state->set_context( $context );
+ return $state;
+ }
+}
diff --git a/src/Internal/Payment/State/SystemErrorState.php b/src/Internal/Payment/State/SystemErrorState.php
new file mode 100644
index 00000000000..c960ee3d23d
--- /dev/null
+++ b/src/Internal/Payment/State/SystemErrorState.php
@@ -0,0 +1,18 @@
+order_service = $order_service;
+ $this->account = $account;
+ $this->legacy_proxy = $legacy_proxy;
+ }
+
+ /**
+ * Create the level 3 data array to send to Stripe when making a purchase.
+ *
+ * @param int $order_id The order that is being paid for.
+ * @return array The level 3 data to send to the API.
+ * @throws Order_Not_Found_Exception
+ */
+ public function get_data_from_order( int $order_id ): array {
+ $order = $this->order_service->_deprecated_get_order( $order_id );
+
+ $merchant_country = $this->account->get_account_country();
+ // We do not need to send level3 data if merchant account country is non-US.
+ if ( 'US' !== $merchant_country ) {
+ return [];
+ }
+
+ // Get the order items. Don't need their keys, only their values.
+ // Order item IDs are used as keys in the original order items array.
+ $order_items = array_values( $order->get_items( [ 'line_item', 'fee' ] ) );
+ $currency = $order->get_currency();
+
+ $process_item = function( $item ) use ( $currency ) {
+ return $this->process_item( $item, $currency );
+ };
+ $items_to_send = array_map( $process_item, $order_items );
+
+ if ( count( $items_to_send ) > 200 ) {
+ // If more than 200 items are present, bundle the last ones in a single item.
+ $items_to_send = array_merge(
+ array_slice( $items_to_send, 0, 199 ),
+ [ $this->bundle_level3_data_from_items( array_slice( $items_to_send, 200 ) ) ]
+ );
+ }
+
+ $level3_data = [
+ 'merchant_reference' => (string) $order->get_id(), // An alphanumeric string of up to characters in length. This unique value is assigned by the merchant to identify the order. Also known as an “Order ID”.
+ 'customer_reference' => (string) $order->get_id(),
+ 'shipping_amount' => $this->prepare_amount( (float) $order->get_shipping_total() + (float) $order->get_shipping_tax(), $currency ), // The shipping cost, in cents, as a non-negative integer.
+ 'line_items' => $items_to_send,
+ ];
+
+ // The customer’s U.S. shipping ZIP code.
+ $shipping_address_zip = $order->get_shipping_postcode();
+ if ( WC_Payments_Utils::is_valid_us_zip_code( $shipping_address_zip ) ) {
+ $level3_data['shipping_address_zip'] = $shipping_address_zip;
+ }
+
+ // The merchant’s U.S. shipping ZIP code.
+ $store_postcode = $this->legacy_proxy->call_function( 'get_option', 'woocommerce_store_postcode' );
+ if ( WC_Payments_Utils::is_valid_us_zip_code( $store_postcode ) ) {
+ $level3_data['shipping_from_zip'] = $store_postcode;
+ }
+
+ return $level3_data;
+ }
+
+ /**
+ * Processes a single order item.
+ * Based on the queried items, this class should only receive
+ * `WC_Order_Item_Product` or `WC_Order_Item_Fee` line items.
+ *
+ * @param WC_Order_Item_Product|WC_Order_Item_Fee $item Item to process.
+ * @param string $currency Currency to use.
+ * @return \stdClass
+ */
+ private function process_item( WC_Order_Item $item, string $currency ): stdClass {
+ // Check to see if it is a WC_Order_Item_Product or a WC_Order_Item_Fee.
+ if ( $item instanceof WC_Order_Item_Product ) {
+ $subtotal = $item->get_subtotal();
+ $product_id = $item->get_variation_id()
+ ? $item->get_variation_id()
+ : $item->get_product_id();
+ $product_code = substr( $product_id, 0, 12 );
+ } else {
+ $subtotal = $item->get_total();
+ $product_code = substr( sanitize_title( $item->get_name() ), 0, 12 );
+ }
+
+ $description = substr( $item->get_name(), 0, 26 );
+ $quantity = ceil( $item->get_quantity() );
+ $tax_amount = $this->prepare_amount( $item->get_total_tax(), $currency );
+ if ( $subtotal >= 0 ) {
+ $unit_cost = $this->prepare_amount( $subtotal / $quantity, $currency );
+ $discount_amount = $this->prepare_amount( $subtotal - $item->get_total(), $currency );
+ } else {
+ // It's possible to create products with negative price - represent it as free one with discount.
+ $discount_amount = abs( $this->prepare_amount( $subtotal / $quantity, $currency ) );
+ $unit_cost = 0;
+ }
+
+ return (object) [
+ 'product_code' => (string) $product_code, // Up to 12 characters that uniquely identify the product.
+ 'product_description' => $description, // Up to 26 characters long describing the product.
+ 'unit_cost' => $unit_cost, // Cost of the product, in cents, as a non-negative integer.
+ 'quantity' => $quantity, // The number of items of this type sold, as a non-negative integer.
+ 'tax_amount' => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer.
+ 'discount_amount' => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer.
+ ];
+ }
+
+ /**
+ * Returns a bundle of products passed as an argument. Useful when working with Stripe's level 3 data
+ *
+ * @param array $items The Stripe's level 3 array of items.
+ *
+ * @return \stdClass A bundle of the products passed.
+ */
+ private function bundle_level3_data_from_items( array $items ) {
+ // Total cost is the sum of each product cost * quantity.
+ $items_count = count( $items );
+ $total_cost = array_sum(
+ array_map(
+ function( $cost, $qty ) {
+ return $cost * $qty;
+ },
+ array_column( $items, 'unit_cost' ),
+ array_column( $items, 'quantity' )
+ )
+ );
+
+ return (object) [
+ 'product_code' => (string) substr( uniqid(), 0, 26 ),
+ 'product_description' => "{$items_count} more items",
+ 'unit_cost' => $total_cost,
+ 'quantity' => 1,
+ 'tax_amount' => array_sum( array_column( $items, 'tax_amount' ) ),
+ 'discount_amount' => array_sum( array_column( $items, 'discount_amount' ) ),
+ ];
+ }
+
+ /**
+ * Returns an API-ready amount based on a currency.
+ *
+ * @param float $amount The base amount.
+ * @param string $currency The currency for the amount.
+ *
+ * @return int The amount in cents.
+ */
+ private function prepare_amount( float $amount, string $currency ): int {
+ return WC_Payments_Utils::prepare_amount( $amount, $currency );
+ }
+}
diff --git a/src/Internal/Service/OrderService.php b/src/Internal/Service/OrderService.php
new file mode 100644
index 00000000000..ca7b3ee9083
--- /dev/null
+++ b/src/Internal/Service/OrderService.php
@@ -0,0 +1,285 @@
+legacy_service = $legacy_service;
+ $this->legacy_proxy = $legacy_proxy;
+ $this->account = $account;
+ $this->hooks_proxy = $hooks_proxy;
+ }
+
+ /**
+ * Retrieves the order object.
+ *
+ * Please restrain from using this method!
+ * It can only be used to (temporarily) provide the order object
+ * to legacy (`includes`) services, which are not adapted to work
+ * with order IDs yet.
+ *
+ * @see https://github.com/Automattic/woocommerce-payments/issues/7367
+ * @param int $order_id ID of the order.
+ * @return WC_Order Order object.
+ * @throws Order_Not_Found_Exception If the order could not be found.
+ */
+ public function _deprecated_get_order( int $order_id ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
+ return $this->get_order( $order_id );
+ }
+
+ /**
+ * Set the payment metadata for payment method id.
+ *
+ * @param int $order_id ID of the order.
+ * @param string $payment_method_id The value to be set.
+ *
+ * @throws Order_Not_Found_Exception
+ */
+ public function set_payment_method_id( int $order_id, string $payment_method_id ) {
+ $this->legacy_service->set_payment_method_id_for_order( $order_id, $payment_method_id );
+ }
+
+ /**
+ * Generates payment metadata from order details.
+ *
+ * @param int $order_id ID of the order.
+ * @param Payment_Type $payment_type Type of the payment (recurring or not).
+ * @return array The metadat athat will be sent to the server.
+ * @throws Order_Not_Found_Exception
+ */
+ public function get_payment_metadata( int $order_id, Payment_Type $payment_type = null ) {
+ $order = $this->get_order( $order_id );
+
+ $name = sanitize_text_field( $order->get_billing_first_name() ) . ' ' . sanitize_text_field( $order->get_billing_last_name() );
+ $email = sanitize_email( $order->get_billing_email() );
+ $metadata = [
+ 'customer_name' => $name,
+ 'customer_email' => $email,
+ 'site_url' => esc_url( get_site_url() ),
+ 'order_id' => $order->get_id(),
+ 'order_number' => $order->get_order_number(),
+ 'order_key' => $order->get_order_key(),
+ 'payment_type' => $payment_type,
+ 'checkout_type' => $order->get_created_via(),
+ 'client_version' => WCPAY_VERSION_NUMBER,
+ 'subscription_payment' => 'no',
+ ];
+
+ if (
+ 'recurring' === (string) $payment_type
+ && $this->legacy_proxy->call_function( 'function_exists', 'wcs_order_contains_subscription' )
+ && $this->legacy_proxy->call_function( 'wcs_order_contains_subscription', $order, 'any' )
+ ) {
+ $use_stripe_billing = $this->legacy_proxy->call_static( WC_Payments_Features::class, 'should_use_stripe_billing' );
+ $is_renewal = $this->legacy_proxy->call_function( 'wcs_order_contains_renewal', $order );
+
+ $metadata['subscription_payment'] = $is_renewal ? 'renewal' : 'initial';
+ $metadata['payment_context'] = $use_stripe_billing ? 'wcpay_subscription' : 'regular_subscription';
+ }
+
+ return $this->hooks_proxy->apply_filters( 'wcpay_metadata_from_order', $metadata, $order, $payment_type );
+ }
+
+ /**
+ * Imports the data from an order to a payment context.
+ *
+ * @param int $order_id ID of the order.
+ * @param PaymentContext $context A payment context, awaiting order data.
+ * @throws Order_Not_Found_Exception
+ */
+ public function import_order_data_to_payment_context( int $order_id, PaymentContext $context ) {
+ $order = $this->get_order( $order_id );
+
+ $currency = strtolower( $order->get_currency() );
+ $amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency );
+
+ $user = $order->get_user();
+ if ( false === $user ) { // Default to the current user.
+ $user = $this->legacy_proxy->call_function( 'wp_get_current_user' );
+ }
+
+ $context->set_currency( $currency );
+ $context->set_amount( $amount );
+ $context->set_user_id( $user->ID );
+ }
+
+ /**
+ * God method for updating orders once a payment has succeeded.
+ *
+ * @param int $order_id ID of the order that was just paid.
+ * @param WC_Payments_API_Abstract_Intention $intent Remote object. To be abstracted.
+ * @param PaymentContext $context Context for the payment.
+ * @throws Order_Not_Found_Exception
+ */
+ public function update_order_from_successful_intent(
+ int $order_id,
+ WC_Payments_API_Abstract_Intention $intent,
+ PaymentContext $context
+ ) {
+ $order = $this->get_order( $order_id );
+
+ $charge = null;
+ $charge_id = null;
+ if ( $intent instanceof WC_Payments_API_Payment_Intention ) {
+ $charge = $intent->get_charge();
+ $charge_id = $intent->get_charge()->get_id();
+ }
+
+ $this->legacy_service->attach_intent_info_to_order(
+ $order,
+ $intent->get_id(),
+ $intent->get_status(),
+ $context->get_payment_method()->get_id(),
+ $context->get_customer_id(),
+ $charge_id,
+ $context->get_currency()
+ );
+
+ $this->legacy_service->attach_transaction_fee_to_order( $order, $charge );
+ $this->legacy_service->update_order_status_from_intent( $order, $intent );
+
+ if ( ! is_null( $charge ) ) {
+ $this->attach_exchange_info_to_order( $order_id, $charge );
+ }
+ }
+
+ /**
+ * Given the charge data, checks if there was an exchange and adds it to the given order as metadata
+ *
+ * @param int $order_id The order to update.
+ * @param WC_Payments_API_Charge $charge Charge object.
+ * @throws Order_Not_Found_Exception
+ */
+ public function attach_exchange_info_to_order( int $order_id, WC_Payments_API_Charge $charge ) {
+ $order = $this->get_order( $order_id );
+
+ // This is a good example of something, which should be a service.
+ $currency_store = $this->legacy_proxy->call_function( 'get_option', 'woocommerce_currency' );
+ $currency_store = strtolower( $currency_store );
+ $currency_order = strtolower( $order->get_currency() );
+ $currency_account = strtolower( $this->account->get_account_default_currency() );
+
+ // If the default currency for the store is different from the currency for the merchant's Stripe account,
+ // the conversion rate provided by Stripe won't make sense, so we should not attach it to the order meta data
+ // and instead we'll rely on the _wcpay_multi_currency_order_exchange_rate meta key for analytics.
+ if ( $currency_store !== $currency_account ) {
+ return;
+ }
+
+ // If the account and order currency are the same, there was no exchange.
+ if ( $currency_order === $currency_account ) {
+ return;
+ }
+
+ // Without the balance transaction, we cannot check the exchange rate.
+ $transaction = $charge->get_balance_transaction();
+ $exchange_rate = $transaction['exchange_rate'] ?? null;
+ if ( is_null( $exchange_rate ) ) {
+ return;
+ }
+
+ // This is a pure method and can remain static.
+ $exchange_rate = WC_Payments_Utils::interpret_string_exchange_rate( $exchange_rate, $currency_order, $currency_account );
+ $order->update_meta_data( '_wcpay_multi_currency_stripe_exchange_rate', $exchange_rate );
+ $order->save_meta_data();
+ }
+
+ /**
+ * Retrieves the order object.
+ *
+ * This method should be only used internally within this service.
+ * Other `src` methods and services should not access and manipulate
+ * order data directly, utilizing this service instead.
+ *
+ * Unlike the legacy service, this one only accepts integer IDs,
+ * and returns only the `WC_Order` object, no refunds.
+ *
+ * @param int $order_id ID of the order.
+ * @return WC_Order Order object.
+ * @throws Order_Not_Found_Exception If the order could not be found.
+ */
+ protected function get_order( int $order_id ): WC_Order {
+ $order = $this->legacy_proxy->call_function( 'wc_get_order', $order_id );
+ if ( ! $order instanceof WC_Order ) {
+ throw new Order_Not_Found_Exception(
+ sprintf(
+ // Translators: %d is the ID of an order.
+ __( 'The requested order (ID %d) was not found.', 'woocommerce-payments' ),
+ $order_id
+ ),
+ 'order_not_found'
+ );
+ }
+ return $order;
+ }
+}
diff --git a/src/Internal/Service/PaymentProcessingService.php b/src/Internal/Service/PaymentProcessingService.php
index 7e1783f0004..e32a823ef12 100644
--- a/src/Internal/Service/PaymentProcessingService.php
+++ b/src/Internal/Service/PaymentProcessingService.php
@@ -8,19 +8,79 @@
namespace WCPay\Internal\Service;
use Exception; // Temporary exception! This service would have its own exception when more business logics are added.
+use WCPay\Vendor\League\Container\Exception\ContainerException;
+use WCPay\Internal\Payment\PaymentContext;
+use WCPay\Internal\Payment\State\InitialState;
+use WCPay\Internal\Payment\State\StateFactory;
+use WCPay\Internal\Payment\Exception\StateTransitionException;
+use WCPay\Internal\Payment\PaymentRequestException;
+use WCPay\Internal\Payment\PaymentRequest;
+use WCPay\Internal\Proxy\LegacyProxy;
/**
* Payment Processing Service.
*/
class PaymentProcessingService {
+ /**
+ * Factory for states.
+ *
+ * @var StateFactory
+ */
+ private $state_factory;
+
+ /**
+ * Legacy Proxy.
+ *
+ * @var LegacyProxy
+ */
+ private $legacy_proxy;
+
+ /**
+ * Service constructor.
+ *
+ * @param StateFactory $state_factory Factory for payment states.
+ * @param LegacyProxy $legacy_proxy Legacy proxy.
+ */
+ public function __construct(
+ StateFactory $state_factory,
+ LegacyProxy $legacy_proxy
+ ) {
+ $this->state_factory = $state_factory;
+ $this->legacy_proxy = $legacy_proxy;
+ }
+
/**
* Process payment.
*
- * @param int $order_id Order ID provided by WooCommerce core.
+ * @param int $order_id Order ID provided by WooCommerce core.
+ * @param bool $manual_capture Whether to only create an authorization instead of a charge (optional).
*
* @throws Exception
+ * @throws StateTransitionException In case a state cannot be initialized.
+ * @throws PaymentRequestException When the request is malformed. This should be converted to a failure state.
+ * @throws ContainerException When the dependency container cannot instantiate the state.
+ */
+ public function process_payment( int $order_id, bool $manual_capture = false ) {
+ // Start with a basis context.
+ $context = $this->create_payment_context( $order_id, $manual_capture );
+
+ $request = new PaymentRequest( $this->legacy_proxy );
+ $initial_state = $this->state_factory->create_state( InitialState::class, $context );
+ $completed_state = $initial_state->process( $request );
+
+ return $completed_state;
+ }
+
+ /**
+ * Instantiates a new empty payment context.
+ *
+ * @param int $order_id ID of the order that the context belongs to.
+ * @param bool $manual_capture Whether manual capture is enabled.
+ * @return PaymentContext
*/
- public function process_payment( int $order_id ) {
- throw new Exception( 'Re-engineering payment process is in-progress. Sit tight, and wait more!' );
+ protected function create_payment_context( int $order_id, bool $manual_capture = false ): PaymentContext {
+ $context = new PaymentContext( $order_id );
+ $context->toggle_manual_capture( $manual_capture );
+ return $context;
}
}
diff --git a/src/Internal/Service/PaymentRequestService.php b/src/Internal/Service/PaymentRequestService.php
new file mode 100644
index 00000000000..c7ba7b92428
--- /dev/null
+++ b/src/Internal/Service/PaymentRequestService.php
@@ -0,0 +1,48 @@
+set_amount( $context->get_amount() );
+ $request->set_currency_code( $context->get_currency() );
+ $request->set_payment_method( $context->get_payment_method()->get_id() );
+ $request->set_customer( $context->get_customer_id() );
+ $request->set_capture_method( $context->should_capture_manually() );
+ $request->set_metadata( $context->get_metadata() );
+ $request->set_level3( $context->get_level3_data() );
+ $request->set_payment_methods( [ 'card' ] ); // Initial payment process only supports cards.
+ $request->set_cvc_confirmation( $context->get_cvc_confirmation() );
+ // Empty string to trigger the link to `Buyer_Fingerprinting_Service`.
+ $request->set_fingerprint( $context->get_fingerprint() ?? '' );
+
+ // ToDo: The WooPay service should accept the old and new payment contexts.
+ $request->assign_hook( 'wcpay_create_and_confirm_intent_request_new' );
+ return $request->send();
+ }
+}
diff --git a/tests/WCPAY_UnitTestCase.php b/tests/WCPAY_UnitTestCase.php
index 01d7bf22dfd..5e3fc92ccc3 100644
--- a/tests/WCPAY_UnitTestCase.php
+++ b/tests/WCPAY_UnitTestCase.php
@@ -5,6 +5,7 @@
* @package WooCommerce\Payments\Tests
*/
+use PHPUnit\Framework\MockObject\MockObject;
use WCPay\Core\Server\Request;
use WCPay\Core\Server\Response;
@@ -18,6 +19,22 @@ protected function is_wpcom() {
return defined( 'IS_WPCOM' ) && IS_WPCOM;
}
+ /**
+ * Creates a mock object.
+ *
+ * This method does not work differently from `createMock`,
+ * but the DocBlock comment indicates a proper return type,
+ * combining `MockObject` and the provided class name.
+ *
+ * @template ID
+ * @param class-string $original_class_name Name of the class to mock.
+ * @return ID|MockObject
+ */
+ public function createMock( string $original_class_name ): MockObject { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found
+ return parent::createMock( $original_class_name );
+ }
+
+
/**
* Mocks an outgoing WCPay request (Those from WCPay\Core\Server\Request).
*
@@ -36,7 +53,7 @@ protected function is_wpcom() {
* @param WC_Payments_API_Client $api_client_mock Specific API client mock if necessary.
* @param WC_Payments_Http $http_mock Specific HTTP mock if necessary.
*
- * @return Request The mocked request.
+ * @return Request|MockObject The mocked request.
*/
protected function mock_wcpay_request( string $request_class, int $total_api_calls = 1, $request_class_constructor_id = null, $response = null, $api_client_mock = null, $http_mock = null ) {
$http_mock = $http_mock ? $http_mock : $this->createMock( WC_Payments_Http::class );
@@ -48,9 +65,12 @@ protected function mock_wcpay_request( string $request_class, int $total_api_cal
// No expectation for calls, return here.
return;
}
+ // Since setMethodsExcept is deprecated, this is the only alternative I came upon.
+ $methods_to_mock = array_diff( get_class_methods( $request_class ), [ 'set_hook_args', 'assign_hook' ] );
$request = $this->getMockBuilder( $request_class )
->setConstructorArgs( [ $api_client_mock, $http_mock, $request_class_constructor_id ] )
+ ->onlyMethods( $methods_to_mock ) // Mock all methods except set_hook_args and assign_hook to accommodate filter args when apply_filters is called.
->getMock();
$api_client_mock->expects( $this->exactly( $total_api_calls ) )
diff --git a/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-for-editing.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-for-editing.spec.js
index 49d6d61d1e4..0fcd4bfa625 100644
--- a/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-for-editing.spec.js
+++ b/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-for-editing.spec.js
@@ -14,7 +14,7 @@ const { merchant, shopper } = require( '@woocommerce/e2e-utils' );
let orderId;
-describe( 'Disputes > Save dispute for editing', () => {
+describe.skip( 'Disputes > Save dispute for editing', () => {
beforeAll( async () => {
await page.goto( config.get( 'url' ), { waitUntil: 'networkidle0' } );
@@ -54,28 +54,9 @@ describe( 'Disputes > Save dispute for editing', () => {
await expect( page ).toMatchElement( 'li.woocommerce-timeline-item', {
text: 'Payment disputed as Product not received.',
} );
-
- await expect( page ).toMatchElement(
- 'div.woocommerce-timeline-item__body a',
- {
- text: 'View dispute',
- }
- );
} );
it( 'should be able to save dispute for editing', async () => {
- // Get the link to the dispute details
- const disputeDetailsElement = await page.$(
- '[data-testid="view-dispute-button"]'
- );
- const disputeDetailsLink = await page.evaluate(
- ( anchor ) => anchor.getAttribute( 'href' ),
- disputeDetailsElement
- );
-
- // Open the dispute details
- await merchantWCP.openDisputeDetails( disputeDetailsLink );
-
// Click to challenge the dispute
await merchantWCP.openChallengeDispute();
@@ -121,11 +102,8 @@ describe( 'Disputes > Save dispute for editing', () => {
}
);
- // Re-open the dispute to view the details
- await merchantWCP.openDisputeDetails( disputeDetailsLink );
-
- // View the saved challenge
- await merchantWCP.openChallengeDispute();
+ // Reload the page
+ await page.reload();
// Verify the previously selected Product type was saved
await expect( page ).toMatchElement(
diff --git a/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-losing.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-losing.spec.js
index 0312ca52a17..6a258c066be 100644
--- a/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-losing.spec.js
+++ b/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-losing.spec.js
@@ -12,7 +12,7 @@ const { merchant, shopper } = require( '@woocommerce/e2e-utils' );
let orderId;
-describe( 'Disputes > Submit losing dispute', () => {
+describe.skip( 'Disputes > Submit losing dispute', () => {
beforeAll( async () => {
await page.goto( config.get( 'url' ), { waitUntil: 'networkidle0' } );
@@ -52,75 +52,34 @@ describe( 'Disputes > Submit losing dispute', () => {
await expect( page ).toMatchElement( 'li.woocommerce-timeline-item', {
text: 'Payment disputed as Product not received.',
} );
- await expect( page ).toMatchElement(
- 'div.woocommerce-timeline-item__body a',
- {
- text: 'View dispute',
- }
- );
-
- // Get the link to the dispute details
- const disputeDetailsElement = await page.$(
- '[data-testid="view-dispute-button"]'
- );
- const disputeDetailsLink = await page.evaluate(
- ( anchor ) => anchor.getAttribute( 'href' ),
- disputeDetailsElement
- );
-
- // Open the dispute details
- await merchantWCP.openDisputeDetails( disputeDetailsLink );
-
- // Verify we're on the view dispute page
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .header-dispute-overview',
- {
- text: 'Dispute overview',
- }
- );
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .components-card .components-card__header',
- {
- text: 'Dispute: Product not received',
- }
- );
// Accept the dispute
await merchantWCP.openAcceptDispute();
// If webhooks are not received, the dispute status won't be updated in the dispute list page resulting in test failure.
- // Workaround - Open dispute details page again and check status.
- await merchantWCP.openDisputeDetails( disputeDetailsLink );
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .header-dispute-overview',
- {
- text: 'Dispute overview',
- }
- );
+ // Workaround - Open payment details page again and check dispute's status.
+ await merchantWCP.openPaymentDetails( paymentDetailsLink );
// Confirm buttons are not present anymore since a dispute has been accepted.
await expect( page ).not.toMatchElement(
- 'div.components-card > .components-card__footer > a',
+ // eslint-disable-next-line max-len
+ 'div.transaction-details-dispute-details-body div.transaction-details-dispute-details-body__actions button.components-button.is-primary',
{
text: 'Challenge dispute',
}
);
await expect( page ).not.toMatchElement(
- 'div.components-card > .components-card__footer > button',
+ // eslint-disable-next-line max-len
+ 'div.transaction-details-dispute-details-body div.transaction-details-dispute-details-body__actions button.components-button.is-tertiary',
{
text: 'Accept dispute',
}
);
// Confirm dispute status is Lost.
- await page.waitForSelector(
- 'div.wcpay-dispute-details .header-dispute-overview span.chip-light'
- );
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .header-dispute-overview span.chip-light',
- {
- text: 'Lost',
- }
- );
+ await page.waitForSelector( 'li.woocommerce-timeline-item' );
+ await expect( page ).toMatchElement( 'li.woocommerce-timeline-item', {
+ text: 'Dispute lost. The bank ruled in favor of your customer.',
+ } );
} );
} );
diff --git a/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-winning.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-winning.spec.js
index 12396928b1e..37f5989dfd7 100644
--- a/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-winning.spec.js
+++ b/tests/e2e/specs/wcpay/merchant/merchant-disputes-submit-winning.spec.js
@@ -17,7 +17,7 @@ const {
let orderId;
-describe( 'Disputes > Submit winning dispute', () => {
+describe.skip( 'Disputes > Submit winning dispute', () => {
beforeAll( async () => {
await page.goto( config.get( 'url' ), { waitUntil: 'networkidle0' } );
@@ -56,38 +56,6 @@ describe( 'Disputes > Submit winning dispute', () => {
await expect( page ).toMatchElement( 'li.woocommerce-timeline-item', {
text: 'Payment disputed as Transaction unauthorized.',
} );
- await expect( page ).toMatchElement(
- 'div.woocommerce-timeline-item__body a',
- {
- text: 'View dispute',
- }
- );
-
- // Get the link to the dispute details
- const disputeDetailsElement = await page.$(
- '[data-testid="view-dispute-button"]'
- );
- const disputeDetailsLink = await page.evaluate(
- ( anchor ) => anchor.getAttribute( 'href' ),
- disputeDetailsElement
- );
-
- // Open the dispute details
- await merchantWCP.openDisputeDetails( disputeDetailsLink );
-
- // Verify we're on the view dispute page
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .header-dispute-overview',
- {
- text: 'Dispute overview',
- }
- );
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .components-card .components-card__header',
- {
- text: 'Dispute: Transaction unauthorized',
- }
- );
// Challenge the dispute
await merchantWCP.openChallengeDispute();
@@ -142,32 +110,13 @@ describe( 'Disputes > Submit winning dispute', () => {
] );
// If webhooks are not received, the dispute status won't be updated in the dispute list page resulting in test failure.
- // Workaround - Open dispute details page again and check status.
- await merchantWCP.openDisputeDetails( disputeDetailsLink );
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .header-dispute-overview',
- {
- text: 'Dispute overview',
- }
- );
-
- // Check view submitted evidence is present on page.
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .components-card div.components-flex > div > a',
- {
- text: 'View submitted evidence',
- }
- );
+ // Workaround - Open payment details page again and check dispute's status.
+ await merchantWCP.openPaymentDetails( paymentDetailsLink );
// Confirm dispute status is Won.
- await page.waitForSelector(
- 'div.wcpay-dispute-details .header-dispute-overview span.chip'
- );
- await expect( page ).toMatchElement(
- 'div.wcpay-dispute-details .header-dispute-overview span.chip',
- {
- text: 'Won',
- }
- );
+ await page.waitForSelector( 'li.woocommerce-timeline-item' );
+ await expect( page ).toMatchElement( 'li.woocommerce-timeline-item', {
+ text: 'Dispute won! The bank ruled in your favor.',
+ } );
} );
} );
diff --git a/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js
new file mode 100644
index 00000000000..e7cf88553e1
--- /dev/null
+++ b/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js
@@ -0,0 +1,128 @@
+/**
+ * External dependencies
+ */
+const { merchant, evalAndClick } = require( '@woocommerce/e2e-utils' );
+
+/**
+ * Internal dependencies
+ */
+import { merchantWCP, uiLoaded } from '../../../utils';
+
+describe( 'Admin merchant progressive onboarding', () => {
+ beforeAll( async () => {
+ await merchant.login();
+ await merchantWCP.enableProgressiveOnboarding();
+ await merchantWCP.enableActAsDisconnectedFromWCPay();
+ } );
+
+ afterAll( async () => {
+ await merchantWCP.disableProgressiveOnboarding();
+ await merchantWCP.disableActAsDisconnectedFromWCPay();
+ await merchant.logout();
+ } );
+
+ it( 'should pass merchant flow without any errors', async () => {
+ // Open connect account page and click Finish Setup
+ await merchantWCP.openConnectPage();
+ await Promise.all( [
+ evalAndClick(
+ 'div.connect-account-page button.components-button.is-primary'
+ ),
+ page.waitForNavigation( { waitUntil: 'networkidle0' } ),
+ uiLoaded(),
+ ] );
+
+ // Merchant vs builder flow step
+ await expect( page ).toMatchElement( 'h1.stepper__heading', {
+ text: 'Let’s get your store ready to accept payments',
+ } );
+ await expect( page ).toClick(
+ 'div.stepper__content button.components-button.is-primary',
+ {
+ text: 'Continue',
+ }
+ );
+
+ // User details step
+ await expect( page ).toMatchElement( 'h1.stepper__heading', {
+ text: 'First, you’ll need to create an account',
+ } );
+ await expect( page ).toFill( '[name="individual.first_name"]', 'Test' );
+ await expect( page ).toFill( '[name="individual.last_name"]', 'Test' );
+ await expect( page ).toFill( '[name="email"]', 'test@gmail.com' );
+ await page.waitForSelector(
+ 'div.wcpay-component-phone-number-control input[type="text"]'
+ );
+ await expect( page ).toFill(
+ 'div.wcpay-component-phone-number-control input[type="text"]',
+ '0000000000'
+ );
+ await expect( page ).toClick(
+ 'div.stepper__content button.components-button.is-primary',
+ {
+ text: 'Continue',
+ }
+ );
+
+ // Tell us about your business step
+ await expect( page ).toMatchElement( 'h1.stepper__heading', {
+ text: 'Tell us about your business',
+ } );
+ // pick Individual business entity
+ await expect( page ).toClick( '[name="business_type"]' );
+ await page.waitForSelector(
+ '[name="business_type"] ~ ul li.components-custom-select-control__item'
+ );
+ await expect( page ).toClick(
+ '[name="business_type"] ~ ul li.components-custom-select-control__item'
+ );
+ // pick Software type of goods
+ await expect( page ).toClick( '[name="mcc"]' );
+ await page.waitForSelector(
+ '[name="mcc"] ~ ul li.wcpay-component-grouped-select-control__item:not(.is-group)'
+ );
+ await expect( page ).toClick(
+ '[name="mcc"] ~ ul li.wcpay-component-grouped-select-control__item:not(.is-group)'
+ );
+ await expect( page ).toClick(
+ 'div.stepper__content button.components-button.is-primary',
+ {
+ text: 'Continue',
+ }
+ );
+
+ // Store details step: pick annual revenue and go live timeframe
+ await expect( page ).toMatchElement( 'h1.stepper__heading', {
+ text: 'Please share a few more details',
+ } );
+ await expect( page ).toClick( '[name="annual_revenue"]' );
+ await page.waitForSelector(
+ '[name="annual_revenue"] ~ ul li.components-custom-select-control__item'
+ );
+ await expect( page ).toClick(
+ '[name="annual_revenue"] ~ ul li.components-custom-select-control__item'
+ );
+ await expect( page ).toClick( '[name="go_live_timeframe"]' );
+ await page.waitForSelector(
+ '[name="go_live_timeframe"] ~ ul li.components-custom-select-control__item'
+ );
+ await expect( page ).toClick(
+ '[name="go_live_timeframe"] ~ ul li.components-custom-select-control__item'
+ );
+ await expect( page ).toClick(
+ 'div.stepper__content button.components-button.is-primary',
+ {
+ text: 'Continue',
+ }
+ );
+
+ // Loading screen
+ await expect( page ).toMatchElement( 'h1.stepper__heading', {
+ text: 'Let’s get you set up for payments',
+ } );
+
+ // Merchant is redirected away to payments/connect again (because of force fisconnected option)
+ // todo at some point test real Stripe KYC
+ await page.waitForNavigation( { waitUntil: 'networkidle0' } );
+ } );
+} );
diff --git a/tests/e2e/utils/flows.js b/tests/e2e/utils/flows.js
index 702fb057a1d..6459acaf6ca 100644
--- a/tests/e2e/utils/flows.js
+++ b/tests/e2e/utils/flows.js
@@ -30,6 +30,8 @@ const WC_ADMIN_BASE_URL = baseUrl + 'wp-admin/';
const MY_ACCOUNT_SUBSCRIPTIONS = baseUrl + 'my-account/subscriptions';
const MY_ACCOUNT_EDIT = baseUrl + 'my-account/edit-account';
const MY_ACCOUNT_ORDERS = SHOP_MY_ACCOUNT_PAGE + 'orders/';
+const WCPAY_CONNECT =
+ baseUrl + 'wp-admin/admin.php?page=wc-admin&path=/payments/connect';
const WCPAY_DISPUTES =
baseUrl + 'wp-admin/admin.php?page=wc-admin&path=/payments/disputes';
const WCPAY_DEPOSITS =
@@ -462,6 +464,79 @@ export const merchantWCP = {
} );
},
+ enableProgressiveOnboarding: async () => {
+ await page.goto( WCPAY_DEV_TOOLS, {
+ waitUntil: 'networkidle0',
+ } );
+
+ if (
+ ! ( await page.$(
+ '#_wcpay_feature_progressive_onboarding:checked'
+ ) )
+ ) {
+ await expect( page ).toClick(
+ 'label[for="_wcpay_feature_progressive_onboarding"]'
+ );
+ }
+
+ await expect( page ).toClick( 'input#submit' );
+ await page.waitForNavigation( {
+ waitUntil: 'networkidle0',
+ } );
+ },
+
+ disableProgressiveOnboarding: async () => {
+ await page.goto( WCPAY_DEV_TOOLS, {
+ waitUntil: 'networkidle0',
+ } );
+
+ if (
+ await page.$( '#_wcpay_feature_progressive_onboarding:checked' )
+ ) {
+ await expect( page ).toClick(
+ 'label[for="_wcpay_feature_progressive_onboarding"]'
+ );
+ }
+
+ await expect( page ).toClick( 'input#submit' );
+ await page.waitForNavigation( {
+ waitUntil: 'networkidle0',
+ } );
+ },
+
+ enableActAsDisconnectedFromWCPay: async () => {
+ await page.goto( WCPAY_DEV_TOOLS, {
+ waitUntil: 'networkidle0',
+ } );
+
+ if ( ! ( await page.$( '#wcpaydev_force_disconnected:checked' ) ) ) {
+ await expect( page ).toClick(
+ 'label[for="wcpaydev_force_disconnected"]'
+ );
+ }
+
+ await expect( page ).toClick( 'input#submit' );
+ await page.waitForNavigation( {
+ waitUntil: 'networkidle0',
+ } );
+ },
+
+ disableActAsDisconnectedFromWCPay: async () => {
+ await page.goto( WCPAY_DEV_TOOLS, {
+ waitUntil: 'networkidle0',
+ } );
+
+ if ( await page.$( '#wcpaydev_force_disconnected:checked' ) ) {
+ await expect( page ).toClick(
+ 'label[for="wcpaydev_force_disconnected"]'
+ );
+ }
+ await expect( page ).toClick( 'input#submit' );
+ await page.waitForNavigation( {
+ waitUntil: 'networkidle0',
+ } );
+ },
+
enablePaymentMethod: async ( paymentMethods ) => {
await page.goto( WCPAY_PAYMENT_SETTINGS, {
waitUntil: 'networkidle0',
@@ -527,7 +602,8 @@ export const merchantWCP = {
openChallengeDispute: async () => {
await Promise.all( [
evalAndClick(
- 'div.wcpay-dispute-details a.components-button.is-primary'
+ // eslint-disable-next-line max-len
+ 'div.transaction-details-dispute-details-body div.transaction-details-dispute-details-body__actions button.components-button.is-primary'
),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
uiLoaded(),
@@ -538,12 +614,13 @@ export const merchantWCP = {
await Promise.all( [
page.removeAllListeners( 'dialog' ),
evalAndClick(
- 'div.wcpay-dispute-details button.components-button.is-secondary'
+ // eslint-disable-next-line max-len
+ 'div.transaction-details-dispute-details-body div.transaction-details-dispute-details-body__actions button.components-button.is-tertiary'
+ ),
+ evalAndClick(
+ // eslint-disable-next-line max-len
+ '.transaction-details-dispute-accept-modal__actions button.components-button.is-primary'
),
- page.on( 'dialog', async ( dialog ) => {
- await dialog.accept();
- } ),
- uiUnblocked(),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
uiLoaded(),
] );
@@ -605,6 +682,13 @@ export const merchantWCP = {
await uiLoaded();
},
+ openConnectPage: async () => {
+ await page.goto( WCPAY_CONNECT, {
+ waitUntil: 'networkidle0',
+ } );
+ await uiLoaded();
+ },
+
openOrderAnalytics: async () => {
await merchant.openAnalyticsPage( 'orders' );
await uiLoaded();
diff --git a/tests/unit/admin/test-class-wc-payments-admin-sections-overwrite.php b/tests/unit/admin/test-class-wc-payments-admin-sections-overwrite.php
index 47ec5c73bef..3a308511249 100644
--- a/tests/unit/admin/test-class-wc-payments-admin-sections-overwrite.php
+++ b/tests/unit/admin/test-class-wc-payments-admin-sections-overwrite.php
@@ -47,7 +47,9 @@ public function test_checkout_sections_are_modified() {
->expects( $this->any() )
->method( 'get_cached_account_data' )
->willReturn( [ 'is_live' => true ] );
- new WC_Payments_Admin_Sections_Overwrite( $this->account_service );
+
+ $admin_sections_overwrite = new WC_Payments_Admin_Sections_Overwrite( $this->account_service );
+ $admin_sections_overwrite->init_hooks();
$this->assertEquals(
$expected_sections,
diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php
index 1447d17234e..10079f3838b 100644
--- a/tests/unit/admin/test-class-wc-payments-admin.php
+++ b/tests/unit/admin/test-class-wc-payments-admin.php
@@ -23,6 +23,13 @@ class WC_Payments_Admin_Test extends WCPAY_UnitTestCase {
*/
private $mock_gateway;
+ /**
+ * Mock WC_Payments_API_Client.
+ *
+ * @var WC_Payments_API_Client|MockObject
+ */
+ private $mock_api_client;
+
/**
* Mock Onboarding Service.
*
@@ -62,7 +69,7 @@ public function set_up() {
$menu = null; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
$submenu = null; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
- $mock_api_client = $this->getMockBuilder( WC_Payments_API_Client::class )
+ $this->mock_api_client = $this->getMockBuilder( WC_Payments_API_Client::class )
->disableOriginalConstructor()
->getMock();
@@ -99,7 +106,7 @@ public function set_up() {
);
$this->payments_admin = new WC_Payments_Admin(
- $mock_api_client,
+ $this->mock_api_client,
$this->mock_gateway,
$this->mock_account,
$this->mock_onboarding_service,
@@ -373,6 +380,82 @@ public function data_maybe_redirect_overview_to_connect() {
];
}
+ /**
+ * @dataProvider data_maybe_redirect_onboarding_flow_to_connect
+ */
+ public function test_maybe_redirect_onboarding_flow_to_connect( $expected_times_redirect_called, $is_server_connected, $get_params ) {
+ $_GET = $get_params;
+
+ $this->mock_api_client
+ ->method( 'is_server_connected' )
+ ->willReturn( $is_server_connected );
+
+ $this->mock_account
+ ->expects( $this->exactly( $expected_times_redirect_called ) )
+ ->method( 'redirect_to_onboarding_welcome_page' );
+
+ $this->payments_admin->maybe_redirect_onboarding_flow_to_connect();
+ }
+
+ /**
+ * Data provider for test_maybe_redirect_onboarding_flow_to_connect
+ */
+ public function data_maybe_redirect_onboarding_flow_to_connect() {
+ return [
+ 'no_get_params' => [
+ 0,
+ false,
+ [],
+ ],
+ 'empty_page_param' => [
+ 0,
+ false,
+ [
+ 'path' => '/payments/onboarding',
+ ],
+ ],
+ 'incorrect_page_param' => [
+ 0,
+ false,
+ [
+ 'page' => 'wc-settings',
+ 'path' => '/payments/onboarding',
+ ],
+ ],
+ 'empty_path_param' => [
+ 0,
+ false,
+ [
+ 'page' => 'wc-admin',
+ ],
+ ],
+ 'incorrect_path_param' => [
+ 0,
+ false,
+ [
+ 'page' => 'wc-admin',
+ 'path' => '/payments/does-not-exist',
+ ],
+ ],
+ 'server_connected' => [
+ 0,
+ true,
+ [
+ 'page' => 'wc-admin',
+ 'path' => '/payments/onboarding',
+ ],
+ ],
+ 'happy_path' => [
+ 1,
+ false,
+ [
+ 'page' => 'wc-admin',
+ 'path' => '/payments/onboarding',
+ ],
+ ],
+ ];
+ }
+
/**
* Tests WC_Payments_Admin::add_disputes_notification_badge()
*/
diff --git a/tests/unit/admin/test-class-wc-rest-payments-customer-controller.php b/tests/unit/admin/test-class-wc-rest-payments-customer-controller.php
new file mode 100644
index 00000000000..11b9eb84669
--- /dev/null
+++ b/tests/unit/admin/test-class-wc-rest-payments-customer-controller.php
@@ -0,0 +1,124 @@
+mock_api_client = $this->createMock( WC_Payments_API_Client::class );
+ $this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class );
+ $this->controller = new WC_REST_Payments_Customer_Controller( $this->mock_api_client, $this->mock_customer_service );
+ }
+
+ public function test_get_customer_payment_methods_endpoint_will_return_correct_response_for_card() {
+
+ $request = new WP_REST_Request( 'GET' );
+ $request->set_param( 'customer_id', 1 );
+ $payment_method = $this->get_base_payment_method_data();
+ $payment_method['type'] = 'card';
+ $payment_method['card'] = [
+ 'brand' => 'mastercard',
+ 'checks' => [
+ 'address_line1_check' => 'fail',
+ 'address_postal_code_check' => 'unchecked',
+ 'cvc_check' => 'pass',
+ ],
+ 'country' => 'US',
+ 'exp_month' => 11,
+ 'exp_year' => 2030,
+ 'fingerprint' => 'RSTUvWXZa1b2c3Y4',
+ 'funding' => 'debit',
+ 'generated_from' => null,
+ 'last4' => '5678',
+ 'networks' => [
+ 'available' => [ 'mastercard' ],
+ 'preferred' => null,
+ ],
+ 'three_d_secure_usage' => [
+ 'supported' => false,
+ ],
+ 'wallet' => null,
+ ];
+
+ $this->mock_customer_service
+ ->expects( $this->once() )
+ ->method( 'get_payment_methods_for_customer' )
+ ->with( $this->anything(), 'card' )
+ ->willReturn( [ $payment_method ] ); // We will test each payment method type in different test.
+
+ $response = $this->controller->get_customer_payment_methods( $request );
+ $data = $response->get_data()[0];
+ $this->assertIsArray( $data );
+ $this->assertSame( $data['id'], $payment_method['id'] );
+ $this->assertSame( $data['type'], $payment_method['type'] );
+ $this->assertSame( $data['billing_details'], $payment_method['billing_details'] );
+ $this->assertSame( $data['billing_details'], $payment_method['billing_details'] );
+ $this->assertSame( $data['card']['brand'], $payment_method['card']['brand'] );
+ $this->assertSame( $data['card']['last4'], $payment_method['card']['last4'] );
+ $this->assertSame( $data['card']['exp_month'], $payment_method['card']['exp_month'] );
+ $this->assertArrayNotHasKey( 'sepa_debit', $data );
+ $this->assertArrayNotHasKey( 'link', $data );
+ }
+
+ /**
+ * Get base payment method data.
+ *
+ * @return array[]
+ */
+ private function get_base_payment_method_data() {
+ return [
+
+ 'id' => 'pm_mock',
+ 'object' => 'payment_method',
+ 'billing_details' => [
+ 'address' => [
+ 'city' => 'Los Angeles',
+ 'country' => 'US',
+ 'line1' => '123 Sunset Blvd',
+ 'line2' => 'Apt 456',
+ 'postal_code' => '90028',
+ 'state' => 'CA',
+ ],
+ 'email' => 'john.doe@example.com',
+ 'name' => 'John Doe',
+ 'phone' => '1234567890',
+ ],
+ 'created' => 1692367890,
+ 'customer' => 'cus_mock',
+ 'livemode' => true,
+ 'metadata' => [],
+ ];
+ }
+}
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 e5f50a01baa..3a76bcba3ea 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
@@ -1605,6 +1605,121 @@ public function test_get_terminal_intent_payment_method_invalid_value() {
$this->controller->get_terminal_intent_payment_method( $request );
}
+ /**
+ * @dataProvider provider_capture_terminal_payment_allows_charging_order_with_intent_meta
+ */
+ public function test_capture_terminal_payment_allows_charging_order_with_intent_meta( string $order_meta_intent_status ) {
+ $order = $this->create_mock_order();
+ $order->update_meta_data( WC_Payments_Order_Service::INTENT_ID_META_KEY, $this->mock_intent_id );
+ $order->update_meta_data( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY, $order_meta_intent_status );
+ $order->save_meta_data();
+
+ $mock_intent = WC_Helper_Intention::create_intention(
+ [
+ 'status' => Intent_Status::REQUIRES_CAPTURE,
+ 'metadata' => [
+ 'order_id' => $order->get_id(),
+ ],
+ ]
+ );
+
+ $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->anything() );
+
+ $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 );
+ $response_data = $response->get_data();
+
+ $this->assertSame( 200, $response->status );
+ $this->assertSame(
+ [
+ 'status' => Intent_Status::SUCCEEDED,
+ 'id' => $this->mock_intent_id,
+ ],
+ $response_data
+ );
+ }
+
+ public function provider_capture_terminal_payment_allows_charging_order_with_intent_meta(): array {
+ return [
+ [ '' ],
+ [ Intent_Status::REQUIRES_CAPTURE ],
+ ];
+ }
+
+ /**
+ * @dataProvider provider_capture_terminal_payment_prevents_double_charging_order_with_intent_meta
+ */
+ public function test_capture_terminal_payment_prevents_double_charging_order_with_intent_meta( string $order_meta_intent_id, string $order_meta_intent_status, string $request_intent_id ) {
+ $order = $this->create_mock_order();
+ $order->update_meta_data( WC_Payments_Order_Service::INTENT_ID_META_KEY, $order_meta_intent_id );
+ $order->update_meta_data( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY, $order_meta_intent_status );
+ $order->save_meta_data();
+
+ $request = $this->mock_wcpay_request( Get_Intention::class, 0, $request_intent_id );
+
+ $this->mock_gateway
+ ->expects( $this->never() )
+ ->method( 'capture_charge' );
+
+ $this->order_service
+ ->expects( $this->never() )
+ ->method( 'attach_intent_info_to_order' );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $order->get_id(),
+ 'payment_intent_id' => $request_intent_id,
+ ]
+ );
+
+ $response = $this->controller->capture_terminal_payment( $request );
+
+ $this->assertInstanceOf( 'WP_Error', $response );
+ $data = $response->get_error_data();
+ $this->assertArrayHasKey( 'status', $data );
+ $this->assertSame( 409, $data['status'] );
+ }
+
+ public function provider_capture_terminal_payment_prevents_double_charging_order_with_intent_meta(): array {
+ return [
+ [ 'pi_abc', Intent_Status::REQUIRES_CAPTURE, 'pi_xyz' ],
+ [ 'pi_abc', '', 'pi_xyz' ],
+ [ 'pi_abc', Intent_Status::SUCCEEDED, 'pi_abc' ],
+ [ 'pi_abc', Intent_Status::SUCCEEDED, 'pi_xyz' ],
+ [ 'pi_abc', Intent_Status::CANCELED, 'pi_abc' ],
+ [ 'pi_abc', Intent_Status::CANCELED, 'pi_xyz' ],
+ [ 'pi_abc', Intent_Status::PROCESSING, 'pi_abc' ],
+ ];
+ }
+
/**
* @dataProvider provider_get_terminal_intent_capture_method
*/
diff --git a/tests/unit/admin/test-class-wc-rest-payments-payment-intents-controller.php b/tests/unit/admin/test-class-wc-rest-payments-payment-intents-controller.php
new file mode 100644
index 00000000000..1bdb96fe54b
--- /dev/null
+++ b/tests/unit/admin/test-class-wc-rest-payments-payment-intents-controller.php
@@ -0,0 +1,194 @@
+mock_api_client = $this->createMock( WC_Payments_API_Client::class );
+ $this->mock_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class );
+ $this->mock_order_service = $this->createMock( OrderService::class );
+ $this->mock_level3_service = $this->createMock( Level3Service::class );
+
+ $this->controller = new WC_REST_Payments_Payment_Intents_Create_Controller(
+ $this->mock_api_client,
+ $this->mock_gateway,
+ $this->mock_order_service,
+ $this->mock_level3_service
+ );
+ }
+
+ public function test_create_payment_intent_success() {
+ $current_order = WC_Helper_Order::create_order();
+ $current_order_id = $current_order->get_id();
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'get_payment_metadata' )
+ ->with( $current_order_id, Payment_Type::SINGLE() )
+ ->willReturn(
+ [
+ 'order_id' => $current_order->get_id(),
+ ]
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $current_order_id,
+ 'customer' => 'cus_123',
+ 'payment_method' => 'pm_123',
+ 'payment_method_types' => [ 'card' ],
+ ]
+ );
+
+ $mock_intent = WC_Helper_Intention::create_intention();
+ $this->mock_wcpay_request( Create_And_Confirm_Intention::class )
+ ->expects( $this->once() )
+ ->method( 'format_response' )
+ ->with()
+ ->willReturn(
+ $mock_intent
+ );
+
+ $response = $this->controller->create_payment_intent( $request );
+ $this->assertEquals( 200, $response->get_status() );
+ $data = $response->get_data();
+ $this->assertSame( $mock_intent->get_id(), $data['id'] );
+ $this->assertSame( $mock_intent->get_currency(), $data['currency'] );
+ $this->assertSame( $mock_intent->get_amount(), $data['amount'] );
+ $this->assertSame( $mock_intent->get_charge()->get_id(), $data['charge']['id'] );
+ }
+
+ public function test_create_payment_intent_no_order() {
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => 'no_such_order',
+ 'customer' => 'cus_123',
+ 'payment_method' => 'pm_123',
+ 'payment_method_types' => [ 'card' ],
+ ]
+ );
+
+ $response = $this->controller->create_payment_intent( $request );
+ $this->assertInstanceOf( 'WP_Error', $response );
+ $data = $response->get_error_data();
+ $this->assertArrayHasKey( 'status', $data );
+ $this->assertSame( 500, $data['status'] );
+ }
+
+
+ public function test_create_payment_intent_missing_required_params() {
+ $current_order = WC_Helper_Order::create_order();
+ $current_order_id = $current_order->get_id();
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'get_payment_metadata' )
+ ->with( $current_order_id, Payment_Type::SINGLE() )
+ ->willReturn(
+ [
+ 'order_id' => $current_order->get_id(),
+ ]
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $current_order_id,
+ ]
+ );
+
+ $response = $this->controller->create_payment_intent( $request );
+ $this->assertInstanceOf( 'WP_Error', $response );
+ $data = $response->get_error_data();
+ $this->assertArrayHasKey( 'status', $data );
+ $this->assertSame( 500, $data['status'] );
+ }
+
+ public function test_create_payment_intent_server_error() {
+ $current_order = WC_Helper_Order::create_order();
+ $current_order_id = $current_order->get_id();
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'get_payment_metadata' )
+ ->with( $current_order_id, Payment_Type::SINGLE() )
+ ->willReturn(
+ [
+ 'order_id' => $current_order->get_id(),
+ ]
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $current_order_id,
+ 'customer' => 'cus_123',
+ 'payment_method' => 'pm_123',
+ 'payment_method_types' => [ 'card' ],
+ ]
+ );
+
+ $wcpay_request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class );
+ $wcpay_request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->will(
+ $this->throwException(
+ new API_Exception(
+ 'Test error.',
+ 'test_error',
+ 400
+ )
+ )
+ );
+
+ $response = $this->controller->create_payment_intent( $request );
+ $this->assertInstanceOf( 'WP_Error', $response );
+ $data = $response->get_error_data();
+ $this->assertArrayHasKey( 'status', $data );
+ $this->assertSame( 500, $data['status'] );
+ $this->assertEquals( 'Test error.', $response->get_error_message() );
+ }
+
+}
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
index 48ba5bcdf7d..468c5ed0f98 100755
--- a/tests/unit/bootstrap.php
+++ b/tests/unit/bootstrap.php
@@ -94,7 +94,9 @@ function() {
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-reader-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-files-controller.php';
require_once $_plugin_dir . 'includes/reports/class-wc-rest-payments-reports-transactions-controller.php';
+ require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-payment-intents-controller.php';
require_once $_plugin_dir . 'includes/class-woopay-tracker.php';
+ require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-customer-controller.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
diff --git a/tests/unit/fraud-prevention/test-class-fraud-risk-tools.php b/tests/unit/fraud-prevention/test-class-fraud-risk-tools.php
index 97242880109..7af9c4b76a1 100644
--- a/tests/unit/fraud-prevention/test-class-fraud-risk-tools.php
+++ b/tests/unit/fraud-prevention/test-class-fraud-risk-tools.php
@@ -289,6 +289,14 @@ public function set_up() {
$this->fraud_risk_tools = new Fraud_Risk_Tools( $this->mock_wcpay_account );
}
+ public function test_registers_action_properly() {
+ wp_set_current_user( 1 );
+ $this->set_is_admin( true );
+ $this->set_current_user_can( true );
+ $this->fraud_risk_tools->init_hooks();
+ $this->assertNotFalse( has_action( 'admin_menu', [ $this->fraud_risk_tools, 'init_advanced_settings_page' ] ) );
+ }
+
public function test_it_gets_basic_protection_settings() {
update_option( 'woocommerce_allowed_countries', 'all' );
@@ -358,4 +366,37 @@ public function get_matching_protection_level_provider() {
'advanced' => [ $this->advanced_protection_level, 'advanced' ],
];
}
+
+ /**
+ * @param bool $is_admin
+ */
+ private function set_is_admin( bool $is_admin ) {
+ global $current_screen;
+
+ if ( ! $is_admin ) {
+ $current_screen = null; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
+ return;
+ }
+
+ // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
+ $current_screen = $this->getMockBuilder( \stdClass::class )
+ ->addMethods( [ 'in_admin' ] )
+ ->getMock();
+
+ $current_screen->method( 'in_admin' )->willReturn( $is_admin );
+ }
+
+ /**
+ * @param bool $can
+ */
+ private function set_current_user_can( bool $can ) {
+ global $current_user_can;
+
+ // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
+ $current_user_can = $this->getMockBuilder( \stdClass::class )
+ ->addMethods( [ 'current_user_can' ] )
+ ->getMock();
+
+ $current_user_can->method( 'current_user_can' )->willReturn( $can );
+ }
}
diff --git a/tests/unit/fraud-prevention/test-class-order-fraud-and-risk-meta-box.php b/tests/unit/fraud-prevention/test-class-order-fraud-and-risk-meta-box.php
index 0ec12e225e1..dbe604c82e2 100644
--- a/tests/unit/fraud-prevention/test-class-order-fraud-and-risk-meta-box.php
+++ b/tests/unit/fraud-prevention/test-class-order-fraud-and-risk-meta-box.php
@@ -40,6 +40,7 @@ public function set_up() {
// Create the mock Order Service and the Fraud and Risk meta box objects.
$this->mock_order_service = $this->createMock( WC_Payments_Order_Service::class );
$this->order_fraud_and_risk_meta_box = new Order_Fraud_And_Risk_Meta_Box( $this->mock_order_service );
+ $this->order_fraud_and_risk_meta_box->init_hooks();
// Create the mock order and set the gateway.
$this->order = WC_Helper_Order::create_order();
@@ -47,6 +48,10 @@ public function set_up() {
$this->order->save();
}
+ public function test_registers_action_properly() {
+ $this->assertNotFalse( has_action( 'add_meta_boxes', [ $this->order_fraud_and_risk_meta_box, 'maybe_add_meta_box' ] ) );
+ }
+
/**
* @dataProvider display_order_fraud_and_risk_meta_box_message_provider
*/
diff --git a/tests/unit/multi-currency/compatibility/test-class-woocommerce-name-your-price.php b/tests/unit/multi-currency/compatibility/test-class-woocommerce-name-your-price.php
index 9e2d4fffa8e..bb563f80b60 100644
--- a/tests/unit/multi-currency/compatibility/test-class-woocommerce-name-your-price.php
+++ b/tests/unit/multi-currency/compatibility/test-class-woocommerce-name-your-price.php
@@ -197,13 +197,8 @@ public function test_convert_cart_currency_returns_cart_item_with_converted_valu
$this->mock_multi_currency
->expects( $this->once() )
- ->method( 'get_enabled_currencies' )
- ->willReturn( [ $item_currency->get_code() => $item_currency ] );
-
- $this->mock_multi_currency
- ->expects( $this->once() )
- ->method( 'get_price' )
- ->with( $nyp_value / $item_currency->get_rate() )
+ ->method( 'get_raw_conversion' )
+ ->with( $nyp_value, $selected_currency->get_code(), $item_currency->get_code() )
->willReturn( $expected_value );
// Act: Attempt to convert the cart item amount.
@@ -242,8 +237,9 @@ public function test_convert_cart_currency_returns_cart_item_with_converted_valu
$this->mock_multi_currency
->expects( $this->once() )
- ->method( 'get_enabled_currencies' )
- ->willReturn( [ $item_currency->get_code() => $item_currency ] );
+ ->method( 'get_raw_conversion' )
+ ->with( $nyp_value, $selected_currency->get_code(), $item_currency->get_code() )
+ ->willReturn( $expected_value );
// Act: Attempt to convert the cart item amount.
$cart_item = $this->woocommerce_nyp->convert_cart_currency( $cart_item, null );
@@ -253,7 +249,6 @@ public function test_convert_cart_currency_returns_cart_item_with_converted_valu
// Assert: Confirm the cart_item value matches the expected value.
$this->assertEquals( $expected_value, $cart_item['nyp'] );
-
}
// If the method is passed false it should return false.
@@ -322,6 +317,123 @@ public function test_should_convert_product_price_returns_true_when_no_matches()
$this->assertTrue( $this->woocommerce_nyp->should_convert_product_price( true, $product ) );
}
+ public function test_edit_in_cart_args() {
+ // Arrange: Set up the currency used for the test.
+ $selected_currency = new Currency( 'EUR', 2.0 );
+
+ // Arrange: Set up the mock_multi_currency method mock.
+ $this->mock_multi_currency
+ ->expects( $this->once() )
+ ->method( 'get_selected_currency' )
+ ->willReturn( $selected_currency );
+
+ // Act: Edit the in cart args.
+ $result = $this->woocommerce_nyp->edit_in_cart_args( [], [] );
+
+ // Assert: Confirm that the currency code was added to the arg array.
+ $this->assertSame( $selected_currency->get_code(), $result['nyp_currency'] );
+ }
+
+ /**
+ * Runs through all the checks in the method returning the initial price until the last one passes all the checks.
+ *
+ * @dataProvider provider_get_initial_price
+ */
+ public function test_get_initial_price( $initial_price, $suffix, $request, $get_selected_currency, $get_raw_conversion ) {
+ // Arrange: Set the initial expected price and the currencies that may be used.
+ $expected_price = $initial_price;
+ $selected_currency = new Currency( 'EUR', 2.0 );
+ $store_currency = new Currency( 'USD', 1 );
+
+ // Arrange: Set expectations for calls to get_selected_currency method.
+ if ( $get_selected_currency ) {
+ $this->mock_multi_currency
+ ->expects( $this->once() )
+ ->method( 'get_selected_currency' )
+ ->willReturn( $selected_currency );
+ } else {
+ $this->mock_multi_currency
+ ->expects( $this->never() )
+ ->method( 'get_selected_currency' );
+ }
+
+ // Arrange: Set expectations for calls to get_raw_conversion method and update expected price.
+ if ( $get_raw_conversion ) {
+ $expected_price = $initial_price * ( $selected_currency->get_rate() / $store_currency->get_rate() );
+
+ $this->mock_multi_currency
+ ->expects( $this->once() )
+ ->method( 'get_raw_conversion' )
+ ->with( $initial_price, $selected_currency->get_code(), $store_currency->get_code() )
+ ->willReturn( $expected_price );
+ } else {
+ $this->mock_multi_currency
+ ->expects( $this->never() )
+ ->method( 'get_raw_conversion' );
+ }
+
+ // Arrange: Manually set the request prarameters.
+ foreach ( $request as $key => $value ) {
+ $_REQUEST[ $key ] = $value;
+ }
+
+ // Act: Get the initial price.
+ $result = $this->woocommerce_nyp->get_initial_price( $initial_price, '', $suffix );
+
+ // Assert: Confirm the initial price is returned.
+ $this->assertSame( $expected_price, $result );
+ }
+
+ public function provider_get_initial_price() {
+ return [
+ 'Both requests false - return initial_price' => [
+ 'initial_price' => 10.00,
+ 'suffix' => '',
+ 'request' => [],
+ 'get_selected_currency' => false,
+ 'get_raw_conversion' => false,
+ ],
+ 'First request true - return initial_price' => [
+ 'initial_price' => 10.00,
+ 'suffix' => '_suffix',
+ 'request' => [
+ 'nyp_raw_suffix' => 'test',
+ ],
+ 'get_selected_currency' => false,
+ 'get_raw_conversion' => false,
+ ],
+ 'Second request true - return initial_price' => [
+ 'initial_price' => 10.00,
+ 'suffix' => '_suffix',
+ 'request' => [
+ 'nyp_currency' => 'test',
+ ],
+ 'get_selected_currency' => false,
+ 'get_raw_conversion' => false,
+ ],
+ 'Both requests true - return initial_price' => [
+ 'initial_price' => 10.00,
+ 'suffix' => '_suffix',
+ 'request' => [
+ 'nyp_raw_suffix' => 10.00,
+ 'nyp_currency' => 'EUR',
+ ],
+ 'get_selected_currency' => true,
+ 'get_raw_conversion' => false,
+ ],
+ 'Both requests true - return converted price' => [
+ 'initial_price' => 10.00,
+ 'suffix' => '_suffix',
+ 'request' => [
+ 'nyp_raw_suffix' => 10.00,
+ 'nyp_currency' => 'USD',
+ ],
+ 'get_selected_currency' => true,
+ 'get_raw_conversion' => true,
+ ],
+ ];
+ }
+
/**
* Sets up `is_nyp` to return true or false for a test.
*/
diff --git a/tests/unit/multi-currency/test-class-admin-notices.php b/tests/unit/multi-currency/test-class-admin-notices.php
index 10b4d055a32..31e5b3504b6 100644
--- a/tests/unit/multi-currency/test-class-admin-notices.php
+++ b/tests/unit/multi-currency/test-class-admin-notices.php
@@ -23,6 +23,7 @@ public function set_up() {
parent::set_up();
$this->admin_notices = new WCPay\MultiCurrency\AdminNotices();
+ $this->admin_notices->init_hooks();
}
public function test_admin_notices_displays_currency_changed_notice() {
diff --git a/tests/unit/multi-currency/test-class-analytics.php b/tests/unit/multi-currency/test-class-analytics.php
index 0ca89b7fd6e..7f2ad134a43 100644
--- a/tests/unit/multi-currency/test-class-analytics.php
+++ b/tests/unit/multi-currency/test-class-analytics.php
@@ -145,6 +145,8 @@ public function test_has_multi_currency_orders() {
}
public function test_register_customer_currencies_for_empty_customer_currencies() {
+ delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
+
$this->mock_multi_currency->expects( $this->once() )
->method( 'get_all_customer_currencies' )
->willReturn( [] );
diff --git a/tests/unit/multi-currency/test-class-backend-currencies.php b/tests/unit/multi-currency/test-class-backend-currencies.php
index 9601dec5afb..530efc71b7d 100644
--- a/tests/unit/multi-currency/test-class-backend-currencies.php
+++ b/tests/unit/multi-currency/test-class-backend-currencies.php
@@ -43,6 +43,7 @@ public function set_up() {
set_current_screen( 'edit-post' );
$this->backend_currencies = new BackendCurrencies( $this->mock_multi_currency, $this->mock_localization_service );
+ $this->backend_currencies->init_hooks();
}
public function tear_down() {
@@ -74,6 +75,7 @@ public function test_registers_woocommerce_filter_with_multiple_enabled_currenci
->method( 'has_additional_currencies_enabled' )
->willReturn( true );
$this->backend_currencies = new BackendCurrencies( $this->mock_multi_currency, $this->mock_localization_service );
+ $this->backend_currencies->init_hooks();
$this->assertGreaterThan(
10,
@@ -89,6 +91,7 @@ public function test_doesnt_register_woocommerce_filter_on_frontend( $filter, $f
$this->tear_down();
$this->backend_currencies = new BackendCurrencies( $this->mock_multi_currency, $this->mock_localization_service );
+ $this->backend_currencies->init_hooks();
$this->assertFalse(
has_filter( $filter, [ $this->backend_currencies, $function_name ] ),
diff --git a/tests/unit/multi-currency/test-class-currency-switcher-block.php b/tests/unit/multi-currency/test-class-currency-switcher-block.php
index 843662ab39a..5e42e7e6659 100644
--- a/tests/unit/multi-currency/test-class-currency-switcher-block.php
+++ b/tests/unit/multi-currency/test-class-currency-switcher-block.php
@@ -52,6 +52,7 @@ public function set_up() {
$this->mock_multi_currency,
$this->mock_compatibility
);
+ $this->currency_switcher_block->init_hooks();
}
/**
diff --git a/tests/unit/multi-currency/test-class-frontend-currencies.php b/tests/unit/multi-currency/test-class-frontend-currencies.php
index fbf24d404a2..ea7302e7c29 100644
--- a/tests/unit/multi-currency/test-class-frontend-currencies.php
+++ b/tests/unit/multi-currency/test-class-frontend-currencies.php
@@ -66,6 +66,7 @@ public function set_up() {
->willReturn( new Currency( 'USD' ) );
$this->frontend_currencies = new FrontendCurrencies( $this->mock_multi_currency, $this->mock_localization_service, $this->mock_utils, $this->mock_compatibility );
+ $this->frontend_currencies->init_hooks();
}
public function tear_down() {
diff --git a/tests/unit/multi-currency/test-class-frontend-prices.php b/tests/unit/multi-currency/test-class-frontend-prices.php
index 2a1e8cac6b7..db34cc1b40d 100644
--- a/tests/unit/multi-currency/test-class-frontend-prices.php
+++ b/tests/unit/multi-currency/test-class-frontend-prices.php
@@ -40,6 +40,7 @@ public function set_up() {
$this->mock_multi_currency = $this->createMock( WCPay\MultiCurrency\MultiCurrency::class );
$this->frontend_prices = new WCPay\MultiCurrency\FrontendPrices( $this->mock_multi_currency, $this->mock_compatibility );
+ $this->frontend_prices->init_hooks();
}
public function tear_down() {
diff --git a/tests/unit/multi-currency/test-class-multi-currency.php b/tests/unit/multi-currency/test-class-multi-currency.php
index 8f08af4d7c6..e5a360e2f5a 100644
--- a/tests/unit/multi-currency/test-class-multi-currency.php
+++ b/tests/unit/multi-currency/test-class-multi-currency.php
@@ -306,7 +306,7 @@ public function test_set_enabled_currencies_throws_exception_on_unavailable_curr
// Arrange/Assert: Set expected exception and message.
$this->expectException( InvalidCurrencyException::class );
- $this->expectExceptionMessage( 'Invalid currency/currencies passed to set_enabled_currencies: banana' );
+ $this->expectExceptionMessage( 'Invalid currency passed to set_enabled_currencies: banana' );
// Act: Set the currencies.
$this->multi_currency->set_enabled_currencies( $currencies );
@@ -662,6 +662,72 @@ public function test_get_price_converts_using_ceil_and_precision( $target_price,
$this->assertSame( $expected, $this->multi_currency->get_price( 1, 'shipping' ) );
}
+ /**
+ * @dataProvider get_raw_conversion_provider
+ */
+ public function test_get_raw_conversion( $amount, $to_currency, $from_currency ) {
+ // Arrange: Get the expected amount.
+ $expected = $amount;
+ if ( '' !== $from_currency ) {
+ $expected = $expected * ( 1 / $this->mock_available_currencies[ $from_currency ] );
+ }
+ $expected = $expected * $this->mock_available_currencies[ $to_currency ];
+
+ // Act/Assert: Confirm the expected amount is returned.
+ $this->assertSame( $expected, $this->multi_currency->get_raw_conversion( $amount, $to_currency, $from_currency ) );
+ }
+
+ public function get_raw_conversion_provider() {
+ return [
+ 'CAD' => [ 10.00, 'CAD', '' ],
+ 'GBP CAD' => [ 10.00, 'GBP', 'CAD' ],
+ 'CAD GBP' => [ 10.00, 'CAD', 'GBP' ],
+ ];
+ }
+
+ /**
+ * @dataProvider get_raw_conversion_exception_provider
+ */
+ public function test_get_raw_conversion_throws_exception_on_unavailable_currency( $amount, $to_currency, $from_currency ) {
+ // Arrange/Assert: Set expected exception and message.
+ $this->expectException( InvalidCurrencyException::class );
+ $this->expectExceptionMessage( 'Invalid currency passed to get_raw_conversion: BANANA' );
+
+ // Act: Attempt to get the conversion.
+ $this->multi_currency->get_raw_conversion( $amount, $to_currency, $from_currency );
+ }
+
+ public function get_raw_conversion_exception_provider() {
+ return [
+ 'CAD banana' => [ 10.00, 'CAD', 'banana' ],
+ 'banana CAD' => [ 10.00, 'banana', 'CAD' ],
+ ];
+ }
+
+ public function test_get_raw_conversion_throws_exception_on_invalid_from_rate() {
+ // Arrange: Update a valid currency to be enabled and have a zero conversion rate.
+ $this->mock_enabled_currencies[] = 'BAM';
+ $this->mock_available_currencies['BAM'] = 0;
+
+ // Arrange: Add the new available currencies to the cache.
+ $this->mock_cached_currencies = [
+ 'currencies' => $this->mock_available_currencies,
+ 'updated' => $this->timestamp_for_testing,
+ 'expires' => $this->timestamp_for_testing + DAY_IN_SECONDS,
+ ];
+
+ // Arrange: Update the enabled currencies in the db and init MC again.
+ update_option( self::ENABLED_CURRENCIES_OPTION, $this->mock_enabled_currencies );
+ $this->init_multi_currency();
+
+ // Arrange/Assert: Set expected exception and message.
+ $this->expectException( InvalidCurrencyRateException::class );
+ $this->expectExceptionMessage( 'Invalid rate for from_currency in get_raw_conversion: 0' );
+
+ // Act: Attempt to get the conversion.
+ $this->multi_currency->get_raw_conversion( 10, 'CAD', 'BAM' );
+ }
+
public function test_get_cached_currencies_with_no_server_connection() {
// Need to create a new instance of MultiCurrency with a different $mock_api_client
// Because the mock return value of 'is_server_connected' cannot be overridden.
@@ -987,6 +1053,8 @@ public function get_has_additional_currencies_enabled_provider() {
}
public function test_get_all_customer_currencies() {
+ delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
+
$mock_orders = [];
$mock_orders[] = $this->add_mock_order_with_currency_meta( 'GBP' );
@@ -995,21 +1063,10 @@ public function test_get_all_customer_currencies() {
$mock_database_cache = $this->createMock( Database_Cache::class );
$mock_database_cache
- ->expects( $this->exactly( 2 ) )
+ ->expects( $this->once() )
->method( 'get_or_add' )
- ->withConsecutive(
- [ Database_Cache::CURRENCIES_KEY, $this->anything(), $this->anything() ],
- [ Database_Cache::CUSTOMER_CURRENCIES_KEY, $this->anything(), $this->anything() ]
- )->willReturnCallback(
- function( $key, $generator, $validator ) {
- if ( Database_Cache::CURRENCIES_KEY === $key ) {
- return $this->mock_cached_currencies;
- } else {
- // If calling the get all customer currencies function, run the callback function.
- return $generator();
- }
- }
- );
+ ->with( Database_Cache::CURRENCIES_KEY, $this->anything(), $this->anything() )
+ ->willReturn( $this->mock_cached_currencies );
$this->init_multi_currency( null, true, null, $mock_database_cache );
@@ -1022,6 +1079,68 @@ function( $key, $generator, $validator ) {
}
}
+ public function test_get_all_customer_currencies_with_option_data() {
+ $mock_option_data = [ 'GBP', 'EUR', 'USD' ];
+ update_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY, $mock_option_data );
+
+ $mock_database_cache = $this->createMock( Database_Cache::class );
+ $mock_database_cache
+ ->expects( $this->once() )
+ ->method( 'get_or_add' )
+ ->with( Database_Cache::CURRENCIES_KEY, $this->anything(), $this->anything() )
+ ->willReturn( $this->mock_cached_currencies );
+
+ $this->init_multi_currency( null, true, null, $mock_database_cache );
+
+ $result = $this->multi_currency->get_all_customer_currencies();
+
+ $this->assertEquals( [ 'GBP', 'EUR', 'USD' ], $result );
+
+ delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
+ }
+
+ /**
+ * Tests that if the option data is invalid, the currencies are fetched from the database.
+ *
+ * @dataProvider get_all_customer_currencies_with_invalid_option_data_provider
+ */
+ public function test_get_all_customer_currencies_with_invalid_option_data( $option_data ) {
+ update_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY, $option_data );
+
+ $mock_orders = [];
+
+ $mock_orders[] = $this->add_mock_order_with_currency_meta( 'GBP' );
+ $mock_orders[] = $this->add_mock_order_with_currency_meta( 'EUR' );
+ $mock_orders[] = $this->add_mock_order_with_currency_meta( 'USD' );
+
+ $mock_database_cache = $this->createMock( Database_Cache::class );
+ $mock_database_cache
+ ->expects( $this->once() )
+ ->method( 'get_or_add' )
+ ->with( Database_Cache::CURRENCIES_KEY, $this->anything(), $this->anything() )
+ ->willReturn( $this->mock_cached_currencies );
+
+ $this->init_multi_currency( null, true, null, $mock_database_cache );
+
+ $result = $this->multi_currency->get_all_customer_currencies();
+
+ $this->assertEquals( [ 'EUR', 'GBP', 'USD' ], $result );
+
+ foreach ( $mock_orders as $order_id ) {
+ wp_delete_post( $order_id, true );
+ }
+
+ delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
+ }
+
+ public function get_all_customer_currencies_with_invalid_option_data_provider() {
+ return [
+ 'Empty string' => [ '' ],
+ 'Invalid data' => [ 'invalid-data' ],
+ 'Empty array' => [ [] ],
+ ];
+ }
+
public function test_get_store_currencies_returns_expected() {
// Arrange: Get the expected values.
$expected = [
@@ -1224,6 +1343,75 @@ public function update_single_currency_settings_throws_exception_on_invalid_curr
];
}
+ public function test_maybe_update_customer_currencies_option() {
+ $customer_currencies = [ 'EUR', 'USD' ];
+ update_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY, $customer_currencies );
+
+ $order = wc_create_order();
+ $order->set_currency( 'GBP' );
+ $order->save();
+
+ $this->multi_currency->maybe_update_customer_currencies_option( $order->get_id() );
+
+ $expected = array_merge( $customer_currencies, [ 'GBP' ] );
+
+ $this->assertEquals( $expected, get_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY ) );
+
+ delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
+ }
+
+ public function test_maybe_update_customer_currencies_option_currency_is_already_included() {
+ $customer_currencies = [ 'EUR', 'USD' ];
+ update_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY, $customer_currencies );
+
+ $order = wc_create_order();
+ $order->set_currency( 'USD' );
+ $order->save();
+
+ $this->multi_currency->maybe_update_customer_currencies_option( $order->get_id() );
+
+ $this->assertEquals( $customer_currencies, get_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY ) );
+
+ delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
+ }
+
+ public function test_maybe_update_customer_currencies_option_invalid() {
+ $customer_currencies = 'invalid-data';
+ update_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY, $customer_currencies );
+
+ $order = wc_create_order();
+ $order->set_currency( 'USD' );
+ $order->save();
+
+ $order = wc_create_order();
+ $order->set_currency( 'EUR' );
+ $order->save();
+
+ $this->multi_currency->maybe_update_customer_currencies_option( $order->get_id() );
+ $expected = [ 'EUR', 'USD' ];
+
+ $this->assertEquals( $expected, get_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY ) );
+
+ delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
+ }
+
+ public function test_maybe_update_customer_currencies_option_does_not_exist() {
+ delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
+
+ $order = wc_create_order();
+ $order->set_currency( 'BRL' );
+ $order->save();
+
+ $order = wc_create_order();
+ $order->set_currency( 'EUR' );
+ $order->save();
+
+ $this->multi_currency->maybe_update_customer_currencies_option( $order->get_id() );
+ $expected = [ 'BRL', 'EUR' ];
+
+ $this->assertEquals( $expected, get_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY ) );
+ }
+
private function mock_currency_settings( $currency_code, $settings ) {
foreach ( $settings as $setting => $value ) {
update_option( 'wcpay_multi_currency_' . $setting . '_' . strtolower( $currency_code ), $value );
diff --git a/tests/unit/multi-currency/test-class-payment-methods-compatibility.php b/tests/unit/multi-currency/test-class-payment-methods-compatibility.php
index 2dfa1db645b..b71c305ef2d 100644
--- a/tests/unit/multi-currency/test-class-payment-methods-compatibility.php
+++ b/tests/unit/multi-currency/test-class-payment-methods-compatibility.php
@@ -59,6 +59,7 @@ public function set_up() {
->getMock();
$this->payment_methods_compatibility = new \WCPay\MultiCurrency\PaymentMethodsCompatibility( $this->multi_currency_mock, $this->gateway_mock );
+ $this->payment_methods_compatibility->init_hooks();
add_filter( 'pre_option__wcpay_feature_upe', [ $this, 'mock_upe_flag' ], 50, 3 );
}
diff --git a/tests/unit/multi-currency/test-class-rest-controller.php b/tests/unit/multi-currency/test-class-rest-controller.php
index a44ea71fb50..49ac60ad1be 100644
--- a/tests/unit/multi-currency/test-class-rest-controller.php
+++ b/tests/unit/multi-currency/test-class-rest-controller.php
@@ -83,7 +83,7 @@ public function test_update_enabled_currencies_throws_exception_on_unavailable_c
// Arrange: Set expected result.
$error_code = 'wcpay_multi_currency_invalid_currency';
- $error_message = 'Invalid currency/currencies passed to set_enabled_currencies: ' . implode( ', ', $error_currencies );
+ $error_message = 'Invalid currency passed to set_enabled_currencies: ' . implode( ', ', $error_currencies );
$expected = rest_ensure_response( new WP_Error( $error_code, $error_message ) );
// Arrange: Create the new REST request.
diff --git a/tests/unit/multi-currency/test-class-settings.php b/tests/unit/multi-currency/test-class-settings.php
index 7c49ec8e14c..1320ae9f0de 100644
--- a/tests/unit/multi-currency/test-class-settings.php
+++ b/tests/unit/multi-currency/test-class-settings.php
@@ -36,15 +36,13 @@ public function set_up() {
// The settings pages file is only included in woocommerce_get_settings_pages, so we need to manually include it here.
$this->settings = new WCPay\MultiCurrency\Settings( $this->mock_multi_currency );
+ $this->settings->init_hooks();
}
/**
* @dataProvider woocommerce_action_provider
*/
public function test_registers_internal_actions_with_account( $action, $function_name ) {
- // Init Settings again to get proper registration of hooks/filters.
- $this->settings = new WCPay\MultiCurrency\Settings( $this->mock_multi_currency );
-
$this->assertNotFalse(
has_action( $action, [ $this->settings, $function_name ] ),
"Action '$action' was not registered with '$function_name'"
diff --git a/tests/unit/multi-currency/test-class-tracking.php b/tests/unit/multi-currency/test-class-tracking.php
index 754d64399f4..af8fcccc41f 100644
--- a/tests/unit/multi-currency/test-class-tracking.php
+++ b/tests/unit/multi-currency/test-class-tracking.php
@@ -113,6 +113,7 @@ public function set_up() {
->willReturn( $this->mock_default_currency );
$this->tracking = new WCPay\MultiCurrency\Tracking( $this->mock_multi_currency );
+ $this->tracking->init_hooks();
}
public function tear_down() {
diff --git a/tests/unit/multi-currency/test-class-user-settings.php b/tests/unit/multi-currency/test-class-user-settings.php
index 97f3dacb10d..24026b71b94 100644
--- a/tests/unit/multi-currency/test-class-user-settings.php
+++ b/tests/unit/multi-currency/test-class-user-settings.php
@@ -43,6 +43,7 @@ public function set_up() {
);
$this->user_settings = new WCPay\MultiCurrency\UserSettings( $this->mock_multi_currency );
+ $this->user_settings->init_hooks();
}
public function test_add_presentment_currency_switch_renders_markup() {
diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php
index e52cac12c1d..fa2b0f3051b 100644
--- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php
+++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php
@@ -35,6 +35,8 @@
use Exception;
use WCPay\Duplicate_Payment_Prevention_Service;
use WC_Payments_Localization_Service;
+use WCPay\Internal\Service\Level3Service;
+use WCPay\Internal\Service\OrderService;
require_once dirname( __FILE__ ) . '/../helpers/class-wc-helper-site-currency.php';
@@ -93,11 +95,11 @@ class UPE_Payment_Gateway_Test extends WCPAY_UnitTestCase {
private $mock_rate_limiter;
/**
- * WC_Payments_Order_Service.
+ * Mock WC_Payments_Order_Service.
*
- * @var WC_Payments_Order_Service
+ * @var WC_Payments_Order_Service|PHPUnit_Framework_MockObject_MockObject
*/
- private $order_service;
+ private $mock_order_service;
/**
* Array of mock UPE payment methods.
@@ -240,7 +242,18 @@ public function set_up() {
$this->mock_payment_methods[ $mock_payment_method->get_id() ] = $mock_payment_method;
}
- $this->order_service = new WC_Payments_Order_Service( $this->mock_api_client );
+ $this->mock_order_service = $this->getMockBuilder( WC_Payments_Order_Service::class )
+ ->setConstructorArgs(
+ [
+ $this->mock_api_client,
+ ]
+ )
+ ->setMethods(
+ [
+ 'get_payment_method_id_for_order',
+ ]
+ )
+ ->getMock();
// Arrange: Mock UPE_Payment_Gateway so that some of its methods can be
// mocked, and their return values can be used for testing.
@@ -254,7 +267,7 @@ public function set_up() {
$this->mock_action_scheduler_service,
$this->mock_payment_methods,
$this->mock_rate_limiter,
- $this->order_service,
+ $this->mock_order_service,
$this->mock_dpps,
$this->mock_localization_service,
]
@@ -293,6 +306,20 @@ public function set_up() {
'wcpay-payment-method' => 'pm_mock',
'payment_method' => UPE_Payment_Gateway::GATEWAY_ID,
];
+
+ // Mock the level3 service to always return an empty array.
+ $mock_level3_service = $this->createMock( Level3Service::class );
+ $mock_level3_service->expects( $this->any() )
+ ->method( 'get_data_from_order' )
+ ->willReturn( [] );
+ wcpay_get_test_container()->replace( Level3Service::class, $mock_level3_service );
+
+ // Mock the order service to always return an empty array for meta.
+ $mock_order_service = $this->createMock( OrderService::class );
+ $mock_order_service->expects( $this->any() )
+ ->method( 'get_payment_metadata' )
+ ->willReturn( [] );
+ wcpay_get_test_container()->replace( OrderService::class, $mock_order_service );
}
/**
@@ -304,6 +331,7 @@ public function tear_down() {
parent::tear_down();
update_option( '_wcpay_feature_upe', '0' );
update_option( '_wcpay_feature_upe_split', '0' );
+ wcpay_get_test_container()->reset_all_replacements();
}
public function test_payment_fields_outputs_fields() {
@@ -372,47 +400,7 @@ public function test_update_payment_intent_adds_customer_save_payment_and_level3
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- [
- 'customer_name' => 'Jeroen Sormani',
- 'customer_email' => 'admin@example.org',
- 'site_url' => 'http://example.org',
- 'order_id' => $order_id,
- 'order_number' => $order_number,
- 'order_key' => $order->get_order_key(),
- 'payment_type' => Payment_Type::SINGLE(),
- 'gateway_type' => 'upe',
- 'checkout_type' => '',
- 'client_version' => WCPAY_VERSION_NUMBER,
- 'subscription_payment' => 'no',
- ]
- );
-
- $request->expects( $this->once() )
- ->method( 'set_level3' )
- ->with(
- [
- 'merchant_reference' => (string) $order_id,
- 'customer_reference' => (string) $order_id,
- 'shipping_amount' => 1000.0,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- 'product_code' => $product_item->get_product_id(),
- 'product_description' => 'Dummy Product',
- 'unit_cost' => 1000.0,
- 'quantity' => 4,
- 'tax_amount' => 0.0,
- 'discount_amount' => 0.0,
- ],
- ],
- ]
- );
+ ->with( [ 'gateway_type' => 'legacy_upe' ] );
$request->expects( $this->once() )
->method( 'format_response' )
@@ -474,47 +462,7 @@ public function test_update_payment_intent_with_selected_upe_payment_method() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- [
- 'customer_name' => 'Jeroen Sormani',
- 'customer_email' => 'admin@example.org',
- 'site_url' => 'http://example.org',
- 'order_id' => $order_id,
- 'order_number' => $order_number,
- 'order_key' => $order->get_order_key(),
- 'payment_type' => Payment_Type::SINGLE(),
- 'gateway_type' => 'upe',
- 'checkout_type' => '',
- 'client_version' => WCPAY_VERSION_NUMBER,
- 'subscription_payment' => 'no',
- ]
- );
-
- $request->expects( $this->once() )
- ->method( 'set_level3' )
- ->with(
- [
- 'merchant_reference' => (string) $order_id,
- 'shipping_amount' => 1000.0,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- 'product_code' => $product_item->get_product_id(),
- 'product_description' => 'Dummy Product',
- 'unit_cost' => 1000.0,
- 'quantity' => 4,
- 'tax_amount' => 0.0,
- 'discount_amount' => 0.0,
- ],
- ],
- 'customer_reference' => (string) $order_id,
- ]
- );
+ ->with( [ 'gateway_type' => 'legacy_upe' ] );
$request->expects( $this->once() )
->method( 'format_response' )
@@ -572,47 +520,7 @@ public function test_update_payment_intent_with_payment_country() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- [
- 'customer_name' => 'Jeroen Sormani',
- 'customer_email' => 'admin@example.org',
- 'site_url' => 'http://example.org',
- 'order_id' => $order_id,
- 'order_number' => $order_number,
- 'order_key' => $order->get_order_key(),
- 'payment_type' => Payment_Type::SINGLE(),
- 'gateway_type' => 'upe',
- 'checkout_type' => '',
- 'client_version' => WCPAY_VERSION_NUMBER,
- 'subscription_payment' => 'no',
- ]
- );
-
- $request->expects( $this->once() )
- ->method( 'set_level3' )
- ->with(
- [
- 'merchant_reference' => (string) $order_id,
- 'shipping_amount' => 1000.0,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- 'product_code' => $product_item->get_product_id(),
- 'product_description' => 'Dummy Product',
- 'unit_cost' => 1000.0,
- 'quantity' => 4,
- 'tax_amount' => 0.0,
- 'discount_amount' => 0.0,
- ],
- ],
- 'customer_reference' => (string) $order_id,
- ]
- );
+ ->with( [ 'gateway_type' => 'legacy_upe' ] );
$request->expects( $this->once() )
->method( 'format_response' )
@@ -640,13 +548,7 @@ public function test_create_payment_intent_uses_order_amount_if_order() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) {
- return isset( $metadata['order_number'] );
- }
- )
- );
+ ->with( [ 'order_number' => $order->get_order_number() ] );
$request->expects( $this->once() )
->method( 'set_payment_method_types' );
@@ -753,10 +655,10 @@ public function test_create_payment_intent_with_fingerprint() {
->expects( $this->once() )
->method( 'set_payment_method_types' )
->with( [ 'card' ] );
- $request
- ->expects( $this->once() )
+
+ $request->expects( $this->once() )
->method( 'set_metadata' )
- ->with( [ 'order_number' => $order_id ] );
+ ->with( [ 'order_number' => $order->get_order_number() ] );
$request
->expects( $this->once() )
->method( 'set_fingerprint' )
@@ -1444,6 +1346,89 @@ public function test_correct_payment_method_title_for_order() {
}
}
+ public function test_set_payment_method_title_for_email_updates_title() {
+ $mock_visa_details = [
+ 'type' => 'card',
+ 'card' => [
+ 'network' => 'visa',
+ 'funding' => 'debit',
+ ],
+ ];
+
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'get_payment_method_id_for_order' )
+ ->will(
+ $this->returnValue( 'pm_XXXXXXX' )
+ );
+
+ $this->mock_api_client
+ ->expects( $this->once() )
+ ->method( 'get_payment_method' )
+ ->will(
+ $this->returnValue( $mock_visa_details )
+ );
+
+ $order = WC_Helper_Order::create_order();
+ $order->set_payment_method( 'woocommerce_payments' );
+ $order->set_payment_method_title( 'Popular Payment Methods' );
+
+ $this->mock_upe_gateway->set_payment_method_title_for_email( $order );
+ $this->assertEquals( 'Visa debit card', $order->get_payment_method_title() );
+ }
+
+ public function test_correct_payment_method_title_for_order_when_set_for_email() {
+ $payment_methods = [
+ 'cheque' => 'Check payments',
+ 'cod' => 'Cash on delivery',
+ 'bacs' => 'Direct bank transfer',
+ ];
+
+ // Emulates order creation which sets the payment method and title.
+ $order = WC_Helper_Order::create_order();
+
+ foreach ( $payment_methods as $method => $title ) {
+ $order->set_payment_method( $method );
+ $order->set_payment_method_title( $title );
+
+ $this->mock_upe_gateway->set_payment_method_title_for_email( $order );
+ $this->assertEquals( $title, $order->get_payment_method_title() );
+ }
+ }
+
+ public function test_set_payment_method_title_for_email_fallback() {
+ $this->mock_order_service
+ ->expects( $this->once() )
+ ->method( 'get_payment_method_id_for_order' )
+ ->will(
+ $this->returnValue( '' )
+ );
+
+ $order = WC_Helper_Order::create_order();
+ $order->set_payment_method( 'woocommerce_payments' );
+ $order->set_payment_method_title( 'Popular Payment Methods' );
+
+ $this->mock_upe_gateway->set_payment_method_title_for_email( $order );
+ $this->assertEquals( 'WooPayments', $order->get_payment_method_title() );
+ }
+
+ public function test_set_payment_method_title_for_email_only_runs_for_legacy_upe() {
+ update_option( '_wcpay_feature_upe', '0' );
+ update_option( '_wcpay_feature_upe_split', '1' );
+
+ // set_payment_method_title_for_email should return before this functions runs.
+ $this->mock_order_service
+ ->expects( $this->never() )
+ ->method( 'get_payment_method_id_for_order' );
+
+ $order = WC_Helper_Order::create_order();
+ $order->set_payment_method( 'woocommerce_payments' );
+ $order->set_payment_method_title( 'Credit / Debit Card' );
+
+ $this->mock_upe_gateway->set_payment_method_title_for_email( $order );
+ $this->assertEquals( 'Credit / Debit Card', $order->get_payment_method_title() );
+ }
+
public function test_payment_methods_show_correct_default_outputs() {
$mock_token = WC_Helper_Token::create_token( 'pm_mock' );
$this->mock_token_service->expects( $this->any() )
@@ -1855,13 +1840,7 @@ public function test_process_payment_caches_mimimum_amount_and_displays_error_up
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) {
- return is_array( $metadata );
- }
- )
- );
+ ->with( [ 'gateway_type' => 'legacy_upe' ] );
$request->expects( $this->once() )
->method( 'set_level3' )
@@ -1904,8 +1883,7 @@ public function test_maybe_filter_gateway_title_with_no_additional_feature_flags
}
public function test_maybe_filter_gateway_title_skips_update_due_to_enabled_split_upe() {
- update_option( '_wcpay_feature_upe_split', '1' );
- update_option( '_wcpay_feature_upe_deferred_intent', '0' );
+ update_option( '_wcpay_feature_upe_deferred_intent', '1' );
$data = [
'methods' => [
@@ -1978,7 +1956,7 @@ public function test_remove_link_payment_method_if_card_disabled() {
$this->mock_action_scheduler_service,
$this->mock_payment_methods,
$this->mock_rate_limiter,
- $this->order_service,
+ $this->mock_order_service,
$this->mock_dpps,
$this->mock_localization_service,
]
@@ -2027,7 +2005,7 @@ public function test_link_payment_method_if_card_enabled() {
$this->mock_action_scheduler_service,
$this->mock_payment_methods,
$this->mock_rate_limiter,
- $this->order_service,
+ $this->mock_order_service,
$this->mock_dpps,
$this->mock_localization_service,
]
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 b6a2aa24a93..b5ea19ee3f9 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
@@ -38,6 +38,8 @@
use WCPay\Payment_Information;
use WC_Payments;
use WC_Payments_Localization_Service;
+use WCPay\Internal\Service\Level3Service;
+use WCPay\Internal\Service\OrderService;
require_once dirname( __FILE__ ) . '/../helpers/class-wc-helper-site-currency.php';
@@ -294,6 +296,7 @@ public function set_up() {
'get_payment_method_ids_enabled_at_checkout',
'wc_payments_get_payment_gateway_by_id',
'get_selected_payment_method',
+ 'get_upe_enabled_payment_method_ids',
]
)
->getMock();
@@ -328,6 +331,20 @@ public function set_up() {
$this->returnValueMap( $get_payment_gateway_by_id_return_value_map )
);
}
+
+ // Mock the level3 service to always return an empty array.
+ $mock_level3_service = $this->createMock( Level3Service::class );
+ $mock_level3_service->expects( $this->any() )
+ ->method( 'get_data_from_order' )
+ ->willReturn( [] );
+ wcpay_get_test_container()->replace( Level3Service::class, $mock_level3_service );
+
+ // Mock the order service to always return an empty array for meta.
+ $mock_order_service = $this->createMock( OrderService::class );
+ $mock_order_service->expects( $this->any() )
+ ->method( 'get_payment_metadata' )
+ ->willReturn( [] );
+ wcpay_get_test_container()->replace( OrderService::class, $mock_order_service );
}
/**
@@ -339,6 +356,7 @@ public function tear_down() {
parent::tear_down();
update_option( '_wcpay_feature_upe_split', '0' );
update_option( '_wcpay_feature_upe_deferred_intent', '0' );
+ wcpay_get_test_container()->reset_all_replacements();
}
/**
@@ -382,6 +400,29 @@ public function test_should_not_use_stripe_platform_on_checkout_page_for_upe() {
$this->assertFalse( $payment_gateway->should_use_stripe_platform_on_checkout_page() );
}
+ public function test_link_payment_method_requires_mandate_data() {
+ $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::CARD ];
+
+ $mock_upe_gateway
+ ->expects( $this->once() )
+ ->method( 'get_upe_enabled_payment_method_ids' )
+ ->will(
+ $this->returnValue( [ 'link' ] )
+ );
+
+ $this->assertTrue( $mock_upe_gateway->is_mandate_data_required() );
+ }
+
+ public function test_sepa_debit_payment_method_requires_mandate_data() {
+ $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::SEPA ];
+ $this->assertTrue( $mock_upe_gateway->is_mandate_data_required() );
+ }
+
+ public function test_non_required_mandate_data() {
+ $mock_gateway_not_requiring_mandate_data = $this->mock_payment_gateways[ Payment_Method::GIROPAY ];
+ $this->assertFalse( $mock_gateway_not_requiring_mandate_data->is_mandate_data_required() );
+ }
+
public function test_non_reusable_payment_method_is_not_available_when_subscription_is_in_cart() {
$non_reusable_payment_method = Payment_Method::BANCONTACT;
$payment_gateway = $this->mock_payment_gateways[ $non_reusable_payment_method ];
@@ -395,7 +436,6 @@ public function test_update_payment_intent_adds_customer_save_payment_and_level3
$order = WC_Helper_Order::create_order();
$order_id = $order->get_id();
$order_number = $order->get_order_number();
- $product_item = current( $order->get_items( 'line_item' ) );
$user = '';
$customer_id = 'cus_mock';
$save_payment_method = true;
@@ -407,42 +447,6 @@ public function test_update_payment_intent_adds_customer_save_payment_and_level3
->expects( $this->never() )
->method( 'create_customer_for_user' );
- $metadata = [
- 'customer_name' => 'Jeroen Sormani',
- 'customer_email' => 'admin@example.org',
- 'site_url' => 'http://example.org',
- 'order_id' => $order_id,
- 'order_number' => $order_number,
- 'order_key' => $order->get_order_key(),
- 'payment_type' => Payment_Type::SINGLE(),
- 'gateway_type' => 'split_upe',
- 'checkout_type' => '',
- 'client_version' => WCPAY_VERSION_NUMBER,
- 'subscription_payment' => 'no',
- ];
-
- $level3 = [
- 'merchant_reference' => (string) $order_id,
- 'customer_reference' => (string) $order_id,
- 'shipping_amount' => 1000.0,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- 'product_code' => $product_item->get_product_id(),
- 'product_description' => 'Dummy Product',
- 'unit_cost' => 1000.0,
- 'quantity' => 4,
- 'tax_amount' => 0.0,
- 'discount_amount' => 0.0,
- ],
- ],
- ];
-
// Test update_payment_intent on each payment gateway.
foreach ( $this->mock_payment_gateways as $mock_payment_gateway ) {
$request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent->get_id() );
@@ -450,8 +454,7 @@ public function test_update_payment_intent_adds_customer_save_payment_and_level3
$request->expects( $this->once() )->method( 'set_currency_code' )->with( 'usd' );
$request->expects( $this->once() )->method( 'setup_future_usage' );
$request->expects( $this->once() )->method( 'set_customer' )->with( 'cus_mock' );
- $request->expects( $this->once() )->method( 'set_metadata' )->with( $metadata );
- $request->expects( $this->once() )->method( 'set_level3' )->with( $level3 );
+ $request->expects( $this->once() )->method( 'set_metadata' )->with( [ 'gateway_type' => 'split_upe_with_deferred_intent_creation' ] );
$request->expects( $this->once() )
->method( 'format_response' )
->willReturn( $intent );
@@ -482,43 +485,6 @@ public function test_update_payment_intent_with_selected_upe_payment_method() {
->expects( $this->never() )
->method( 'create_customer_for_user' );
- $metadata = [
- 'customer_name' => 'Jeroen Sormani',
- 'customer_email' => 'admin@example.org',
- 'site_url' => 'http://example.org',
- 'order_id' => $order_id,
- 'order_number' => $order_number,
- 'order_key' => $order->get_order_key(),
- 'payment_type' => Payment_Type::SINGLE(),
- 'gateway_type' => 'split_upe',
- 'checkout_type' => '',
- 'client_version' => WCPAY_VERSION_NUMBER,
- 'subscription_payment' => 'no',
-
- ];
-
- $level3 = [
- 'merchant_reference' => (string) $order_id,
- 'shipping_amount' => 1000.0,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- 'product_code' => $product_item->get_product_id(),
- 'product_description' => 'Dummy Product',
- 'unit_cost' => 1000.0,
- 'quantity' => 4,
- 'tax_amount' => 0.0,
- 'discount_amount' => 0.0,
- ],
- ],
- 'customer_reference' => (string) $order_id,
- ];
-
/**
* In order to test each gateway, we need to setup mock_api_client so that
* its input are mocked in sequence, matching the gateways.
@@ -529,8 +495,7 @@ public function test_update_payment_intent_with_selected_upe_payment_method() {
$request->expects( $this->once() )->method( 'set_currency_code' )->with( 'usd' );
$request->expects( $this->once() )->method( 'setup_future_usage' );
$request->expects( $this->once() )->method( 'set_customer' )->with( 'cus_mock' );
- $request->expects( $this->once() )->method( 'set_metadata' )->with( $metadata );
- $request->expects( $this->once() )->method( 'set_level3' )->with( $level3 );
+ $request->expects( $this->once() )->method( 'set_metadata' )->with( [ 'gateway_type' => 'split_upe_with_deferred_intent_creation' ] );
$request->expects( $this->once() )->method( 'set_payment_method_types' )->with( [ $payment_method_id ] );
$request->expects( $this->once() )
@@ -561,50 +526,13 @@ public function test_update_payment_intent_with_payment_country() {
->expects( $this->never() )
->method( 'create_customer_for_user' );
- $metadata = [
- 'customer_name' => 'Jeroen Sormani',
- 'customer_email' => 'admin@example.org',
- 'site_url' => 'http://example.org',
- 'order_id' => $order_id,
- 'order_number' => $order_number,
- 'order_key' => $order->get_order_key(),
- 'payment_type' => Payment_Type::SINGLE(),
- 'gateway_type' => 'split_upe',
- 'checkout_type' => '',
- 'client_version' => WCPAY_VERSION_NUMBER,
- 'subscription_payment' => 'no',
- ];
-
- $level3 = [
- 'merchant_reference' => (string) $order_id,
- 'shipping_amount' => 1000.0,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- 'product_code' => $product_item->get_product_id(),
- 'product_description' => 'Dummy Product',
- 'unit_cost' => 1000.0,
- 'quantity' => 4,
- 'tax_amount' => 0.0,
- 'discount_amount' => 0.0,
- ],
- ],
- 'customer_reference' => (string) $order_id,
- ];
-
// Test update_payment_intent on each payment gateway.
foreach ( $this->mock_payment_gateways as $mock_payment_gateway ) {
$request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent->get_id() );
$request->expects( $this->once() )->method( 'set_amount' )->with( 5000 );
$request->expects( $this->once() )->method( 'set_currency_code' )->with( 'usd' );
$request->expects( $this->once() )->method( 'set_customer' )->with( 'cus_mock' );
- $request->expects( $this->once() )->method( 'set_metadata' )->with( $metadata );
- $request->expects( $this->once() )->method( 'set_level3' )->with( $level3 );
+ $request->expects( $this->once() )->method( 'set_metadata' )->with( [ 'gateway_type' => 'split_upe_with_deferred_intent_creation' ] );
$request->expects( $this->once() )->method( 'set_payment_country' )->with( 'US' );
$request->expects( $this->once() )
->method( 'format_response' )
@@ -1225,7 +1153,6 @@ public function test_process_redirect_setup_intent_succeded() {
$save_payment_method = true;
$user = wp_get_current_user();
$intent_status = Intent_Status::SUCCEEDED;
- $intent_metadata = [ 'order_id' => (string) $order_id ];
$client_secret = 'cs_mock';
$customer_id = 'cus_mock';
$intent_id = 'si_mock';
@@ -2101,6 +2028,7 @@ public function test_remove_link_payment_method_if_card_disabled() {
}
public function test_link_payment_method_if_card_enabled() {
+ update_option( '_wcpay_feature_upe_deferred_intent', '1' );
WC_Helper_Site_Currency::$mock_site_currency = 'USD';
$mock_upe_gateway = $this->getMockBuilder( UPE_Split_Payment_Gateway::class )
@@ -2402,6 +2330,7 @@ public function test_get_payment_methods_without_request_context_or_token() {
* @return void
*/
public function test_get_payment_methods_from_gateway_id() {
+ update_option( '_wcpay_feature_upe_deferred_intent', '1' );
$order = WC_Helper_Order::create_order();
$mock_upe_gateway = $this->getMockBuilder( UPE_Split_Payment_Gateway::class )
->setConstructorArgs(
diff --git a/tests/unit/src/ContainerTest.php b/tests/unit/src/ContainerTest.php
index 370e9d08311..413befcd5a0 100644
--- a/tests/unit/src/ContainerTest.php
+++ b/tests/unit/src/ContainerTest.php
@@ -13,7 +13,7 @@
use stdClass;
use WCPay\Container;
use WCPay\Core\Mode;
-use WCPay\Internal\DependencyManagement\ContainerException;
+use WCPay\Vendor\League\Container\Exception\ContainerException;
use WCPay\Internal\DependencyManagement\ExtendedContainer;
use WCPay\Internal\DependencyManagement\DelegateContainer\WooContainer;
use WCPay\Internal\DependencyManagement\DelegateContainer\LegacyContainer;
@@ -122,6 +122,14 @@ public function test_container_loads_service() {
$this->assertInstanceOf( ExampleService::class, $service );
}
+ /**
+ * Checks if the container throws the right exception on `get` calls.
+ */
+ public function test_get_throws_container_exception() {
+ $this->expectException( ContainerException::class );
+ $this->sut->get( 'Class_Name_That_Does_Not_Exist' );
+ }
+
/**
* Checks if the delegate container provides a WooCommerce instance.
*/
diff --git a/tests/unit/src/Internal/DependencyManagement/DelegateContainer/LegacyContainerTest.php b/tests/unit/src/Internal/DependencyManagement/DelegateContainer/LegacyContainerTest.php
index 4a80d7660f3..584785b4621 100644
--- a/tests/unit/src/Internal/DependencyManagement/DelegateContainer/LegacyContainerTest.php
+++ b/tests/unit/src/Internal/DependencyManagement/DelegateContainer/LegacyContainerTest.php
@@ -11,7 +11,7 @@
use WCPay\Core\Mode;
use WCPay\Database_Cache;
use WCPay\Internal\DependencyManagement\DelegateContainer\LegacyContainer;
-use WCPay\Internal\DependencyManagement\ContainerException;
+use WCPay\Vendor\League\Container\Exception\ContainerException;
/**
* Tests for the legacy container.
@@ -48,6 +48,7 @@ public function available_classes_provider() {
[ \WC_Payments_Action_Scheduler_Service::class ],
[ \WC_Payments_Fraud_Service::class ],
[ \WC_Payments_Customer_Service::class ],
+ [ \WC_Payments_Order_Service::class ],
];
}
diff --git a/tests/unit/src/Internal/Payment/PaymentContextTest.php b/tests/unit/src/Internal/Payment/PaymentContextTest.php
new file mode 100644
index 00000000000..71ed252cfcf
--- /dev/null
+++ b/tests/unit/src/Internal/Payment/PaymentContextTest.php
@@ -0,0 +1,120 @@
+sut = new PaymentContext( $this->order_id );
+ }
+
+ public function test_order_id() {
+ $this->assertSame( $this->order_id, $this->sut->get_order_id() );
+ }
+
+ public function test_amount() {
+ $amount = 456;
+
+ $this->sut->set_amount( $amount );
+ $this->assertSame( $amount, $this->sut->get_amount() );
+ }
+ public function test_payment_method() {
+ $payment_method = new NewPaymentMethod( 'pm_XYZ' );
+
+ $this->sut->set_payment_method( $payment_method );
+ $this->assertSame( $payment_method, $this->sut->get_payment_method() );
+ }
+
+ public function test_currency() {
+ $currency = 'eur';
+
+ $this->sut->set_currency( $currency );
+ $this->assertSame( $currency, $this->sut->get_currency() );
+ }
+
+ public function test_manual_capture_disabled() {
+ $toggle_manual_capture = false;
+
+ $this->sut->toggle_manual_capture( $toggle_manual_capture );
+ $this->assertSame( $toggle_manual_capture, $this->sut->should_capture_manually() );
+ }
+
+ public function test_manual_capture_enabled() {
+ $toggle_manual_capture = true;
+
+ $this->sut->toggle_manual_capture( $toggle_manual_capture );
+ $this->assertSame( $toggle_manual_capture, $this->sut->should_capture_manually() );
+ }
+
+ public function test_metadata() {
+ $metadata = [ 'some_meta_key' => 'yes' ];
+
+ $this->sut->set_metadata( $metadata );
+ $this->assertSame( $metadata, $this->sut->get_metadata() );
+ }
+
+ public function test_level3_data() {
+ $level3_data = [ 'items' => [] ];
+
+ $this->sut->set_level3_data( $level3_data );
+ $this->assertSame( $level3_data, $this->sut->get_level3_data() );
+ }
+
+ public function test_cvc_confirmation() {
+ $cvc_confirmation = 'confirmation';
+
+ $this->sut->set_cvc_confirmation( $cvc_confirmation );
+ $this->assertSame( $cvc_confirmation, $this->sut->get_cvc_confirmation() );
+ }
+
+ public function test_fingerprint() {
+ $fingerprint = 'fingerprint';
+
+ $this->sut->set_fingerprint( $fingerprint );
+ $this->assertSame( $fingerprint, $this->sut->get_fingerprint() );
+ }
+
+ public function test_user_id() {
+ $user_id = 123;
+
+ $this->sut->set_user_id( $user_id );
+ $this->assertSame( $user_id, $this->sut->get_user_id() );
+ }
+
+ public function test_customer_id() {
+ $customer_id = 'cus_ZYX';
+
+ $this->sut->set_customer_id( $customer_id );
+ $this->assertSame( $customer_id, $this->sut->get_customer_id() );
+ }
+}
diff --git a/tests/unit/src/Internal/Payment/PaymentMethod/SavedPaymentMethodTest.php b/tests/unit/src/Internal/Payment/PaymentMethod/SavedPaymentMethodTest.php
index 64bf9ef95c5..691d83bf28e 100644
--- a/tests/unit/src/Internal/Payment/PaymentMethod/SavedPaymentMethodTest.php
+++ b/tests/unit/src/Internal/Payment/PaymentMethod/SavedPaymentMethodTest.php
@@ -7,8 +7,6 @@
namespace WCPay\Tests\Internal\Payment\PaymentMethod;
-use WC_Helper_Token;
-use WC_Payment_Token;
use WCPAY_UnitTestCase;
use WCPay\Internal\Payment\PaymentMethod\SavedPaymentMethod;
@@ -26,24 +24,37 @@ class SavedPaymentMethodTest extends WCPAY_UnitTestCase {
/**
* Saved token including the payment method.
*
- * @var WC_Payment_Token
+ * @var int
*/
- private $saved_token;
+ private $saved_token_id = 234;
+
+ /**
+ * Saved payment method ID.
+ *
+ * @var string
+ */
+ private $payment_method_id = 'pm_saved_as_woo_token';
+
protected function setUp(): void {
parent::setUp();
- $this->saved_token = WC_Helper_Token::create_token( 'pm_saved_as_woo_token' );
- $this->sut = new SavedPaymentMethod( $this->saved_token );
+
+ $this->sut = new SavedPaymentMethod( $this->payment_method_id, $this->saved_token_id );
}
public function test_get_id() {
- $this->assertSame( 'pm_saved_as_woo_token', $this->sut->get_id() );
+ $this->assertSame( $this->payment_method_id, $this->sut->get_id() );
+ }
+
+ public function test_get_token_id() {
+ $this->assertSame( $this->saved_token_id, $this->sut->get_token_id() );
}
public function test_get_data() {
$this->assertSame(
[
- 'type' => 'saved',
- 'id' => $this->saved_token->get_id(),
+ 'type' => 'saved',
+ 'id' => $this->payment_method_id,
+ 'token_id' => $this->saved_token_id,
],
$this->sut->get_data()
);
diff --git a/tests/unit/src/Internal/Payment/PaymentRequestTest.php b/tests/unit/src/Internal/Payment/PaymentRequestTest.php
index 0cde154fa20..2d1ac129b74 100644
--- a/tests/unit/src/Internal/Payment/PaymentRequestTest.php
+++ b/tests/unit/src/Internal/Payment/PaymentRequestTest.php
@@ -199,6 +199,12 @@ public function test_get_payment_return_saved_payment_method() {
$request
);
$mock_token = $this->createMock( WC_Payment_Token::class );
+ $mock_token->expects( $this->once() )
+ ->method( 'get_token' )
+ ->willReturn( 'pm_saved_method' );
+ $mock_token->expects( $this->once() )
+ ->method( 'get_id' )
+ ->willReturn( 123 );
$this->mock_legacy_proxy->expects( $this->once() )
->method( 'call_static' )
->with( WC_Payment_Tokens::class, 'get', 123456 )
@@ -209,12 +215,11 @@ public function test_get_payment_return_saved_payment_method() {
// Assert: correct type of instance.
$this->assertInstanceOf( SavedPaymentMethod::class, $pm );
-
- // Assert: the same payment method string saved in the token object.
- $mock_token->expects( $this->once() )
- ->method( 'get_token' )
- ->willReturn( 'pm_saved_method' );
- $this->assertSame( $pm->get_id(), 'pm_saved_method' );
+ if ( $pm instanceof SavedPaymentMethod ) { // Let IDEs understand the type.
+ // Assert: the same payment method string saved in the token object.
+ $this->assertSame( 'pm_saved_method', $pm->get_id() );
+ $this->assertSame( 123, $pm->get_token_id() );
+ }
}
public function test_get_payment_return_new_payment_method() {
@@ -244,4 +249,70 @@ public function test_get_payment_method_throw_exception_due_to_no_payment_method
$this->sut->get_payment_method();
}
+
+ public function provider_get_cvc_confirmation() {
+ return [
+ 'No payment method' => [
+ null,
+ null,
+ null,
+ ],
+ 'Payment method set, no CVC' => [
+ 'woocommerce_payments',
+ null,
+ null,
+ ],
+ 'Payment method set, new CVC' => [
+ 'woocommerce_payments',
+ 'new',
+ null,
+ ],
+ 'Payment method set, meaningful CVC' => [
+ 'woocommerce_payments',
+ 'xyz1234',
+ 'xyz1234',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provider_get_cvc_confirmation
+ */
+ public function test_get_cvc_confirmation( $payment_method, $cvc, $expected ) {
+ $request = [];
+ if ( $payment_method ) {
+ $request['payment_method'] = $payment_method;
+ }
+
+ if ( $cvc ) {
+ $request[ 'wc-' . $payment_method . '-payment-cvc-confirmation' ] = $cvc;
+ }
+
+ $sut = new PaymentRequest( $this->mock_legacy_proxy, $request );
+ $result = $sut->get_cvc_confirmation();
+ $this->assertSame( $expected, $result );
+ }
+
+ public function provider_get_fingerprint() {
+ return [
+ 'Nothing provided' => [ null, null ],
+ 'Empty string' => [ '', null ],
+ 'Normal string' => [ 'abc', 'abc' ],
+ 'Needs normalization' => [ 'mock_legacy_proxy, $request );
+ $result = $sut->get_fingerprint();
+ $this->assertSame( $expected, $result );
+ }
}
diff --git a/tests/unit/src/Internal/Payment/State/AbstractPaymentStateTest.php b/tests/unit/src/Internal/Payment/State/AbstractPaymentStateTest.php
new file mode 100644
index 00000000000..0635600fce3
--- /dev/null
+++ b/tests/unit/src/Internal/Payment/State/AbstractPaymentStateTest.php
@@ -0,0 +1,88 @@
+create_state( $state_class );
+ }
+}
+
+/**
+ * Tests for the base payment state class.
+ */
+class AbstractPaymentStateTest extends WCPAY_UnitTestCase {
+ /**
+ * Service under test.
+ *
+ * @var PureState
+ */
+ private $sut;
+
+ /**
+ * @var StateFactory|MockObject
+ */
+ private $mock_state_factory;
+
+ /**
+ * @var PaymentContext|MockObject
+ */
+ private $mock_context;
+
+ /**
+ * Set up the test.
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->mock_context = $this->createMock( PaymentContext::class );
+ $this->mock_state_factory = $this->createMock( StateFactory::class );
+
+ $this->sut = new PureState( $this->mock_state_factory );
+ $this->sut->set_context( $this->mock_context );
+ }
+
+ public function test_set_context_and_get_context_work() {
+ $mock_context = $this->createMock( PaymentContext::class );
+
+ $this->sut->set_context( $mock_context );
+ $this->assertSame( $mock_context, $this->sut->get_context() );
+ }
+
+ public function test_create_state_uses_the_factory() {
+ $mock_completed_state = $this->createMock( CompletedState::class );
+
+ $this->mock_state_factory->expects( $this->once() )
+ ->method( 'create_state' )
+ ->with( CompletedState::class, $this->mock_context )
+ ->willReturn( $mock_completed_state );
+
+ $result = $this->sut->create_test_state( CompletedState::class );
+ $this->assertSame( $mock_completed_state, $result );
+ }
+
+ public function test_process_throws_exception() {
+ $this->expectException( StateTransitionException::class );
+ $this->expectExceptionMessage( 'The WCPay\Internal\Payment\State\AbstractPaymentState::process method is not available in the current payment state (' . PureState::class . ').' );
+ $this->sut->process( $this->createMock( PaymentRequest::class ) );
+ }
+}
diff --git a/tests/unit/src/Internal/Payment/State/InitialStateTest.php b/tests/unit/src/Internal/Payment/State/InitialStateTest.php
new file mode 100644
index 00000000000..6e8a0b3f68c
--- /dev/null
+++ b/tests/unit/src/Internal/Payment/State/InitialStateTest.php
@@ -0,0 +1,257 @@
+mock_state_factory = $this->createMock( StateFactory::class );
+ $this->mock_order_service = $this->createMock( OrderService::class );
+ $this->mock_context = $this->createMock( PaymentContext::class );
+ $this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class );
+ $this->mock_level3_service = $this->createMock( Level3Service::class );
+ $this->mock_payment_request_service = $this->createMock( PaymentRequestService::class );
+
+ $this->sut = new InitialState(
+ $this->mock_state_factory,
+ $this->mock_order_service,
+ $this->mock_customer_service,
+ $this->mock_level3_service,
+ $this->mock_payment_request_service
+ );
+ $this->sut->set_context( $this->mock_context );
+ }
+
+ /**
+ * Different `process` scenarios.
+ *
+ * There will be a single parameter, representing an exception, if any.
+ *
+ * @return array
+ */
+ public function provider_process() {
+ return [
+ [ null ],
+ [ new Invalid_Request_Parameter_Exception( 'A parameter cannot be set.', 'invalid_parameter' ) ],
+ [ new Extend_Request_Exception( 'A parameter cannot be set.', 'cannot_extend' ) ],
+ ];
+ }
+
+ /**
+ * Ensures that the `process` method creates a new completed state.
+ *
+ * @param \Exception|null $exception Exception that would be thrown by intent creation.
+ * @dataProvider provider_process
+ */
+ public function test_process( Exception $exception = null ) {
+ $order_id = 123;
+ $mock_request = $this->createMock( PaymentRequest::class );
+ $mock_intent = $this->createMock( WC_Payments_API_Payment_Intention::class );
+
+ /**
+ * This test works with the root `process` method, which calls a few
+ * internal methods. We want to mock them for the purpose of this test.
+ *
+ * @var MockObject|InitialState
+ */
+ $this->sut = $this->getMockBuilder( InitialState::class )
+ ->onlyMethods( [ 'populate_context_from_request', 'populate_context_from_order' ] )
+ ->setConstructorArgs(
+ [
+ $this->mock_state_factory,
+ $this->mock_order_service,
+ $this->mock_customer_service,
+ $this->mock_level3_service,
+ $this->mock_payment_request_service,
+ ]
+ )
+ ->getMock();
+ $this->sut->set_context( $this->mock_context );
+
+ // There's a single call to get the order ID.
+ $this->mock_context->expects( $this->once() )
+ ->method( 'get_order_id' )
+ ->willReturn( $order_id );
+
+ // Verify that the context is populated.
+ $this->sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request );
+ $this->sut->expects( $this->once() )->method( 'populate_context_from_order' );
+
+ // Arrange intent creation.
+ if ( $exception ) {
+ $this->mock_payment_request_service->expects( $this->once() )
+ ->method( 'create_intent' )
+ ->with( $this->mock_context )
+ ->willThrowException( $exception );
+ } else {
+ $this->mock_payment_request_service->expects( $this->once() )
+ ->method( 'create_intent' )
+ ->with( $this->mock_context )
+ ->willReturn( $mock_intent );
+
+ // Assert order update.
+ $this->mock_order_service->expects( $this->once() )
+ ->method( 'update_order_from_successful_intent' )
+ ->with( $order_id, $mock_intent, $this->mock_context );
+ }
+
+ // Arrange the final state.
+ $state_class = $exception ? SystemErrorState::class : CompletedState::class;
+ $mock_final_state = $this->createMock( $state_class );
+ $this->mock_state_factory->expects( $this->once() )
+ ->method( 'create_state' )
+ ->with( $state_class, $this->mock_context )
+ ->willReturn( $mock_final_state );
+
+ // Act: Process.
+ $result = $this->sut->process( $mock_request );
+
+ // Assert: Successful transition.
+ $this->assertSame( $mock_final_state, $result );
+ }
+
+ public function test_populate_context_from_request() {
+ $payment_method = new NewPaymentMethod( 'pm_123' );
+ $fingerprint = 'fingerprint';
+ $cvc_confirmation = 'CVCConfirmation';
+
+ // Setup the mock request.
+ $mock_request = $this->createMock( PaymentRequest::class );
+ $mock_request->expects( $this->once() )->method( 'get_payment_method' )->willReturn( $payment_method );
+ $mock_request->expects( $this->once() )->method( 'get_cvc_confirmation' )->willReturn( $cvc_confirmation );
+ $mock_request->expects( $this->once() )->method( 'get_fingerprint' )->willReturn( $fingerprint );
+
+ // Assume that everything from the request would be imported into the context.
+ $this->mock_context->expects( $this->once() )->method( 'set_payment_method' )->with( $payment_method );
+ $this->mock_context->expects( $this->once() )->method( 'set_cvc_confirmation' )->with( $cvc_confirmation );
+ $this->mock_context->expects( $this->once() )->method( 'set_fingerprint' )->with( $fingerprint );
+
+ // Use reflection to acces.
+ PHPUnit_Utils::call_method( $this->sut, 'populate_context_from_request', [ $mock_request ] );
+ }
+
+ public function test_populate_context_from_order() {
+ $order_id = 123;
+ $user_id = 456;
+ $customer_id = 'cus_123';
+ $metadata = [ 'sample' => 'true' ];
+ $level3_data = [ 'items' => [] ];
+ $mock_order = $this->createMock( WC_Order::class );
+
+ // Prepare the order ID.
+ $this->mock_context->expects( $this->once() )
+ ->method( 'get_order_id' )
+ ->willReturn( $order_id );
+
+ // Arrange the import of order data to the payment.
+ $this->mock_order_service->expects( $this->once() )
+ ->method( 'import_order_data_to_payment_context' )
+ ->with( $order_id, $this->mock_context );
+
+ // Arrange metadata import.
+ $this->mock_order_service->expects( $this->once() )
+ ->method( 'get_payment_metadata' )
+ ->with( $order_id )
+ ->willReturn( $metadata );
+ $this->mock_context->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( array_merge( $metadata, [ 'gateway_type' => 'src' ] ) );
+
+ // Arrange level 3 data import.
+ $this->mock_level3_service->expects( $this->once() )
+ ->method( 'get_data_from_order' )
+ ->with( $order_id )
+ ->willReturn( $level3_data );
+ $this->mock_context->expects( $this->once() )
+ ->method( 'set_level3_data' )
+ ->with( $level3_data );
+
+ // Arrange customer management.
+ $this->mock_context->expects( $this->once() )
+ ->method( 'get_user_id' )
+ ->willReturn( $user_id );
+ $this->mock_order_service->expects( $this->once() )
+ ->method( '_deprecated_get_order' )
+ ->with( $order_id )
+ ->willReturn( $mock_order );
+ $this->mock_customer_service->expects( $this->once() )
+ ->method( 'get_or_create_customer_id_from_order' )
+ ->with( $user_id, $mock_order )
+ ->willReturn( $customer_id );
+ $this->mock_context->expects( $this->once() )
+ ->method( 'set_customer_id' )
+ ->with( $customer_id );
+
+ PHPUnit_Utils::call_method( $this->sut, 'populate_context_from_order', [] );
+ }
+}
diff --git a/tests/unit/src/Internal/Payment/State/StateFactoryTest.php b/tests/unit/src/Internal/Payment/State/StateFactoryTest.php
new file mode 100644
index 00000000000..5044d6894a6
--- /dev/null
+++ b/tests/unit/src/Internal/Payment/State/StateFactoryTest.php
@@ -0,0 +1,84 @@
+mock_container = $this->createMock( Container::class );
+ $this->mock_context = $this->createMock( PaymentContext::class );
+
+ $this->sut = new StateFactory( $this->mock_container );
+ }
+
+ public function test_create_state_creates_state() {
+ $mock_state = $this->createMock( InitialState::class );
+ $mock_state->expects( $this->once() )
+ ->method( 'set_context' )
+ ->with( $this->mock_context );
+
+ $this->mock_container->expects( $this->once() )
+ ->method( 'get' )
+ ->with( InitialState::class )
+ ->willReturn( $mock_state );
+
+ $result = $this->sut->create_state( InitialState::class, $this->mock_context );
+ $this->assertSame( $mock_state, $result );
+ }
+
+ public function test_create_state_rejects_invalid_classes() {
+ $this->expectException( StateTransitionException::class );
+ $this->sut->create_state( PaymentProcessingService::class, $this->mock_context );
+ }
+}
diff --git a/tests/unit/src/Internal/Proxy/HooksProxyTest.php b/tests/unit/src/Internal/Proxy/HooksProxyTest.php
index 2015a05e2fc..59a833f27b6 100644
--- a/tests/unit/src/Internal/Proxy/HooksProxyTest.php
+++ b/tests/unit/src/Internal/Proxy/HooksProxyTest.php
@@ -83,4 +83,18 @@ public function test_add_action() {
$result = do_action( $hook_name, 1, 2, 3 );
$this->assertNull( $result ); // Non-null would be a filter.
}
+
+ public function test_apply_filters() {
+ $hook_name = 'proxy_test_filter';
+
+ $this->helper->expects( $this->once() )
+ ->method( 'action' )
+ ->with( 1, 2, 3 )
+ ->willReturn( 4 );
+
+ $this->sut->add_filter( $hook_name, [ $this->helper, 'action' ], 11, 3 );
+
+ $result = $this->sut->apply_filters( $hook_name, 1, 2, 3 );
+ $this->assertEquals( 4, $result ); // Non-null would be a filter.
+ }
}
diff --git a/tests/unit/src/Internal/Service/Level3ServiceTest.php b/tests/unit/src/Internal/Service/Level3ServiceTest.php
new file mode 100644
index 00000000000..553c54f862c
--- /dev/null
+++ b/tests/unit/src/Internal/Service/Level3ServiceTest.php
@@ -0,0 +1,518 @@
+mock_order_service = $this->createMock( OrderService::class );
+ $this->mock_account = $this->createMock( WC_Payments_Account::class );
+ $this->legacy_proxy = $this->createMock( LegacyProxy::class );
+
+ // Main service under test: Level3Service.
+ $this->sut = new Level3Service(
+ $this->mock_order_service,
+ $this->mock_account,
+ $this->legacy_proxy
+ );
+ }
+
+ protected function create_mock_item( $name, $quantity, $subtotal, $total_tax, $product_id, $variable = false ) {
+ // Setup the item.
+ $mock_item = $this
+ ->getMockBuilder( WC_Order_Item_Product::class )
+ ->disableOriginalConstructor()
+ ->onlyMethods(
+ [
+ 'get_name',
+ 'get_quantity',
+ 'get_subtotal',
+ 'get_total_tax',
+ 'get_total',
+ 'get_variation_id',
+ 'get_product_id',
+ ]
+ )
+ ->getMock();
+
+ $mock_item
+ ->method( 'get_name' )
+ ->will( $this->returnValue( $name ) );
+
+ $mock_item
+ ->method( 'get_quantity' )
+ ->will( $this->returnValue( $quantity ) );
+
+ $mock_item
+ ->method( 'get_total' )
+ ->will( $this->returnValue( $subtotal ) );
+
+ $mock_item
+ ->method( 'get_subtotal' )
+ ->will( $this->returnValue( $subtotal ) );
+
+ $mock_item
+ ->method( 'get_total_tax' )
+ ->will( $this->returnValue( $total_tax ) );
+
+ $mock_item
+ ->method( 'get_variation_id' )
+ ->will( $this->returnValue( $variable ? 789 : false ) );
+
+ $mock_item
+ ->method( 'get_product_id' )
+ ->will( $this->returnValue( $product_id ) );
+
+ return $mock_item;
+ }
+
+ protected function mock_level_3_order(
+ $shipping_postcode,
+ $with_fee = false,
+ $with_negative_price_product = false,
+ $quantity = 1,
+ $basket_size = 1,
+ $product_id = 30,
+ $variable = false
+ ) {
+ $mock_items[] = $this->create_mock_item( 'Beanie with Logo', $quantity, 18, 2.7, $product_id, $variable );
+
+ if ( $with_fee ) {
+ // Setup the fee.
+ $mock_fee = $this
+ ->getMockBuilder( WC_Order_Item_Fee::class )
+ ->disableOriginalConstructor()
+ ->onlyMethods( [ 'get_name', 'get_quantity', 'get_total_tax', 'get_total' ] )
+ ->getMock();
+
+ $mock_fee
+ ->method( 'get_name' )
+ ->will( $this->returnValue( 'fee' ) );
+
+ $mock_fee
+ ->method( 'get_quantity' )
+ ->will( $this->returnValue( 1 ) );
+
+ $mock_fee
+ ->method( 'get_total' )
+ ->will( $this->returnValue( 10 ) );
+
+ $mock_fee
+ ->method( 'get_total_tax' )
+ ->will( $this->returnValue( 1.5 ) );
+
+ $mock_items[] = $mock_fee;
+ }
+
+ if ( $with_negative_price_product ) {
+ $mock_items[] = $this->create_mock_item( 'Negative Product Price', $quantity, -18.99, 2.7, 42 );
+ }
+
+ if ( $basket_size > 1 ) {
+ // Keep the formely created item/fee and add duplicated items to the basket.
+ $mock_items = array_merge( $mock_items, array_fill( 0, $basket_size - 1, $mock_items[0] ) );
+ }
+
+ // Setup the order.
+ $mock_order = $this
+ ->getMockBuilder( WC_Order::class )
+ ->disableOriginalConstructor()
+ ->onlyMethods(
+ [
+ 'get_id',
+ 'get_items',
+ 'get_currency',
+ 'get_shipping_total',
+ 'get_shipping_tax',
+ 'get_shipping_postcode',
+ ]
+ )
+ ->getMock();
+
+ $mock_order
+ ->method( 'get_id' )
+ ->will( $this->returnValue( 210 ) );
+
+ $mock_order
+ ->method( 'get_items' )
+ ->will( $this->returnValue( $mock_items ) );
+
+ $mock_order
+ ->method( 'get_currency' )
+ ->will( $this->returnValue( 'USD' ) );
+
+ $mock_order
+ ->method( 'get_shipping_total' )
+ ->will( $this->returnValue( 30 ) );
+
+ $mock_order
+ ->method( 'get_shipping_tax' )
+ ->will( $this->returnValue( 8 ) );
+
+ $mock_order
+ ->method( 'get_shipping_postcode' )
+ ->will( $this->returnValue( $shipping_postcode ) );
+
+ $this->mock_order_service->expects( $this->once() )
+ ->method( '_deprecated_get_order' )
+ ->with( $this->order_id )
+ ->willReturn( $mock_order );
+ }
+
+ public function test_full_level3_data() {
+ $expected_data = [
+ 'merchant_reference' => '210',
+ 'customer_reference' => '210',
+ 'shipping_amount' => 3800,
+ 'line_items' => [
+ (object) [
+ 'product_code' => 789,
+ 'product_description' => 'Beanie with Logo',
+ 'unit_cost' => 1800,
+ 'quantity' => 1.0,
+ 'tax_amount' => 270,
+ 'discount_amount' => 0,
+ ],
+ ],
+ 'shipping_address_zip' => '98012',
+ 'shipping_from_zip' => '94110',
+ ];
+
+ $this->legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'get_option', 'woocommerce_store_postcode' )
+ ->willReturn( '94110' );
+
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '98012', false, false, 1, 1, 30, true );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertEquals( $expected_data, $level_3_data );
+ }
+
+ public function test_full_level3_data_with_product_id_longer_than_12_characters() {
+ $expected_data = [
+ 'merchant_reference' => '210',
+ 'customer_reference' => '210',
+ 'shipping_amount' => 3800,
+ 'line_items' => [
+ (object) [
+ 'product_code' => 123456789123,
+ 'product_description' => 'Beanie with Logo',
+ 'unit_cost' => 1800,
+ 'quantity' => 1,
+ 'tax_amount' => 270,
+ 'discount_amount' => 0,
+ ],
+ ],
+ 'shipping_address_zip' => '98012',
+ 'shipping_from_zip' => '94110',
+ ];
+
+ $this->legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'get_option', 'woocommerce_store_postcode' )
+ ->willReturn( '94110' );
+
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '98012', false, false, 1, 1, 123456789123456 );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertEquals( $expected_data, $level_3_data );
+ }
+
+ public function test_full_level3_data_with_fee() {
+ $expected_data = [
+ 'merchant_reference' => '210',
+ 'customer_reference' => '210',
+ 'shipping_amount' => 3800,
+ 'line_items' => [
+ (object) [
+ 'product_code' => 30,
+ 'product_description' => 'Beanie with Logo',
+ 'unit_cost' => 1800,
+ 'quantity' => 1,
+ 'tax_amount' => 270,
+ 'discount_amount' => 0,
+ ],
+ (object) [
+ 'product_code' => 'fee',
+ 'product_description' => 'fee',
+ 'unit_cost' => 1000,
+ 'quantity' => 1,
+ 'tax_amount' => 150,
+ 'discount_amount' => 0,
+ ],
+ ],
+ 'shipping_address_zip' => '98012',
+ 'shipping_from_zip' => '94110',
+ ];
+
+ $this->legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'get_option', 'woocommerce_store_postcode' )
+ ->willReturn( '94110' );
+
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '98012', true );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertEquals( $expected_data, $level_3_data );
+ }
+
+ public function test_full_level3_data_with_negative_price_product() {
+ $expected_data = [
+ 'merchant_reference' => '210',
+ 'customer_reference' => '210',
+ 'shipping_amount' => 3800,
+ 'line_items' => [
+ (object) [
+ 'product_code' => 30,
+ 'product_description' => 'Beanie with Logo',
+ 'unit_cost' => 1800,
+ 'quantity' => 1,
+ 'tax_amount' => 270,
+ 'discount_amount' => 0,
+ ],
+ (object) [
+ 'product_code' => 42,
+ 'product_description' => 'Negative Product Price',
+ 'unit_cost' => 0,
+ 'quantity' => 1,
+ 'tax_amount' => 270,
+ 'discount_amount' => 1899,
+ ],
+ ],
+ 'shipping_address_zip' => '98012',
+ 'shipping_from_zip' => '94110',
+ ];
+
+ $this->legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'get_option', 'woocommerce_store_postcode' )
+ ->willReturn( '94110' );
+
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '98012', false, true, 1, 1 );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertEquals( $expected_data, $level_3_data );
+ }
+
+ public function test_us_store_level_3_data() {
+ // Use a non-us customer postcode to ensure it's not included in the level3 data.
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '9000' );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertArrayNotHasKey( 'shipping_address_zip', $level_3_data );
+ }
+
+ public function test_us_customer_level_3_data() {
+ $expected_data = [
+ 'merchant_reference' => '210',
+ 'customer_reference' => '210',
+ 'shipping_amount' => 3800,
+ 'line_items' => [
+ (object) [
+ 'product_code' => 30,
+ 'product_description' => 'Beanie with Logo',
+ 'unit_cost' => 1800,
+ 'quantity' => 1,
+ 'tax_amount' => 270,
+ 'discount_amount' => 0,
+ ],
+ ],
+ 'shipping_address_zip' => '98012',
+ ];
+
+ // Use a non-US postcode.
+ $this->legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'get_option', 'woocommerce_store_postcode' )
+ ->willReturn( '9000' );
+
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '98012' );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertEquals( $expected_data, $level_3_data );
+ }
+
+ public function test_non_us_customer_level_3_data() {
+ $expected_data = [];
+
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'CA' );
+ $this->mock_level_3_order( 'K0A' );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertEquals( $expected_data, $level_3_data );
+ }
+
+ public function test_full_level3_data_with_float_quantity() {
+ $expected_data = [
+ 'merchant_reference' => '210',
+ 'customer_reference' => '210',
+ 'shipping_amount' => 3800,
+ 'line_items' => [
+ (object) [
+ 'product_code' => 30,
+ 'product_description' => 'Beanie with Logo',
+ 'unit_cost' => 450,
+ 'quantity' => 4,
+ 'tax_amount' => 270,
+ 'discount_amount' => 0,
+ ],
+ ],
+ 'shipping_address_zip' => '98012',
+ 'shipping_from_zip' => '94110',
+ ];
+
+ $this->legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'get_option', 'woocommerce_store_postcode' )
+ ->willReturn( '94110' );
+
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '98012', false, false, 3.7 );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertEquals( $expected_data, $level_3_data );
+ }
+
+ public function test_full_level3_data_with_float_quantity_zero() {
+ $expected_data = [
+ 'merchant_reference' => '210',
+ 'customer_reference' => '210',
+ 'shipping_amount' => 3800,
+ 'line_items' => [
+ (object) [
+ 'product_code' => 30,
+ 'product_description' => 'Beanie with Logo',
+ 'unit_cost' => 1800,
+ 'quantity' => 1,
+ 'tax_amount' => 270,
+ 'discount_amount' => 0,
+ ],
+ ],
+ 'shipping_address_zip' => '98012',
+ 'shipping_from_zip' => '94110',
+ ];
+
+ $this->legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'get_option', 'woocommerce_store_postcode' )
+ ->willReturn( '94110' );
+
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '98012', false, false, 0.4 );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertEquals( $expected_data, $level_3_data );
+ }
+
+ public function test_level3_data_bundle() {
+ $items = (array) [
+ (object) [
+ 'product_code' => 'abcd',
+ 'product_description' => 'product description',
+ 'unit_cost' => 1000,
+ 'quantity' => 4,
+ 'tax_amount' => 200,
+ 'discount_amount' => 500,
+ ],
+ (object) [
+ 'product_code' => 'abcd',
+ 'product_description' => 'product description',
+ 'unit_cost' => 5000,
+ 'quantity' => 3,
+ 'tax_amount' => 1000,
+ 'discount_amount' => 200,
+ ],
+ ];
+
+ // Use reflection to test the otherwise method.
+ $reflection = new ReflectionClass( Level3Service::class );
+ $method = $reflection->getMethod( 'bundle_level3_data_from_items' );
+ $method->setAccessible( true );
+ $bundle_data = $method->invoke( $this->sut, $items );
+
+ $this->assertSame( $bundle_data->product_description, '2 more items' );
+
+ // total_unit_cost = sum( unit_cost * quantity ).
+ $this->assertSame( $bundle_data->unit_cost, 19000 );
+
+ // quantity of the bundle = 1.
+ $this->assertSame( $bundle_data->quantity, 1 );
+
+ // total_tax_amount = sum( tax_amount ).
+ $this->assertSame( $bundle_data->tax_amount, 1200 );
+
+ // total_discount_amount = sum( discount_amount ).
+ $this->assertSame( $bundle_data->discount_amount, 700 );
+ }
+
+ public function test_level3_data_bundle_for_orders_with_more_than_200_items() {
+ $this->mock_account->method( 'get_account_country' )->willReturn( 'US' );
+ $this->mock_level_3_order( '98012', true, false, 1, 500 );
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertSame( count( $level_3_data['line_items'] ), 200 );
+
+ $bundled_data = end( $level_3_data['line_items'] );
+
+ $this->assertSame( $bundled_data->product_description, '301 more items' );
+ }
+}
diff --git a/tests/unit/src/Internal/Service/OrderServiceTest.php b/tests/unit/src/Internal/Service/OrderServiceTest.php
new file mode 100644
index 00000000000..362cb725fa9
--- /dev/null
+++ b/tests/unit/src/Internal/Service/OrderServiceTest.php
@@ -0,0 +1,480 @@
+mock_legacy_proxy = $this->createMock( LegacyProxy::class );
+ $this->mock_legacy_service = $this->createMock( WC_Payments_Order_Service::class );
+ $this->mock_account = $this->createMock( WC_Payments_Account::class );
+ $this->mock_hooks_proxy = $this->createMock( HooksProxy::class );
+
+ // Service under test, but with mockable methods.
+ $this->sut = $this->getMockBuilder( OrderService::class )
+ ->onlyMethods( [ 'get_order', 'attach_exchange_info_to_order' ] )
+ ->setConstructorArgs(
+ [
+ $this->mock_legacy_service,
+ $this->mock_legacy_proxy,
+ $this->mock_account,
+ $this->mock_hooks_proxy,
+ ]
+ )
+ ->getMock();
+ }
+
+ public function test_get_order_returns_order() {
+ $this->sut = new OrderService(
+ $this->mock_legacy_service,
+ $this->mock_legacy_proxy,
+ $this->mock_account,
+ $this->mock_hooks_proxy
+ );
+
+ $mock_order = $this->createMock( WC_Order::class );
+ $this->mock_legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'wc_get_order', $this->order_id )
+ ->willReturn( $mock_order );
+
+ // Go through `_deprecated_get_order` to call `get_order`.
+ $result = $this->sut->_deprecated_get_order( $this->order_id );
+ $this->assertSame( $mock_order, $result );
+ }
+
+ public function test_get_order_throws_exception() {
+ $this->sut = new OrderService(
+ $this->mock_legacy_service,
+ $this->mock_legacy_proxy,
+ $this->mock_account,
+ $this->mock_hooks_proxy
+ );
+
+ $this->mock_legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'wc_get_order', $this->order_id )
+ ->willReturn( false );
+
+ $this->expectException( Order_Not_Found_Exception::class );
+ $this->expectExceptionMessage( "The requested order (ID $this->order_id) was not found." );
+
+ // Go through `_deprecated_get_order` to call `get_order`.
+ $this->sut->_deprecated_get_order( $this->order_id );
+ }
+
+ public function test__deprecated_get_order_returns_order() {
+ $mock_order = $this->mock_get_order();
+
+ $result = $this->sut->_deprecated_get_order( $this->order_id );
+ $this->assertSame( $mock_order, $result );
+ }
+
+ public function test_set_payment_method_id() {
+ $pm_id = 'pm_XYZ';
+
+ $this->mock_legacy_service->expects( $this->once() )
+ ->method( 'set_payment_method_id_for_order' )
+ ->with( $this->order_id, $pm_id );
+
+ $this->sut->set_payment_method_id( $this->order_id, $pm_id );
+ }
+
+ public function test_get_payment_metadata_without_subscriptions() {
+ // Prepare data and expectations.
+ $first_name = 'John';
+ $last_name = 'Doe';
+ $email = 'example@example.com';
+ $order_number = 'ABC123';
+ $order_key = 'xyz123';
+ $created_via = 'checkout';
+
+ $expected = [
+ 'customer_name' => $first_name . ' ' . $last_name,
+ 'customer_email' => $email,
+ 'site_url' => esc_url( get_site_url() ),
+ 'order_id' => $this->order_id,
+ 'order_number' => $order_number,
+ 'order_key' => $order_key,
+ 'payment_type' => 'single',
+ 'checkout_type' => $created_via,
+ 'client_version' => WCPAY_VERSION_NUMBER,
+ 'subscription_payment' => 'no',
+ ];
+
+ // Setup the mock order.
+ $mock_order = $this->mock_get_order();
+
+ $order_methods = [
+ 'get_id' => $this->order_id,
+ 'get_billing_first_name' => $first_name,
+ 'get_billing_last_name' => $last_name,
+ 'get_billing_email' => $email,
+ 'get_order_number' => $order_number,
+ 'get_order_key' => $order_key,
+ 'get_created_via' => $created_via,
+ ];
+ foreach ( $order_methods as $name => $value ) {
+ $mock_order->expects( $this->once() )
+ ->method( $name )
+ ->willReturn( $value );
+ }
+
+ // Expect filters.
+ $this->mock_hooks_proxy->expects( $this->once() )
+ ->method( 'apply_filters' )
+ ->with( 'wcpay_metadata_from_order', $expected, $mock_order, Payment_Type::SINGLE() )
+ ->willReturn( $expected );
+
+ // Act.
+ $result = $this->sut->get_payment_metadata( $this->order_id, Payment_Type::SINGLE() );
+
+ // Assert.
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * @dataProvider provider_subscription_details
+ */
+ public function test_get_payment_metadata_with_subscription( bool $is_renewal, bool $wcpay_subscription ) {
+ $mock_order = $this->mock_get_order();
+
+ $this->mock_legacy_proxy->expects( $this->exactly( 3 ) )
+ ->method( 'call_function' )
+ ->withConsecutive(
+ [ 'function_exists', 'wcs_order_contains_subscription' ],
+ [ 'wcs_order_contains_subscription', $mock_order, 'any' ],
+ [ 'wcs_order_contains_renewal', $mock_order ]
+ )
+ ->willReturnOnConsecutiveCalls( true, true, $is_renewal );
+
+ $this->mock_legacy_proxy->expects( $this->once() )
+ ->method( 'call_static' )
+ ->with( WC_Payments_Features::class, 'should_use_stripe_billing' )
+ ->willReturn( $wcpay_subscription );
+
+ // Expect filters.
+ $this->mock_hooks_proxy->expects( $this->once() )
+ ->method( 'apply_filters' )
+ ->with( 'wcpay_metadata_from_order', $this->callback( 'is_array' ), $mock_order, Payment_Type::RECURRING() )
+ ->willReturnArgument( 1 );
+
+ // Act.
+ $result = $this->sut->get_payment_metadata( $this->order_id, Payment_Type::RECURRING() );
+
+ // Assert.
+ $this->assertIsArray( $result );
+ $this->assertEquals( $is_renewal ? 'renewal' : 'initial', $result['subscription_payment'] );
+ $this->assertEquals( $wcpay_subscription ? 'wcpay_subscription' : 'regular_subscription', $result['payment_context'] );
+ }
+
+ public function provider_subscription_details() {
+ return [
+ // is_renewal and wcpay_subscription.
+ [ false, false ],
+ [ false, true ],
+ [ true, false ],
+ [ true, true ],
+ ];
+ }
+
+ public function provider_import_order_data_to_payment_context() {
+ $existing_user = new WP_User();
+ $existing_user->ID = 10;
+
+ return [
+ 'No User' => [ null ],
+ 'User' => [ $existing_user ],
+ ];
+ }
+
+ /**
+ * @dataProvider provider_import_order_data_to_payment_context
+ */
+ public function test_import_order_data_to_payment_context( $user ) {
+ // Create a mock order that will be used to extract data.
+ $mock_order = $this->createMock( WC_Order::class );
+ $this->sut->expects( $this->once() )
+ ->method( 'get_order' )
+ ->willReturn( $mock_order );
+
+ // Create a context where data will be imported.
+ $mock_context = $this->createMock( PaymentContext::class );
+
+ // Currency and amount calls.
+ $currency = 'usd';
+ $mock_order->expects( $this->once() )->method( 'get_currency' )->willReturn( $currency );
+ $mock_context->expects( $this->once() )->method( 'set_currency' )->with( $currency );
+ $amount = 1234;
+ $mock_order->expects( $this->once() )->method( 'get_total' )->willReturn( $amount / 100 );
+ $mock_context->expects( $this->once() )->method( 'set_amount' )->with( $amount );
+
+ // Mock the user.
+ $mock_order->expects( $this->once() )
+ ->method( 'get_user' )
+ ->willReturn( $user ?? false );
+ if ( ! $user ) {
+ $user = $this->createMock( WP_User::class );
+ $user->ID = 10;
+
+ $this->mock_legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'wp_get_current_user' )
+ ->willReturn( $user );
+ }
+ $mock_context->expects( $this->once() )
+ ->method( 'set_user_id' )
+ ->with( 10 );
+
+ // Act.
+ $this->sut->import_order_data_to_payment_context( $this->order_id, $mock_context );
+ }
+
+ public function provider_update_order_from_successful_intent() {
+ $pi = $this->createMock( WC_Payments_API_Payment_Intention::class );
+ $si = $this->createMock( WC_Payments_API_Setup_Intention::class );
+
+ return [
+ 'Payment Intent' => [ $pi ],
+ 'Setup Intent' => [ $si ],
+ ];
+ }
+
+ /**
+ * @param WC_Payments_API_Payment_Intention|WC_Payments_API_Setup_Intention|MockObject $intent
+ * @dataProvider provider_update_order_from_successful_intent
+ */
+ public function test_update_order_from_successful_intent( $intent ) {
+ $charge_id = null;
+ $mock_charge = null;
+ $intent_id = 'pi_XYZ';
+ $intent_status = 'success';
+ $customer_id = 'cus_XYZ';
+ $currency = 'usd';
+ $payment_method_id = 'pm_XYZ';
+
+ // Create a mock order that will be used.
+ $mock_order = $this->createMock( WC_Order::class );
+ $this->sut->expects( $this->once() )
+ ->method( 'get_order' )
+ ->with( $this->order_id )
+ ->willReturn( $mock_order );
+
+ if ( is_a( $intent, WC_Payments_API_Payment_Intention::class ) ) {
+ $charge_id = 'ch_XYZ';
+ $mock_charge = $this->createMock( WC_Payments_API_Charge::class );
+
+ $mock_charge->expects( $this->once() )
+ ->method( 'get_id' )
+ ->willReturn( $charge_id );
+
+ $intent->expects( $this->exactly( 2 ) )
+ ->method( 'get_charge' )
+ ->willReturn( $mock_charge );
+ }
+
+ // Prepare all parameters for `attach_intent_info_to_order`.
+ $intent->expects( $this->once() )
+ ->method( 'get_id' )
+ ->willReturn( $intent_id );
+ $intent->expects( $this->once() )
+ ->method( 'get_status' )
+ ->willReturn( $intent_status );
+
+ $mock_context = $this->createMock( PaymentContext::class );
+ $mock_context->expects( $this->once() )
+ ->method( 'get_payment_method' )
+ ->willReturn( new NewPaymentMethod( $payment_method_id ) );
+ $mock_context->expects( $this->once() )
+ ->method( 'get_customer_id' )
+ ->willReturn( $customer_id );
+ $mock_context->expects( $this->once() )
+ ->method( 'get_currency' )
+ ->willReturn( $currency );
+
+ $this->mock_legacy_service->expects( $this->once() )
+ ->method( 'attach_intent_info_to_order' )
+ ->with(
+ $mock_order,
+ $intent_id,
+ $intent_status,
+ $payment_method_id,
+ $customer_id,
+ $charge_id,
+ $currency
+ );
+
+ // Prepare all additional calls.
+ $this->mock_legacy_service->expects( $this->once() )
+ ->method( 'attach_transaction_fee_to_order' )
+ ->with( $mock_order, $mock_charge );
+ $this->mock_legacy_service->expects( $this->once() )
+ ->method( 'update_order_status_from_intent' )
+ ->with( $mock_order, $intent );
+ if ( ! is_null( $mock_charge ) ) {
+ $this->sut->expects( $this->once() )
+ ->method( 'attach_exchange_info_to_order' )
+ ->with( $this->order_id, $mock_charge );
+ }
+
+ // Act.
+ $this->sut->update_order_from_successful_intent( $this->order_id, $intent, $mock_context );
+ }
+
+ public function provider_attach_exchange_info_to_order() {
+ return [
+ 'Different store and account currencies' => [ 'USD', 'USD', 'EUR', null, null ],
+ 'Same order and account currencies' => [ 'EUR', 'EUR', 'EUR', null, null ],
+ 'No exchange rate' => [ 'USD', 'EUR', 'USD', true, null ],
+ 'With exchange rate' => [ 'USD', 'EUR', 'USD', true, 3.0 ],
+ ];
+ }
+
+ /**
+ * @dataProvider provider_attach_exchange_info_to_order
+ */
+ public function test_attach_exchange_info_to_order( $store_currency, $order_currency, $account_currency, $has_charge = false, $exchange_rate = null ) {
+ /**
+ * Create a SUT that doesn't mock the method here.
+ *
+ * @var OrderService|MockObject
+ */
+ $this->sut = $this->getMockBuilder( OrderService::class )
+ ->onlyMethods( [ 'get_order' ] )
+ ->setConstructorArgs(
+ [
+ $this->mock_legacy_service,
+ $this->mock_legacy_proxy,
+ $this->mock_account,
+ $this->mock_hooks_proxy,
+ ]
+ )
+ ->getMock();
+
+ // Mock the store currency.
+ $this->mock_legacy_proxy->expects( $this->once() )
+ ->method( 'call_function' )
+ ->with( 'get_option', 'woocommerce_currency' )
+ ->willReturn( $store_currency );
+
+ // Mock the order currency.
+ $mock_order = $this->mock_get_order();
+ $mock_order->expects( $this->once() )->method( 'get_currency' )->willReturn( $order_currency );
+
+ // Mock the account currency.
+ $this->mock_account->expects( $this->once() )
+ ->method( 'get_account_default_currency' )
+ ->willReturn( $account_currency );
+
+ // No charge means that the charge object should never be reached.
+ $mock_charge = $this->createMock( WC_Payments_API_Charge::class );
+ if ( ! $has_charge ) {
+ $mock_charge->expects( $this->never() )->method( 'get_balance_transaction' );
+ $this->sut->attach_exchange_info_to_order( $this->order_id, $mock_charge );
+ return;
+ }
+
+ $transaction = [ 'exchange_rate' => $exchange_rate ];
+ $mock_charge->expects( $this->once() )
+ ->method( 'get_balance_transaction' )
+ ->willReturn( $transaction );
+
+ // No exchange rate means that the order will never be updated.
+ if ( ! $exchange_rate ) {
+ $mock_order->expects( $this->never() )->method( 'update_meta_data' );
+ $this->sut->attach_exchange_info_to_order( $this->order_id, $mock_charge );
+ return;
+ }
+
+ $mock_order->expects( $this->once() )
+ ->method( 'update_meta_data' )
+ ->with( '_wcpay_multi_currency_stripe_exchange_rate', $exchange_rate );
+ $mock_order->expects( $this->once() )
+ ->method( 'save_meta_data' );
+
+ // Act.
+ $this->sut->attach_exchange_info_to_order( $this->order_id, $mock_charge );
+ }
+
+ /**
+ * Mocks order retrieval.
+ *
+ * @param int $order_id ID of the order to mock.
+ * @return WC_Order|MockObject The mock order, ready for setup.
+ */
+ private function mock_get_order( int $order_id = null ) {
+ $order_id = $order_id ?? $this->order_id;
+ $mock_order = $this->createMock( WC_Order::class );
+
+ $this->sut->expects( $this->once() )
+ ->method( 'get_order' )
+ ->with( $order_id )
+ ->willReturn( $mock_order );
+
+ return $mock_order;
+ }
+}
diff --git a/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php b/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php
index a0442307705..9b993019506 100644
--- a/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php
+++ b/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php
@@ -7,7 +7,15 @@
namespace WCPay\Tests\Internal\Service;
+use PHPUnit\Framework\MockObject\MockObject;
+use WC_Payment_Gateway_WCPay;
+use WCPay\Internal\Payment\PaymentContext;
+use WCPay\Internal\Payment\PaymentRequest;
+use WCPay\Internal\Payment\State\CompletedState;
+use WCPay\Internal\Payment\State\InitialState;
use WCPAY_UnitTestCase;
+use WCPay\Internal\Proxy\LegacyProxy;
+use WCPay\Internal\Payment\State\StateFactory;
use WCPay\Internal\Service\PaymentProcessingService;
/**
@@ -17,34 +25,93 @@ class PaymentProcessingServiceTest extends WCPAY_UnitTestCase {
/**
* Service under test.
*
- * @var PaymentProcessingService
+ * @var PaymentProcessingService|MockObject
*/
private $sut;
+ /**
+ * @var StateFactory|MockObject
+ */
+ private $mock_state_factory;
+
+ /**
+ * @var LegacyProxy|MockObject
+ */
+ private $mock_legacy_proxy;
+
/**
* Set up the test.
*/
protected function setUp(): void {
parent::setUp();
- $this->sut = new PaymentProcessingService();
+ $this->mock_state_factory = $this->createMock( StateFactory::class );
+ $this->mock_legacy_proxy = $this->createMock( LegacyProxy::class );
+
+ $this->sut = $this->getMockBuilder( PaymentProcessingService::class )
+ ->setConstructorArgs(
+ [
+ $this->mock_state_factory,
+ $this->mock_legacy_proxy,
+ ]
+ )
+ ->onlyMethods( [ 'create_payment_context' ] )
+ ->getMock();
}
/**
- * Used to determine whether autoloading works.
+ * Test the basic happy path of processing a payment.
*/
- public function test_class_is_loaded() {
- $this->assertTrue( class_exists( PaymentProcessingService::class ) );
+ public function test_process_payment_happy_path() {
+ // Prepare all required mocks.
+ $mock_context = $this->createMock( PaymentContext::class );
+ $mock_initial_state = $this->createMock( InitialState::class );
+ $mock_completed_state = $this->createMock( CompletedState::class );
+
+ // Set up the mocks to be returned.
+ $this->sut->expects( $this->once() )
+ ->method( 'create_payment_context' )
+ ->with( 1 )
+ ->willReturn( $mock_context );
- $instance = new PaymentProcessingService();
- $this->assertInstanceOf( PaymentProcessingService::class, $instance );
+ $this->mock_state_factory->expects( $this->once() )
+ ->method( 'create_state' )
+ ->with( InitialState::class, $this->isInstanceOf( PaymentContext::class ) )
+ ->willReturn( $mock_initial_state );
+
+ $mock_initial_state->expects( $this->once() )
+ ->method( 'process' )
+ ->with( $this->isInstanceOf( PaymentRequest::class ) )
+ ->willReturn( $mock_completed_state );
+
+ $result = $this->sut->process_payment( 1 );
+ $this->assertSame( $mock_completed_state, $result );
}
/**
- * Checks if the `process_payment` method throws an exception.
+ * Test the basic happy path of processing a payment.
*/
- public function test_processing_payment_throws_exception() {
- $this->expectException( \Exception::class );
- $this->sut->process_payment( 1 );
+ public function test_process_payment_happy_path_without_mock_builder() {
+ $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy );
+
+ $mock_initial_state = $this->createMock( InitialState::class );
+ $mock_completed_state = $this->createMock( CompletedState::class );
+
+ // Prepare a payment method for the context.
+ $_POST['payment_method'] = WC_Payment_Gateway_WCPay::GATEWAY_ID;
+ $_POST['wcpay-payment-method'] = 'pi_XYZ';
+
+ $this->mock_state_factory->expects( $this->once() )
+ ->method( 'create_state' )
+ ->with( InitialState::class, $this->isInstanceOf( PaymentContext::class ) )
+ ->willReturn( $mock_initial_state );
+
+ $mock_initial_state->expects( $this->once() )
+ ->method( 'process' )
+ ->with( $this->isInstanceOf( PaymentRequest::class ) )
+ ->willReturn( $mock_completed_state );
+
+ $result = $sut->process_payment( 1 );
+ $this->assertSame( $mock_completed_state, $result );
}
}
diff --git a/tests/unit/src/Internal/Service/PaymentRequestServiceTest.php b/tests/unit/src/Internal/Service/PaymentRequestServiceTest.php
new file mode 100644
index 00000000000..f56fbdfcf46
--- /dev/null
+++ b/tests/unit/src/Internal/Service/PaymentRequestServiceTest.php
@@ -0,0 +1,93 @@
+sut = new PaymentRequestService();
+ }
+
+ public function provider_create_intent() {
+ return [
+ 'With fingerprint' => [ 'fingerprint' ],
+ 'Without fingerprint' => [ null ],
+ ];
+ }
+
+ /**
+ * Tests the method, which creates and confirms intents.
+ *
+ * @dataProvider provider_create_intent
+ */
+ public function test_create_intent( $fingerprint ) {
+ $context_data = [
+ 'get_amount' => 123,
+ 'get_currency' => 'usd',
+ 'get_payment_method' => new NewPaymentMethod( 'pm_XYZ' ),
+ 'get_customer_id' => 'cus_XYZ',
+ 'should_capture_manually' => false,
+ 'get_metadata' => [ 'metadata' ],
+ 'get_level3_data' => [ 'level3data' ],
+ 'get_cvc_confirmation' => 'confirmation',
+ 'get_fingerprint' => $fingerprint,
+ ];
+
+ $request_data = [
+ 'set_amount' => 123,
+ 'set_currency_code' => 'usd',
+ 'set_payment_method' => 'pm_XYZ',
+ 'set_customer' => 'cus_XYZ',
+ 'set_capture_method' => false, // No manual capture.
+ 'set_metadata' => [ 'metadata' ],
+ 'set_level3' => [ 'level3data' ],
+ 'set_payment_methods' => [ 'card' ],
+ 'set_cvc_confirmation' => 'confirmation',
+ 'set_fingerprint' => $fingerprint ?? '',
+ ];
+
+ $context = $this->createMock( PaymentContext::class );
+ $intent = $this->createMock( WC_Payments_API_Payment_Intention::class );
+ $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class );
+
+ foreach ( $context_data as $method => $value ) {
+ $context->expects( $this->once() )->method( $method )->willReturn( $value );
+ }
+
+ foreach ( $request_data as $method => $value ) {
+ $request->expects( $this->once() )->method( $method )->with( $value );
+ }
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $intent );
+
+ $result = $this->sut->create_intent( $context );
+ $this->assertSame( $intent, $result );
+ }
+}
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php b/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php
index 94d1ce4e3d1..3165d8e447f 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php
@@ -10,6 +10,7 @@
use WCPay\Duplicate_Payment_Prevention_Service;
use WCPay\Session_Rate_Limiter;
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
+use WCPay\Internal\Service\OrderService;
/**
* WC_Payment_Gateway_WCPay unit tests.
@@ -155,6 +156,7 @@ public function set_up() {
'mark_payment_complete_for_order',
'get_level3_data_from_order', // To avoid needing to mock the order items.
'get_payment_method_ids_enabled_at_checkout',
+ 'get_metadata_from_order',
]
)
->getMock();
@@ -172,6 +174,11 @@ public function set_up() {
'wcpay-payment-method' => 'pm_mock',
'payment_method' => WC_Payment_Gateway_WCPay::GATEWAY_ID,
];
+
+ // Intent metadata is generated elsewhere, use empty arrays here.
+ $this->mock_wcpay_gateway->expects( $this->any() )
+ ->method( 'get_metadata_from_order' )
+ ->willReturn( [] );
}
/**
@@ -225,15 +232,7 @@ public function test_single_payment() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) use ( $order ) {
- $this->assertEquals( $metadata['payment_type'], 'single' );
- $this->assertEquals( $metadata['order_key'], $order->get_order_key() );
- return is_array( $metadata );
- }
- )
- );
+ ->with( [] );
$request->expects( $this->once() )
->method( 'format_response' )
@@ -261,15 +260,7 @@ public function test_initial_subscription_payment() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) use ( $order ) {
- $this->assertEquals( $metadata['payment_type'], 'recurring' );
- $this->assertEquals( $metadata['order_key'], $order->get_order_key() );
- return is_array( $metadata );
- }
- )
- );
+ ->with( [] );
$request->expects( $this->once() )
->method( 'format_response' )
@@ -294,15 +285,7 @@ public function test_renewal_subscription_payment() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) use ( $order ) {
- $this->assertEquals( $metadata['payment_type'], 'recurring' );
- $this->assertEquals( $metadata['order_key'], $order->get_order_key() );
- return is_array( $metadata );
- }
- )
- );
+ ->with( [] );
$request->expects( $this->once() )
->method( 'format_response' )
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 8d3bbc625f7..af672185ee9 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
@@ -174,6 +174,7 @@ public function set_up() {
'get_level3_data_from_order', // To avoid needing to mock the order items.
'should_use_stripe_platform_on_checkout_page',
'get_payment_method_ids_enabled_at_checkout',
+ 'get_metadata_from_order',
]
)
->getMock();
@@ -186,10 +187,16 @@ public function set_up() {
$this->returnValue( $this->return_url )
);
+ $this->mock_wcpay_gateway
+ ->expects( $this->any() )
+ ->method( 'get_payment_method_ids_enabled_at_checkout' )
+ ->willReturn( [ Payment_Method::CARD ] );
+
+ // Plenty of methods require metadata, but it will be tested elsewhere.
$this->mock_wcpay_gateway
->expects( $this->any() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
- ->willReturn( [ Payment_Method::CARD ] );
+ ->method( 'get_metadata_from_order' )
+ ->willReturn( [] );
$this->wcpay_gateway = WC_Payments::get_gateway();
WC_Payments::set_gateway( $this->mock_wcpay_gateway );
@@ -502,10 +509,10 @@ public function test_exception_thrown() {
->expects( $this->once() )
->method( 'get_customer_id_by_user_id' )
->willReturn( 'cus_mock' );
+ $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::AUTOMATIC() ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
// Arrange: Throw an exception in create_and_confirm_intention.
$request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class );
-
$request->expects( $this->once() )
->method( 'format_response' )
->will(
@@ -1462,20 +1469,8 @@ public function test_process_payment_for_subscription_in_woopay_adds_subscriptio
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) {
- $required_keys = [ 'customer_name', 'customer_email', 'site_url', 'order_id', 'order_number', 'order_key', 'payment_type' ];
- foreach ( $required_keys as $key ) {
- if ( ! array_key_exists( $key, $metadata ) ) {
- return false;
- }
- }
- return true;
- }
- )
- )
- ->willReturn( $request );
+ ->with( [] )
+ ->willReturn( $request );
$request->expects( $this->once() )
->method( 'format_response' )
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php
index 74a4ad79b29..6a26e8c4a6c 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php
@@ -160,6 +160,7 @@ public function set_up() {
'get_return_url',
'mark_payment_complete_for_order',
'get_level3_data_from_order', // To avoid needing to mock the order items.
+ 'get_metadata_from_order',
]
)
->getMock();
@@ -181,6 +182,11 @@ public function set_up() {
'wcpay-payment-method' => self::PAYMENT_METHOD_ID,
'payment_method' => WC_Payment_Gateway_WCPay::GATEWAY_ID,
];
+
+ // Intent metadata is generated elsewhere, use empty arrays here.
+ $this->mock_wcpay_gateway->expects( $this->any() )
+ ->method( 'get_metadata_from_order' )
+ ->willReturn( [] );
}
public function test_new_card_subscription() {
@@ -239,19 +245,8 @@ public function test_new_card_subscription() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) {
- $required_keys = [ 'customer_name', 'customer_email', 'site_url', 'order_id', 'order_number', 'order_key', 'payment_type' ];
- foreach ( $required_keys as $key ) {
- if ( ! array_key_exists( $key, $metadata ) ) {
- return false;
- }
- }
- return true;
- }
- )
- );
+ ->with( [] );
+
$request->expects( $this->once() )
->method( 'format_response' )
->willReturn( $this->payment_intent );
@@ -406,19 +401,7 @@ public function test_saved_card_subscription() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) {
- $required_keys = [ 'customer_name', 'customer_email', 'site_url', 'order_id', 'order_number', 'order_key', 'payment_type' ];
- foreach ( $required_keys as $key ) {
- if ( ! array_key_exists( $key, $metadata ) ) {
- return false;
- }
- }
- return true;
- }
- )
- );
+ ->with( [] );
$request->expects( $this->once() )
->method( 'format_response' )
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
index 286be65ff4f..8ca0ea3b83d 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
@@ -8,6 +8,8 @@
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
use WCPay\Duplicate_Payment_Prevention_Service;
use WCPay\Exceptions\API_Exception;
+use WCPay\Internal\Service\Level3Service;
+use WCPay\Internal\Service\OrderService;
use WCPay\Session_Rate_Limiter;
/**
@@ -136,12 +138,27 @@ public function set_up() {
$this->mock_localization_service
);
$this->wcpay_gateway->init_hooks();
+
+ // Mock the level3 service to always return an empty array.
+ $mock_level3_service = $this->createMock( Level3Service::class );
+ $mock_level3_service->expects( $this->any() )
+ ->method( 'get_data_from_order' )
+ ->willReturn( [] );
+ wcpay_get_test_container()->replace( Level3Service::class, $mock_level3_service );
+
+ // Mock the order service to always return an empty array for meta.
+ $mock_order_service = $this->createMock( OrderService::class );
+ $mock_order_service->expects( $this->any() )
+ ->method( 'get_payment_metadata' )
+ ->willReturn( [] );
+ wcpay_get_test_container()->replace( OrderService::class, $mock_order_service );
}
public static function tear_down_after_class() {
WC_Subscriptions::set_wcs_get_subscriptions_for_order( null );
WC_Subscriptions::set_wcs_is_subscription( null );
WC_Subscriptions::set_wcs_get_subscriptions_for_renewal_order( null );
+ wcpay_get_test_container()->reset_all_replacements();
parent::tear_down_after_class();
}
@@ -293,19 +310,7 @@ public function test_scheduled_subscription_payment() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) {
- $required_keys = [ 'customer_name', 'customer_email', 'site_url', 'order_id', 'order_number', 'order_key', 'payment_type' ];
- foreach ( $required_keys as $key ) {
- if ( ! array_key_exists( $key, $metadata ) ) {
- return false;
- }
- }
- return true;
- }
- )
- );
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$request->expects( $this->once() )
->method( 'format_response' )
@@ -466,20 +471,7 @@ public function test_scheduled_subscription_payment_adds_mandate() {
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) {
- $required_keys = [ 'customer_name', 'customer_email', 'site_url', 'order_id', 'order_number', 'order_key', 'payment_type' ];
- foreach ( $required_keys as $key ) {
- if ( ! array_key_exists( $key, $metadata ) ) {
- return false;
- }
- }
- return true;
- }
- )
- );
-
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$request->expects( $this->once() )
->method( 'format_response' )
->willReturn( WC_Helper_Intention::create_intention() );
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php
index 9a675bdcaca..632feacaa78 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php
@@ -23,6 +23,9 @@
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
use WCPay\Internal\Payment\Factor;
use WCPay\Internal\Payment\Router;
+use WCPay\Internal\Payment\State\CompletedState;
+use WCPay\Internal\Service\Level3Service;
+use WCPay\Internal\Service\OrderService;
use WCPay\Internal\Service\PaymentProcessingService;
use WCPay\Payment_Information;
use WCPay\WooPay\WooPay_Utilities;
@@ -50,42 +53,42 @@ class WC_Payment_Gateway_WCPay_Test extends WCPAY_UnitTestCase {
/**
* Mock WC_Payments_API_Client.
*
- * @var WC_Payments_API_Client|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_API_Client|MockObject
*/
private $mock_api_client;
/**
* Mock WC_Payments_Customer_Service.
*
- * @var WC_Payments_Customer_Service|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_Customer_Service|MockObject
*/
private $mock_customer_service;
/**
* Mock WC_Payments_Token_Service.
*
- * @var WC_Payments_Token_Service|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_Token_Service|MockObject
*/
private $mock_token_service;
/**
* Mock WC_Payments_Action_Scheduler_Service.
*
- * @var WC_Payments_Action_Scheduler_Service|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_Action_Scheduler_Service|MockObject
*/
private $mock_action_scheduler_service;
/**
* WC_Payments_Account instance.
*
- * @var WC_Payments_Account|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_Account|MockObject
*/
private $mock_wcpay_account;
/**
* Session_Rate_Limiter instance.
*
- * @var Session_Rate_Limiter|PHPUnit_Framework_MockObject_MockObject
+ * @var Session_Rate_Limiter|MockObject
*/
private $mock_rate_limiter;
@@ -198,6 +201,20 @@ public function set_up() {
$this->mock_wcpay_account,
$this->mock_customer_service
);
+
+ // Mock the level3 service to always return an empty array.
+ $mock_level3_service = $this->createMock( Level3Service::class );
+ $mock_level3_service->expects( $this->any() )
+ ->method( 'get_data_from_order' )
+ ->willReturn( [] );
+ wcpay_get_test_container()->replace( Level3Service::class, $mock_level3_service );
+
+ // Mock the order service to always return an empty array for meta.
+ $mock_order_service = $this->createMock( OrderService::class );
+ $mock_order_service->expects( $this->any() )
+ ->method( 'get_payment_metadata' )
+ ->willReturn( [] );
+ wcpay_get_test_container()->replace( OrderService::class, $mock_order_service );
}
/**
@@ -228,6 +245,8 @@ public function tear_down() {
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
+
+ wcpay_get_test_container()->reset_all_replacements();
}
public function test_attach_exchange_info_to_order_with_no_conversion() {
@@ -420,424 +439,6 @@ function ( $output ) use ( $token_value ) {
$this->wcpay_gateway->payment_fields();
}
- protected function create_mock_item( $name, $quantity, $subtotal, $total_tax, $product_id ) {
- // Setup the item.
- $mock_item = $this
- ->getMockBuilder( WC_Order_Item_Product::class )
- ->disableOriginalConstructor()
- ->setMethods(
- [
- 'get_name',
- 'get_quantity',
- 'get_subtotal',
- 'get_total_tax',
- 'get_total',
- 'get_variation_id',
- 'get_product_id',
- ]
- )
- ->getMock();
-
- $mock_item
- ->method( 'get_name' )
- ->will( $this->returnValue( $name ) );
-
- $mock_item
- ->method( 'get_quantity' )
- ->will( $this->returnValue( $quantity ) );
-
- $mock_item
- ->method( 'get_total' )
- ->will( $this->returnValue( $subtotal ) );
-
- $mock_item
- ->method( 'get_subtotal' )
- ->will( $this->returnValue( $subtotal ) );
-
- $mock_item
- ->method( 'get_total_tax' )
- ->will( $this->returnValue( $total_tax ) );
-
- $mock_item
- ->method( 'get_variation_id' )
- ->will( $this->returnValue( false ) );
-
- $mock_item
- ->method( 'get_product_id' )
- ->will( $this->returnValue( $product_id ) );
-
- return $mock_item;
- }
-
- protected function mock_level_3_order(
- $shipping_postcode,
- $with_fee = false,
- $with_negative_price_product = false,
- $quantity = 1,
- $basket_size = 1,
- $product_id = 30
- ) {
- $mock_items[] = $this->create_mock_item( 'Beanie with Logo', $quantity, 18, 2.7, $product_id );
-
- if ( $with_fee ) {
- // Setup the fee.
- $mock_fee = $this
- ->getMockBuilder( WC_Order_Item_Fee::class )
- ->disableOriginalConstructor()
- ->setMethods( [ 'get_name', 'get_quantity', 'get_total_tax', 'get_total' ] )
- ->getMock();
-
- $mock_fee
- ->method( 'get_name' )
- ->will( $this->returnValue( 'fee' ) );
-
- $mock_fee
- ->method( 'get_quantity' )
- ->will( $this->returnValue( 1 ) );
-
- $mock_fee
- ->method( 'get_total' )
- ->will( $this->returnValue( 10 ) );
-
- $mock_fee
- ->method( 'get_total_tax' )
- ->will( $this->returnValue( 1.5 ) );
-
- $mock_items[] = $mock_fee;
- }
-
- if ( $with_negative_price_product ) {
- $mock_items[] = $this->create_mock_item( 'Negative Product Price', $quantity, -18.99, 2.7, 42 );
- }
-
- if ( $basket_size > 1 ) {
- // Keep the formely created item/fee and add duplicated items to the basket.
- $mock_items = array_merge( $mock_items, array_fill( 0, $basket_size - 1, $mock_items[0] ) );
- }
-
- // Setup the order.
- $mock_order = $this
- ->getMockBuilder( WC_Order::class )
- ->disableOriginalConstructor()
- ->setMethods(
- [
- 'get_id',
- 'get_items',
- 'get_currency',
- 'get_shipping_total',
- 'get_shipping_tax',
- 'get_shipping_postcode',
- ]
- )
- ->getMock();
-
- $mock_order
- ->method( 'get_id' )
- ->will( $this->returnValue( 210 ) );
-
- $mock_order
- ->method( 'get_items' )
- ->will( $this->returnValue( $mock_items ) );
-
- $mock_order
- ->method( 'get_currency' )
- ->will( $this->returnValue( 'USD' ) );
-
- $mock_order
- ->method( 'get_shipping_total' )
- ->will( $this->returnValue( 30 ) );
-
- $mock_order
- ->method( 'get_shipping_tax' )
- ->will( $this->returnValue( 8 ) );
-
- $mock_order
- ->method( 'get_shipping_postcode' )
- ->will( $this->returnValue( $shipping_postcode ) );
-
- return $mock_order;
- }
-
- public function test_full_level3_data() {
- $expected_data = [
- 'merchant_reference' => '210',
- 'customer_reference' => '210',
- 'shipping_amount' => 3800,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- ],
- ],
- 'shipping_address_zip' => '98012',
- 'shipping_from_zip' => '94110',
- ];
-
- update_option( 'woocommerce_store_postcode', '94110' );
-
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '98012' );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertEquals( $expected_data, $level_3_data );
- }
-
- public function test_full_level3_data_with_product_id_longer_than_12_characters() {
- $expected_data = [
- 'merchant_reference' => '210',
- 'customer_reference' => '210',
- 'shipping_amount' => 3800,
- 'line_items' => [
- (object) [
- 'product_code' => 123456789123,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- ],
- ],
- 'shipping_address_zip' => '98012',
- 'shipping_from_zip' => '94110',
- ];
-
- update_option( 'woocommerce_store_postcode', '94110' );
-
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '98012', false, false, 1, 1, 123456789123456 );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertEquals( $expected_data, $level_3_data );
- }
-
- public function test_full_level3_data_with_fee() {
- $expected_data = [
- 'merchant_reference' => '210',
- 'customer_reference' => '210',
- 'shipping_amount' => 3800,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- ],
- (object) [
- 'product_code' => 'fee',
- 'product_description' => 'fee',
- 'unit_cost' => 1000,
- 'quantity' => 1,
- 'tax_amount' => 150,
- 'discount_amount' => 0,
- ],
- ],
- 'shipping_address_zip' => '98012',
- 'shipping_from_zip' => '94110',
- ];
-
- update_option( 'woocommerce_store_postcode', '94110' );
-
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '98012', true );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertEquals( $expected_data, $level_3_data );
- }
-
- public function test_full_level3_data_with_negative_price_product() {
- $expected_data = [
- 'merchant_reference' => '210',
- 'customer_reference' => '210',
- 'shipping_amount' => 3800,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- ],
- (object) [
- 'product_code' => 42,
- 'product_description' => 'Negative Product Price',
- 'unit_cost' => 0,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 1899,
- ],
- ],
- 'shipping_address_zip' => '98012',
- 'shipping_from_zip' => '94110',
- ];
-
- update_option( 'woocommerce_store_postcode', '94110' );
-
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '98012', false, true, 1, 1 );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertEquals( $expected_data, $level_3_data );
- }
-
- public function test_us_store_level_3_data() {
- // Use a non-us customer postcode to ensure it's not included in the level3 data.
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '9000' );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertArrayNotHasKey( 'shipping_address_zip', $level_3_data );
- }
-
- public function test_us_customer_level_3_data() {
- $expected_data = [
- 'merchant_reference' => '210',
- 'customer_reference' => '210',
- 'shipping_amount' => 3800,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- ],
- ],
- 'shipping_address_zip' => '98012',
- ];
-
- // Use a non-US postcode.
- update_option( 'woocommerce_store_postcode', '9000' );
-
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '98012' );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertEquals( $expected_data, $level_3_data );
- }
-
- public function test_non_us_customer_level_3_data() {
- $expected_data = [];
-
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'CA' );
- $mock_order = $this->mock_level_3_order( 'K0A' );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertEquals( $expected_data, $level_3_data );
- }
-
- public function test_full_level3_data_with_float_quantity() {
- $expected_data = [
- 'merchant_reference' => '210',
- 'customer_reference' => '210',
- 'shipping_amount' => 3800,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 450,
- 'quantity' => 4,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- ],
- ],
- 'shipping_address_zip' => '98012',
- 'shipping_from_zip' => '94110',
- ];
-
- update_option( 'woocommerce_store_postcode', '94110' );
-
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '98012', false, false, 3.7 );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertEquals( $expected_data, $level_3_data );
- }
-
- public function test_full_level3_data_with_float_quantity_zero() {
- $expected_data = [
- 'merchant_reference' => '210',
- 'customer_reference' => '210',
- 'shipping_amount' => 3800,
- 'line_items' => [
- (object) [
- 'product_code' => 30,
- 'product_description' => 'Beanie with Logo',
- 'unit_cost' => 1800,
- 'quantity' => 1,
- 'tax_amount' => 270,
- 'discount_amount' => 0,
- ],
- ],
- 'shipping_address_zip' => '98012',
- 'shipping_from_zip' => '94110',
- ];
-
- update_option( 'woocommerce_store_postcode', '94110' );
-
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '98012', false, false, 0.4 );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertEquals( $expected_data, $level_3_data );
- }
-
- public function test_level3_data_bundle() {
- $items = (array) [
- (object) [
- 'product_code' => 'abcd',
- 'product_description' => 'product description',
- 'unit_cost' => 1000,
- 'quantity' => 4,
- 'tax_amount' => 200,
- 'discount_amount' => 500,
- ],
- (object) [
- 'product_code' => 'abcd',
- 'product_description' => 'product description',
- 'unit_cost' => 5000,
- 'quantity' => 3,
- 'tax_amount' => 1000,
- 'discount_amount' => 200,
- ],
- ];
-
- $bundle_data = $this->wcpay_gateway->bundle_level3_data_from_items( $items );
-
- $this->assertSame( $bundle_data->product_description, '2 more items' );
-
- // total_unit_cost = sum( unit_cost * quantity ).
- $this->assertSame( $bundle_data->unit_cost, 19000 );
-
- // quantity of the bundle = 1.
- $this->assertSame( $bundle_data->quantity, 1 );
-
- // total_tax_amount = sum( tax_amount ).
- $this->assertSame( $bundle_data->tax_amount, 1200 );
-
- // total_discount_amount = sum( discount_amount ).
- $this->assertSame( $bundle_data->discount_amount, 700 );
- }
-
- public function test_level3_data_bundle_for_orders_with_more_than_200_items() {
- $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' );
- $mock_order = $this->mock_level_3_order( '98012', true, false, 1, 500 );
- $level_3_data = $this->wcpay_gateway->get_level3_data_from_order( $mock_order );
-
- $this->assertSame( count( $level_3_data['line_items'] ), 200 );
-
- $bundled_data = end( $level_3_data['line_items'] );
-
- $this->assertSame( $bundled_data->product_description, '301 more items' );
- }
-
public function test_capture_charge_success() {
$intent_id = 'pi_mock';
$charge_id = 'ch_mock';
@@ -851,36 +452,17 @@ public function test_capture_charge_success() {
$mock_intent = WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::REQUIRES_CAPTURE ] );
- $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
-
- $request->expects( $this->once() )
- ->method( 'format_response' )
- ->willReturn( $mock_intent );
-
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $argument ) {
- return is_array( $argument ) && ! empty( $argument );
- }
- )
- );
$capture_intent_request = $this->mock_wcpay_request( Capture_Intention::class, 1, $intent_id );
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->willReturn( WC_Helper_Intention::create_intention() );
- $this->mock_wcpay_account
- ->expects( $this->once() )
- ->method( 'get_account_country' )
- ->willReturn( 'US' );
-
$result = $this->wcpay_gateway->capture_charge( $order );
$notes = wc_get_order_notes(
@@ -925,34 +507,17 @@ public function test_capture_charge_success_non_usd() {
]
);
- $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
-
- $request->expects( $this->once() )
- ->method( 'format_response' )
- ->willReturn( $mock_intent );
-
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' );
-
- $update_intent_request->expects( $this->once() )
- ->method( 'format_response' )
- ->willReturn( $mock_intent );
-
$capture_intent_request = $this->mock_wcpay_request( Capture_Intention::class, 1, $intent_id );
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->willReturn( WC_Helper_Intention::create_intention( [ 'currency' => 'eur' ] ) );
- $this->mock_wcpay_account
- ->expects( $this->once() )
- ->method( 'get_account_country' )
- ->willReturn( 'US' );
-
$result = $this->wcpay_gateway->capture_charge( $order );
$notes = wc_get_order_notes(
@@ -994,29 +559,17 @@ public function test_capture_charge_failure() {
$mock_intent = WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::REQUIRES_CAPTURE ] );
- $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
-
- $request->expects( $this->once() )
- ->method( 'format_response' )
- ->willReturn( $mock_intent );
-
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' );
$capture_intent_request = $this->mock_wcpay_request( Capture_Intention::class, 1, $intent_id );
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->willReturn( $mock_intent );
- $this->mock_wcpay_account
- ->expects( $this->once() )
- ->method( 'get_account_country' )
- ->willReturn( 'US' );
-
$result = $this->wcpay_gateway->capture_charge( $order );
$note = wc_get_order_notes(
@@ -1061,29 +614,17 @@ public function test_capture_charge_failure_non_usd() {
]
);
- $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
-
- $request->expects( $this->once() )
- ->method( 'format_response' )
- ->willReturn( $mock_intent );
-
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' );
$capture_intent_request = $this->mock_wcpay_request( Capture_Intention::class, 1, $intent_id );
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->willReturn( $mock_intent );
- $this->mock_wcpay_account
- ->expects( $this->once() )
- ->method( 'get_account_country' )
- ->willReturn( 'US' );
-
$result = $this->wcpay_gateway->capture_charge( $order );
$note = wc_get_order_notes(
@@ -1124,29 +665,23 @@ public function test_capture_charge_api_failure() {
$mock_intent = WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::REQUIRES_CAPTURE ] );
- $request = $this->mock_wcpay_request( Get_Intention::class, 2, $intent_id );
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
- $request->expects( $this->exactly( 2 ) )
+ $request->expects( $this->exactly( 1 ) )
->method( 'format_response' )
->willReturn( $mock_intent );
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' );
$capture_intent_request = $this->mock_wcpay_request( Capture_Intention::class, 1, $intent_id );
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->will( $this->throwException( new API_Exception( 'test exception', 'server_error', 500 ) ) );
- $this->mock_wcpay_account
- ->expects( $this->once() )
- ->method( 'get_account_country' )
- ->willReturn( 'US' );
-
$result = $this->wcpay_gateway->capture_charge( $order );
$note = wc_get_order_notes(
@@ -1192,17 +727,9 @@ public function test_capture_charge_api_failure_non_usd() {
]
);
- $request = $this->mock_wcpay_request( Get_Intention::class, 2, $intent_id );
-
- $request->expects( $this->exactly( 2 ) )
- ->method( 'format_response' )
- ->willReturn( $mock_intent );
-
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' );
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
+ $request->expects( $this->exactly( 1 ) )
->method( 'format_response' )
->willReturn( $mock_intent );
@@ -1210,16 +737,13 @@ public function test_capture_charge_api_failure_non_usd() {
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->will( $this->throwException( new API_Exception( 'test exception', 'server_error', 500 ) ) );
- $this->mock_wcpay_account
- ->expects( $this->once() )
- ->method( 'get_account_country' )
- ->willReturn( 'US' );
-
$result = $this->wcpay_gateway->capture_charge( $order );
$note = wc_get_order_notes(
@@ -1261,29 +785,23 @@ public function test_capture_charge_expired() {
$mock_intent = WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::CANCELED ] );
- $request = $this->mock_wcpay_request( Get_Intention::class, 2, $intent_id );
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
- $request->expects( $this->exactly( 2 ) )
+ $request->expects( $this->exactly( 1 ) )
->method( 'format_response' )
->willReturn( $mock_intent );
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' );
$capture_intent_request = $this->mock_wcpay_request( Capture_Intention::class, 1, $intent_id );
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->will( $this->throwException( new API_Exception( 'test exception', 'server_error', 500 ) ) );
- $this->mock_wcpay_account
- ->expects( $this->once() )
- ->method( 'get_account_country' )
- ->willReturn( 'US' );
-
$result = $this->wcpay_gateway->capture_charge( $order );
$note = wc_get_order_notes(
@@ -1326,50 +844,23 @@ public function test_capture_charge_metadata() {
'status' => Intent_Status::REQUIRES_CAPTURE,
'metadata' => [
'customer_name' => 'Test',
+ 'reader_ID' => 'wisepad',
],
]
);
- $merged_metadata = [
- 'customer_name' => 'Test',
- 'customer_email' => $order->get_billing_email(),
- 'site_url' => esc_url( get_site_url() ),
- 'order_id' => $order->get_id(),
- 'order_number' => $order->get_order_number(),
- 'order_key' => $order->get_order_key(),
- 'payment_type' => Payment_Type::SINGLE(),
- 'gateway_type' => 'classic',
- 'checkout_type' => '',
- 'client_version' => WCPAY_VERSION_NUMBER,
- 'subscription_payment' => 'no',
- ];
-
- $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
-
- $request->expects( $this->once() )
- ->method( 'format_response' )
- ->willReturn( $mock_intent );
-
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' )
- ->with( $merged_metadata );
-
$capture_intent_request = $this->mock_wcpay_request( Capture_Intention::class, 1, $intent_id );
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->willReturn( WC_Helper_Intention::create_intention() );
- $this->mock_wcpay_account
- ->expects( $this->once() )
- ->method( 'get_account_country' )
- ->willReturn( 'US' );
-
- $result = $this->wcpay_gateway->capture_charge( $order );
+ $result = $this->wcpay_gateway->capture_charge( $order, true, [] );
$note = wc_get_order_notes(
[
@@ -1407,21 +898,13 @@ public function test_capture_charge_without_level3() {
$mock_intent = WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::REQUIRES_CAPTURE ] );
- $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id );
-
- $request->expects( $this->once() )
- ->method( 'format_response' )
- ->willReturn( $mock_intent );
-
- $update_intent_request = $this->mock_wcpay_request( Update_Intention::class, 1, $intent_id );
- $update_intent_request->expects( $this->once() )
- ->method( 'set_metadata' );
-
$capture_intent_request = $this->mock_wcpay_request( Capture_Intention::class, 1, $intent_id );
$capture_intent_request->expects( $this->once() )
->method( 'set_amount_to_capture' )
->with( $mock_intent->get_amount() );
-
+ $capture_intent_request->expects( $this->once() )
+ ->method( 'set_metadata' )
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$capture_intent_request->expects( $this->once() )
->method( 'format_response' )
->willReturn( WC_Helper_Intention::create_intention() );
@@ -2027,19 +1510,7 @@ public function test_process_payment_caches_mimimum_amount_and_displays_error_up
$request->expects( $this->once() )
->method( 'set_metadata' )
- ->with(
- $this->callback(
- function( $metadata ) {
- $required_keys = [ 'customer_name', 'customer_email', 'site_url', 'order_id', 'order_number', 'order_key', 'payment_type' ];
- foreach ( $required_keys as $key ) {
- if ( ! array_key_exists( $key, $metadata ) ) {
- return false;
- }
- }
- return true;
- }
- )
- );
+ ->with( [ 'gateway_type' => 'legacy_card' ] );
$request->expects( $this->once() )
->method( 'format_response' )
@@ -2618,10 +2089,10 @@ public function test_new_process_payment() {
// The new payment process is only accessible in dev mode.
WC_Payments::mode()->dev();
- $mock_service = $this->createMock( PaymentProcessingService::class );
- $mock_router = $this->createMock( Router::class );
- $order = WC_Helper_Order::create_order();
- $mock_response = [ 'success' => 'maybe' ];
+ $mock_service = $this->createMock( PaymentProcessingService::class );
+ $mock_router = $this->createMock( Router::class );
+ $order = WC_Helper_Order::create_order();
+ $mock_state = $this->createMock( CompletedState::class );
wcpay_get_test_container()->replace( PaymentProcessingService::class, $mock_service );
wcpay_get_test_container()->replace( Router::class, $mock_router );
@@ -2634,10 +2105,16 @@ public function test_new_process_payment() {
$mock_service->expects( $this->once() )
->method( 'process_payment' )
->with( $order->get_id() )
- ->willReturn( $mock_response );
+ ->willReturn( $mock_state );
$result = $this->wcpay_gateway->process_payment( $order->get_id() );
- $this->assertSame( $mock_response, $result );
+ $this->assertSame(
+ [
+ 'result' => 'success',
+ 'redirect' => $order->get_checkout_order_received_url(),
+ ],
+ $result
+ );
}
/**
diff --git a/tests/unit/test-class-wc-payments-account-capital.php b/tests/unit/test-class-wc-payments-account-capital.php
index a23f6850664..a32610f1625 100644
--- a/tests/unit/test-class-wc-payments-account-capital.php
+++ b/tests/unit/test-class-wc-payments-account-capital.php
@@ -9,6 +9,7 @@
use WCPay\Core\Server\Response;
use WCPay\Exceptions\API_Exception;
use WCPay\Database_Cache;
+use PHPUnit\Framework\MockObject\MockObject;
/**
* WC_Payments_Account unit tests for Capital-related methods.
@@ -24,14 +25,14 @@ class WC_Payments_Account_Capital_Test extends WCPAY_UnitTestCase {
/**
* Mock WC_Payments_API_Client.
*
- * @var WC_Payments_API_Client|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_API_Client|MockObject
*/
private $mock_api_client;
/**
* Mock Database_Cache
*
- * @var Database_Cache|PHPUnit_Framework_MockObject_MockObject
+ * @var Database_Cache|MockObject
*/
private $mock_database_cache;
@@ -44,10 +45,17 @@ class WC_Payments_Account_Capital_Test extends WCPAY_UnitTestCase {
/**
* Mock WC_Payments_Action_Scheduler_Service
*
- * @var WC_Payments_Action_Scheduler_Service|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_Action_Scheduler_Service|MockObject
*/
private $mock_action_scheduler_service;
+ /**
+ * Mock WC_Payments_Session_Service.
+ *
+ * @var WC_Payments_Session_Service|MockObject
+ */
+ private $mock_session_service;
+
/**
* Pre-test setup
*/
@@ -62,17 +70,17 @@ public function set_up() {
add_filter( 'wp_doing_ajax', '__return_false' );
$_GET['wcpay-loan-offer'] = '';
- $this->mock_api_client = $this->createMock( 'WC_Payments_API_Client' );
-
- $this->mock_database_cache = $this->createMock( Database_Cache::class );
-
+ $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
+ $this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
+ $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
// Mock WC_Payments_Account without redirect_to to prevent headers already sent error.
$this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
- ->setMethods( [ 'redirect_to' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service ] )
+ ->setMethods( [ 'redirect_to', 'init_hooks' ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] )
->getMock();
+ $this->wcpay_account->init_hooks();
}
public function tear_down() {
@@ -88,8 +96,10 @@ public function tear_down() {
public function test_maybe_redirect_to_capital_offer_will_run() {
$wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service ] )
+ ->setMethodsExcept( [ 'maybe_redirect_to_capital_offer', 'init_hooks' ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] )
->getMock();
+ $wcpay_account->init_hooks();
$this->assertNotFalse(
has_action( 'admin_init', [ $wcpay_account, 'maybe_redirect_to_capital_offer' ] )
diff --git a/tests/unit/test-class-wc-payments-account-link.php b/tests/unit/test-class-wc-payments-account-link.php
index 2a339a5ca43..09ec6e4a039 100644
--- a/tests/unit/test-class-wc-payments-account-link.php
+++ b/tests/unit/test-class-wc-payments-account-link.php
@@ -7,6 +7,7 @@
use WCPay\Exceptions\API_Exception;
use WCPay\Database_Cache;
+use PHPUnit\Framework\MockObject\MockObject;
/**
* WC_Payments_Account unit tests for Server Links related methods.
@@ -22,14 +23,14 @@ class WC_Payments_Account_Server_Links_Test extends WCPAY_UnitTestCase {
/**
* Mock WC_Payments_API_Client.
*
- * @var WC_Payments_API_Client|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_API_Client|MockObject
*/
private $mock_api_client;
/**
* Mock Database_Cache
*
- * @var Database_Cache|PHPUnit_Framework_MockObject_MockObject
+ * @var Database_Cache|MockObject
*/
private $mock_database_cache;
@@ -42,10 +43,17 @@ class WC_Payments_Account_Server_Links_Test extends WCPAY_UnitTestCase {
/**
* Mock WC_Payments_Action_Scheduler_Service
*
- * @var WC_Payments_Action_Scheduler_Service|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_Action_Scheduler_Service|MockObject
*/
private $mock_action_scheduler_service;
+ /**
+ * Mock WC_Payments_Session_Service.
+ *
+ * @var WC_Payments_Session_Service|MockObject
+ */
+ private $mock_session_service;
+
/**
* Pre-test setup
*/
@@ -60,17 +68,18 @@ public function set_up() {
add_filter( 'wp_doing_ajax', '__return_false' );
$_GET['wcpay-link-handler'] = '';
- $this->mock_api_client = $this->createMock( 'WC_Payments_API_Client' );
-
- $this->mock_database_cache = $this->createMock( Database_Cache::class );
-
+ $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
+ $this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
+ $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
// Mock WC_Payments_Account without redirect_to to prevent headers already sent error.
$this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
->setMethods( [ 'redirect_to' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] )
->getMock();
+
+ $this->wcpay_account->init_hooks();
}
public function tear_down() {
diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php
index 20bb5cca76c..f8162fd2ca5 100644
--- a/tests/unit/test-class-wc-payments-account.php
+++ b/tests/unit/test-class-wc-payments-account.php
@@ -12,6 +12,7 @@
use WCPay\Core\Server\Response;
use WCPay\Exceptions\API_Exception;
use WCPay\Database_Cache;
+use PHPUnit\Framework\MockObject\MockObject;
/**
* WC_Payments_Account unit tests.
@@ -31,24 +32,31 @@ class WC_Payments_Account_Test extends WCPAY_UnitTestCase {
/**
* Mock WC_Payments_API_Client.
*
- * @var WC_Payments_API_Client|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_API_Client|MockObject
*/
private $mock_api_client;
/**
* Mock Database_Cache
*
- * @var Database_Cache|PHPUnit_Framework_MockObject_MockObject
+ * @var Database_Cache|MockObject
*/
private $mock_database_cache;
/**
* Mock WC_Payments_Action_Scheduler_Service
*
- * @var WC_Payments_Action_Scheduler_Service|PHPUnit_Framework_MockObject_MockObject
+ * @var WC_Payments_Action_Scheduler_Service|MockObject
*/
private $mock_action_scheduler_service;
+ /**
+ * Mock WC_Payments_Session_Service.
+ *
+ * @var WC_Payments_Session_Service|MockObject
+ */
+ private $mock_session_service;
+
/**
* Pre-test setup
*/
@@ -68,8 +76,10 @@ public function set_up() {
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
+ $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
- $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service );
+ $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service );
+ $this->wcpay_account->init_hooks();
}
public function tear_down() {
@@ -78,17 +88,20 @@ public function tear_down() {
parent::tear_down();
}
- /**
- * @param bool $can_manage_woocommerce
- *
- * @return Closure
- */
- private function create_can_manage_woocommerce_cap_override( bool $can_manage_woocommerce ) {
- return function ( $allcaps ) use ( $can_manage_woocommerce ) {
- $allcaps['manage_woocommerce'] = $can_manage_woocommerce;
-
- return $allcaps;
- };
+ public function test_filters_registered_properly() {
+ $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_handle_onboarding' ] ), 'maybe_handle_onboarding action does not exist.' );
+ $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_onboarding' ] ), 'maybe_redirect_to_onboarding action does not exist.' );
+ $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_wcpay_connect' ] ), 'maybe_redirect_to_wcpay_connect action does not exist.' );
+ $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_capital_offer' ] ), 'maybe_redirect_to_capital_offer action does not exist.' );
+ $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_server_link' ] ), 'maybe_redirect_to_server_link action does not exist.' );
+ $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_activate_woopay' ] ), 'maybe_activate_woopay action does not exist.' );
+ $this->assertNotFalse( has_action( 'woocommerce_payments_account_refreshed', [ $this->wcpay_account, 'handle_instant_deposits_inbox_note' ] ), 'handle_instant_deposits_inbox_note action does not exist.' );
+ $this->assertNotFalse( has_action( 'woocommerce_payments_account_refreshed', [ $this->wcpay_account, 'handle_loan_approved_inbox_note' ] ), 'handle_loan_approved_inbox_note action does not exist.' );
+ $this->assertNotFalse( has_action( 'wcpay_instant_deposit_reminder', [ $this->wcpay_account, 'handle_instant_deposits_inbox_reminder' ] ), 'handle_instant_deposits_inbox_reminder action does not exist.' );
+ $this->assertNotFalse( has_filter( 'allowed_redirect_hosts', [ $this->wcpay_account, 'allowed_redirect_hosts' ] ), 'allowed_redirect_hooks filter does not exist.' );
+ $this->assertNotFalse( has_action( 'jetpack_site_registered', [ $this->wcpay_account, 'clear_cache' ] ), 'jetpack_site_registered action does not exist.' );
+ $this->assertNotFalse( has_action( 'updated_option', [ $this->wcpay_account, 'possibly_update_wcpay_account_locale' ] ), 'updated_option action does not exist.' );
+ $this->assertNotFalse( has_action( 'woocommerce_woocommerce_payments_updated', [ $this->wcpay_account, 'clear_cache' ] ), 'woocommerce_woocommerce_payments_updated action does not exist.' );
}
public function test_maybe_redirect_to_onboarding_stripe_disconnected_redirects() {
@@ -303,7 +316,7 @@ public function test_maybe_redirect_to_wcpay_connect_do_redirect() {
// Mock WC_Payments_Account without redirect_to to prevent headers already sent error.
$mock_wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
->setMethods( [ 'redirect_to' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] )
->getMock();
$mock_wcpay_account->expects( $this->once() )->method( 'redirect_to' );
@@ -911,8 +924,10 @@ public function test_update_stripe_account_failed() {
->willThrowException(
new API_Exception( 'test', 'bad_request', 400 )
);
- $error_msg = $this->wcpay_account->update_stripe_account( [ 'statement_descriptor' => 'WCPAY_DEV' ] );
- $this->assertEquals( 'test', $error_msg, 'Error message expected' );
+
+ $result = $this->wcpay_account->update_stripe_account( [ 'statement_descriptor' => 'WCPAY_DEV' ] );
+
+ $this->assertInstanceOf( WP_Error::class, $result );
}
/**
@@ -1189,6 +1204,19 @@ function ( $key, $generator, $validator ) {
);
}
+ /**
+ * @param bool $can_manage_woocommerce
+ *
+ * @return Closure
+ */
+ private function create_can_manage_woocommerce_cap_override( bool $can_manage_woocommerce ) {
+ return function ( $allcaps ) use ( $can_manage_woocommerce ) {
+ $allcaps['manage_woocommerce'] = $can_manage_woocommerce;
+
+ return $allcaps;
+ };
+ }
+
/**
* Cache account details.
*
diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php
index a9f8f057bb5..c621a397bc5 100644
--- a/tests/unit/test-class-wc-payments-features.php
+++ b/tests/unit/test-class-wc-payments-features.php
@@ -306,24 +306,8 @@ function ( $pre_option, $option, $default ) {
$this->assertFalse( WC_Payments_Features::is_upe_deferred_intent_enabled() );
}
- public function test_legacy_upe_enabled_with_split_upe_ineligible_merchant() {
+ public function test_deferred_upe_enabled_with_sepa() {
$this->mock_cache->method( 'get' )->willReturn( [ 'capabilities' => [ 'sepa_debit_payments' => 'active' ] ] );
- add_filter(
- 'pre_option_' . WC_Payments_Features::UPE_FLAG_NAME,
- function ( $pre_option, $option, $default ) {
- return '0';
- },
- 10,
- 3
- );
- add_filter(
- 'pre_option_' . WC_Payments_Features::UPE_SPLIT_FLAG_NAME,
- function ( $pre_option, $option, $default ) {
- return '1';
- },
- 10,
- 3
- );
add_filter(
'pre_option_' . WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME,
function ( $pre_option, $option, $default ) {
@@ -332,43 +316,8 @@ function ( $pre_option, $option, $default ) {
10,
3
);
-
- $this->assertTrue( WC_Payments_Features::is_upe_enabled() );
- $this->assertTrue( WC_Payments_Features::is_upe_legacy_enabled() );
- $this->assertFalse( WC_Payments_Features::is_upe_split_enabled() );
- $this->assertFalse( WC_Payments_Features::is_upe_deferred_intent_enabled() );
- }
-
- public function test_split_upe_enabled_with_eligible_merchant() {
- $this->mock_cache->method( 'get' )->willReturn( [ 'capabilities' => [ 'sepa_debit_payments' => 'inactive' ] ] );
- add_filter(
- 'pre_option_' . WC_Payments_Features::UPE_FLAG_NAME,
- function ( $pre_option, $option, $default ) {
- return '0';
- },
- 10,
- 3
- );
- add_filter(
- 'pre_option_' . WC_Payments_Features::UPE_SPLIT_FLAG_NAME,
- function ( $pre_option, $option, $default ) {
- return '1';
- },
- 10,
- 3
- );
- add_filter(
- 'pre_option_' . WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME,
- function ( $pre_option, $option, $default ) {
- return '1';
- },
- 10,
- 3
- );
-
$this->assertTrue( WC_Payments_Features::is_upe_enabled() );
$this->assertFalse( WC_Payments_Features::is_upe_legacy_enabled() );
- $this->assertTrue( WC_Payments_Features::is_upe_split_enabled() );
$this->assertTrue( WC_Payments_Features::is_upe_deferred_intent_enabled() );
}
diff --git a/tests/unit/test-class-wc-payments-fraud-service.php b/tests/unit/test-class-wc-payments-fraud-service.php
index 9e5d1f94303..b14e88ae41e 100644
--- a/tests/unit/test-class-wc-payments-fraud-service.php
+++ b/tests/unit/test-class-wc-payments-fraud-service.php
@@ -6,7 +6,6 @@
*/
use PHPUnit\Framework\MockObject\MockObject;
-use WCPay\Exceptions\API_Exception;
/**
* WC_Payments_Fraud_Service unit tests.
@@ -40,6 +39,13 @@ class WC_Payments_Fraud_Service_Test extends WCPAY_UnitTestCase {
*/
private $mock_account;
+ /**
+ * Mock WC_Payments_Session_Service.
+ *
+ * @var WC_Payments_Session_Service|MockObject
+ */
+ private $mock_session_service;
+
/**
* Pre-test setup
*/
@@ -49,8 +55,10 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class );
$this->mock_account = $this->createMock( WC_Payments_Account::class );
+ $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
- $this->fraud_service = new WC_Payments_Fraud_Service( $this->mock_api_client, $this->mock_customer_service, $this->mock_account );
+ $this->fraud_service = new WC_Payments_Fraud_Service( $this->mock_api_client, $this->mock_customer_service, $this->mock_account, $this->mock_session_service );
+ $this->fraud_service->init_hooks();
}
public function test_registers_filters_and_actions_properly() {
diff --git a/tests/unit/test-class-wc-payments-incentives-service.php b/tests/unit/test-class-wc-payments-incentives-service.php
index 70df9bc262b..0cbaa3b4ae8 100644
--- a/tests/unit/test-class-wc-payments-incentives-service.php
+++ b/tests/unit/test-class-wc-payments-incentives-service.php
@@ -34,6 +34,7 @@ public function set_up() {
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->incentives_service = new WC_Payments_Incentives_Service( $this->mock_database_cache );
+ $this->incentives_service->init_hooks();
global $menu;
// phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
@@ -49,27 +50,6 @@ public function tear_down() {
$menu = null; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
}
- /**
- * Mocked incentive data.
- *
- * @var array
- */
- private $mock_incentive_data = [
- 'incentive' => [
- 'id' => 'incentive_id',
- 'type' => 'connect_page',
- 'description' => 'incentive_description',
- 'tc_url' => 'incentive_tc_url',
- ],
- // This is the hash of the test store context:
- // 'country' => 'US',
- // 'locale' => 'en_US',
- // 'has_orders' => false,
- // 'has_payments' => false,
- // 'has_wcpay' => false.
- 'context_hash' => '6d37bc19d822af681f896b21065134c7',
- ];
-
public function test_filters_registered_properly() {
$this->assertNotFalse( has_action( 'admin_menu', [ $this->incentives_service, 'add_payments_menu_badge' ] ) );
$this->assertNotFalse( has_filter( 'woocommerce_admin_allowed_promo_notes', [ $this->incentives_service, 'allowed_promo_notes' ] ) );
@@ -267,4 +247,25 @@ function() use ( $response ) {
}
);
}
+
+ /**
+ * Mocked incentive data.
+ *
+ * @var array
+ */
+ private $mock_incentive_data = [
+ 'incentive' => [
+ 'id' => 'incentive_id',
+ 'type' => 'connect_page',
+ 'description' => 'incentive_description',
+ 'tc_url' => 'incentive_tc_url',
+ ],
+ // This is the hash of the test store context:
+ // 'country' => 'US',
+ // 'locale' => 'en_US',
+ // 'has_orders' => false,
+ // 'has_payments' => false,
+ // 'has_wcpay' => false.
+ 'context_hash' => '6d37bc19d822af681f896b21065134c7',
+ ];
}
diff --git a/tests/unit/test-class-wc-payments-onboarding-service.php b/tests/unit/test-class-wc-payments-onboarding-service.php
index b867033e51a..8cb3f11ac99 100644
--- a/tests/unit/test-class-wc-payments-onboarding-service.php
+++ b/tests/unit/test-class-wc-payments-onboarding-service.php
@@ -132,6 +132,7 @@ public function set_up() {
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->onboarding_service = new WC_Payments_Onboarding_Service( $this->mock_api_client, $this->mock_database_cache );
+ $this->onboarding_service->init_hooks();
}
public function test_filters_registered_properly() {
diff --git a/tests/unit/test-class-wc-payments-order-service.php b/tests/unit/test-class-wc-payments-order-service.php
index cc49ef3fee7..d9ee2042bc8 100644
--- a/tests/unit/test-class-wc-payments-order-service.php
+++ b/tests/unit/test-class-wc-payments-order-service.php
@@ -847,15 +847,15 @@ public function test_mark_order_blocked_for_fraud() {
* Tests if the payment was updated to show dispute created.
*/
public function test_mark_payment_dispute_created() {
- // Arrange: Set the dispute_id and reason, and the order status.
- $dispute_id = 'dp_123';
+ // Arrange: Set the charge_id and reason, and the order status.
+ $charge_id = 'ch_123';
$amount = '$123.45';
$reason = 'product_not_received';
$deadline = 'June 7, 2023';
$order_status = Order_Status::ON_HOLD;
// Act: Attempt to mark payment dispute created.
- $this->order_service->mark_payment_dispute_created( $this->order, $dispute_id, $amount, $reason, $deadline );
+ $this->order_service->mark_payment_dispute_created( $this->order, $charge_id, $amount, $reason, $deadline );
// Assert: Check that the order status was updated to on-hold status.
$this->assertTrue( $this->order->has_status( [ $order_status ] ) );
@@ -867,13 +867,13 @@ public function test_mark_payment_dispute_created() {
$this->assertStringContainsString( $amount, $notes[0]->content );
$this->assertStringContainsString( 'Product not received', $notes[0]->content );
$this->assertStringContainsString( $deadline, $notes[0]->content );
- $this->assertStringContainsString( '/payments/disputes/details&id=dp_123" target="_blank" rel="noopener noreferrer">Response due by', $notes[0]->content );
+ $this->assertStringContainsString( '/payments/transactions/details&id=ch_123" target="_blank" rel="noopener noreferrer">Response due by', $notes[0]->content );
// Assert: Check that order status change note was added.
$this->assertStringContainsString( 'Pending payment to On hold', $notes[1]->content );
// Assert: Applying the same data multiple times does not cause duplicate actions.
- $this->order_service->mark_payment_dispute_created( $this->order, $dispute_id, $amount, $reason, $deadline );
+ $this->order_service->mark_payment_dispute_created( $this->order, $charge_id, $amount, $reason, $deadline );
$notes_2 = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
$this->assertCount( 2, $notes_2 );
}
@@ -882,17 +882,17 @@ public function test_mark_payment_dispute_created() {
* Tests to make sure mark_payment_dispute_created exits if the order is invalid.
*/
public function test_mark_payment_dispute_created_exits_if_order_invalid() {
- // Arrange: Set the dispute_id and reason, and the order status.
- $dispute_id = 'dp_123';
- $amount = '$123.45';
- $reason = 'product_not_received';
- $deadline = 'June 7, 2023';
+ // Arrange: Set the charge_id and reason, and the order status.
+ $charge_id = 'ch_123';
+ $amount = '$123.45';
+ $reason = 'product_not_received';
+ $deadline = 'June 7, 2023';
$order_status = $this->order->get_status();
$expected_notes = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
// Act: Attempt to mark payment dispute created.
- $this->order_service->mark_payment_dispute_created( 'fake_order', $dispute_id, $amount, $reason, $deadline );
+ $this->order_service->mark_payment_dispute_created( 'fake_order', $charge_id, $amount, $reason, $deadline );
// Assert: Check that the order status was not updated.
$this->assertTrue( $this->order->has_status( [ $order_status ] ) );
@@ -906,13 +906,13 @@ public function test_mark_payment_dispute_created_exits_if_order_invalid() {
* Tests if the payment was updated to show dispute closed with a win.
*/
public function test_mark_payment_dispute_closed_with_status_won() {
- // Arrange: Set the dispute_id and status, and the order status.
- $dispute_id = 'dp_123';
+ // Arrange: Set the charge_id and status, and the order status.
+ $charge_id = 'ch_123';
$status = 'won';
$order_status = Order_Status::COMPLETED;
// Act: Attempt to mark payment dispute created.
- $this->order_service->mark_payment_dispute_closed( $this->order, $dispute_id, $status );
+ $this->order_service->mark_payment_dispute_closed( $this->order, $charge_id, $status );
// Assert: Check that the order status was updated to completed status.
$this->assertTrue( $this->order->has_status( [ $order_status ] ) );
@@ -921,10 +921,10 @@ public function test_mark_payment_dispute_closed_with_status_won() {
$notes = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
$this->assertStringContainsString( 'Pending payment to Completed', $notes[1]->content );
$this->assertStringContainsString( 'Payment dispute has been closed with status won', $notes[0]->content );
- $this->assertStringContainsString( '/payments/disputes/details&id=dp_123" target="_blank" rel="noopener noreferrer">dispute overview', $notes[0]->content );
+ $this->assertStringContainsString( '/payments/transactions/details&id=ch_123" target="_blank" rel="noopener noreferrer">dispute overview', $notes[0]->content );
// Assert: Applying the same data multiple times does not cause duplicate actions.
- $this->order_service->mark_payment_dispute_closed( $this->order, $dispute_id, $status );
+ $this->order_service->mark_payment_dispute_closed( $this->order, $charge_id, $status );
$notes_2 = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
$this->assertCount( 2, $notes_2 );
}
@@ -933,14 +933,14 @@ public function test_mark_payment_dispute_closed_with_status_won() {
* Tests if the payment was updated to show dispute closed with a loss and a refund.
*/
public function test_mark_payment_dispute_closed_with_status_lost() {
- // Arrange: Set the dispute_id, dispute status, the order status, and update the order status.
- $dispute_id = 'dp_123';
+ // Arrange: Set the charge_id, dispute status, the order status, and update the order status.
+ $charge_id = 'ch_123';
$status = 'lost';
$order_status = Order_Status::ON_HOLD;
$this->order->update_status( $order_status ); // When a dispute is created, the order status is changed to On Hold.
// Act: Attempt to mark payment dispute created.
- $this->order_service->mark_payment_dispute_closed( $this->order, $dispute_id, $status );
+ $this->order_service->mark_payment_dispute_closed( $this->order, $charge_id, $status );
// Assert: Check that the order status was left in on-hold status.
$this->assertTrue( $this->order->has_status( [ $order_status ] ) );
@@ -949,7 +949,7 @@ public function test_mark_payment_dispute_closed_with_status_lost() {
$notes = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
$this->assertStringContainsString( 'On hold to Refunded', $notes[1]->content );
$this->assertStringContainsString( 'Payment dispute has been closed with status lost', $notes[0]->content );
- $this->assertStringContainsString( '/payments/disputes/details&id=dp_123" target="_blank" rel="noopener noreferrer">dispute overview', $notes[0]->content );
+ $this->assertStringContainsString( '/payments/transactions/details&id=ch_123" target="_blank" rel="noopener noreferrer">dispute overview', $notes[0]->content );
// Assert: Check for created refund, and the amount is correct.
$refunds = $this->order->get_refunds();
@@ -957,7 +957,7 @@ public function test_mark_payment_dispute_closed_with_status_lost() {
$this->assertEquals( '-' . $this->order->get_total(), $refunds[0]->get_total() );
// Assert: Applying the same data multiple times does not cause duplicate actions.
- $this->order_service->mark_payment_dispute_closed( $this->order, $dispute_id, $status );
+ $this->order_service->mark_payment_dispute_closed( $this->order, $charge_id, $status );
$notes_2 = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
$this->assertCount( 3, $notes_2 );
}
@@ -966,14 +966,14 @@ public function test_mark_payment_dispute_closed_with_status_lost() {
* Tests to make sure mark_payment_dispute_closed exits if the order is invalid.
*/
public function test_mark_payment_dispute_closed_exits_if_order_invalid() {
- // Arrange: Set the dispute_id and reason, and the order status.
- $dispute_id = 'dp_123';
+ // Arrange: Set the charge_id and reason, and the order status.
+ $charge_id = 'ch_123';
$status = 'won';
$order_status = $this->order->get_status();
$expected_notes = wc_get_order_notes( [ 'order_id' => $this->order->get_id() ] );
// Act: Attempt to mark payment dispute created.
- $this->order_service->mark_payment_dispute_closed( 'fake_order', $dispute_id, $status );
+ $this->order_service->mark_payment_dispute_closed( 'fake_order', $charge_id, $status );
// Assert: Check that the order status was not updated.
$this->assertTrue( $this->order->has_status( [ $order_status ] ) );
diff --git a/tests/unit/test-class-wc-payments-session-service.php b/tests/unit/test-class-wc-payments-session-service.php
new file mode 100644
index 00000000000..3a1b7130f56
--- /dev/null
+++ b/tests/unit/test-class-wc-payments-session-service.php
@@ -0,0 +1,318 @@
+mock_api_client = $this->createMock( WC_Payments_API_Client::class );
+
+ $this->session_service = new WC_Payments_Session_Service( $this->mock_api_client );
+
+ $this->mock_session_handler = $this->createMock( WC_Session_Handler::class );
+ }
+
+ public function test_user_just_logged_in_returns_false_for_no_user_logged_in() {
+ wp_set_current_user( 0 );
+
+ $this->assertFalse( $this->session_service->user_just_logged_in() );
+ }
+
+ public function test_user_just_logged_in_returns_false_on_invalid_session() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $customer_id = 1;
+
+ wp_set_current_user( $customer_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->getMockBuilder( \stdClass::class )
+ ->addMethods( [ 'set' ] )
+ ->getMock();
+
+ add_filter( 'woocommerce_session_handler', [ $this, 'return_stdclass' ] );
+
+ $this->assertFalse( $this->session_service->user_just_logged_in() );
+
+ remove_filter( 'woocommerce_session_handler', [ $this, 'return_stdclass' ] );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_user_just_logged_in_returns_false_for_missing_or_invalid_session_cookie() {
+ wp_set_current_user( 1 );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->mock_session_handler;
+
+ $this->mock_session_handler
+ ->method( 'get_session_cookie' )
+ ->willReturn( false );
+
+ $this->assertFalse( $this->session_service->user_just_logged_in() );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_user_just_logged_in() {
+ $new_customer_id = 1;
+ $old_customer_id = 2;
+
+ wp_set_current_user( $new_customer_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->mock_session_handler;
+
+ $this->mock_session_handler
+ ->method( 'get_session_cookie' )
+ ->willReturn(
+ [
+ $old_customer_id,
+ time() + ( 60 * 60 * 48 ),
+ time() + ( 60 * 60 * 47 ),
+ 'hash',
+ ]
+ );
+ $this->mock_session_handler
+ ->method( 'get_customer_id' )
+ ->willReturn( $new_customer_id );
+
+ $this->assertTrue( $this->session_service->user_just_logged_in() );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_get_sift_session_id_returns_null_when_no_jetpack_connection() {
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( null );
+
+ $this->assertNull( $this->session_service->get_sift_session_id() );
+ }
+
+ public function test_get_sift_session_id_from_cookie_when_user_just_logged_in() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $new_customer_id = 1;
+ $old_customer_id = 222;
+
+ wp_set_current_user( $new_customer_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->mock_session_handler;
+
+ $this->mock_session_handler
+ ->method( 'get_session_cookie' )
+ ->willReturn(
+ [
+ $old_customer_id,
+ time() + ( 60 * 60 * 48 ),
+ time() + ( 60 * 60 * 47 ),
+ 'hash',
+ ]
+ );
+ $this->mock_session_handler
+ ->method( 'get_customer_id' )
+ ->willReturn( $new_customer_id );
+
+ $this->assertEquals( $wpcom_blog_id . '_' . $old_customer_id, $this->session_service->get_sift_session_id() );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_get_sift_session_id_from_session_when_didnt_user_just_logged_in() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $customer_id = 1;
+
+ wp_set_current_user( $customer_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->mock_session_handler;
+
+ $this->mock_session_handler
+ ->method( 'get_session_cookie' )
+ ->willReturn(
+ [
+ $customer_id, // The same customer ID is used for the cookie and the session.
+ time() + ( 60 * 60 * 48 ),
+ time() + ( 60 * 60 * 47 ),
+ 'hash',
+ ]
+ );
+ $this->mock_session_handler
+ ->method( 'get_customer_id' )
+ ->willReturn( $customer_id );
+
+ $this->assertEquals( $wpcom_blog_id . '_' . $customer_id, $this->session_service->get_sift_session_id() );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_get_sift_session_id_returns_null_on_invalid_session() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $customer_id = 1;
+
+ wp_set_current_user( $customer_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->getMockBuilder( \stdClass::class )
+ ->addMethods( [ 'set' ] )
+ ->getMock();
+
+ add_filter( 'woocommerce_session_handler', [ $this, 'return_stdclass' ] );
+
+ $this->assertNull( $this->session_service->get_sift_session_id() );
+
+ remove_filter( 'woocommerce_session_handler', [ $this, 'return_stdclass' ] );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_get_cookie_session_id_returns_null_when_no_jetpack_connection() {
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( null );
+
+ $this->assertNull( $this->session_service->get_cookie_session_id() );
+ }
+
+ public function test_get_cookie_session_id_returns_null_when_no_session_handler() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = null;
+
+ $this->assertNull( $this->session_service->get_cookie_session_id() );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_get_cookie_session_id_returns_null_when_invalid_session_handler() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->getMockBuilder( \stdClass::class )
+ ->addMethods( [ 'set' ] )
+ ->getMock();
+
+ add_filter( 'woocommerce_session_handler', [ $this, 'return_stdclass' ] );
+
+ $this->assertNull( $this->session_service->get_cookie_session_id() );
+
+ remove_filter( 'woocommerce_session_handler', [ $this, 'return_stdclass' ] );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_get_cookie_session_id_returns_null_when_no_cookie() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->mock_session_handler;
+
+ $this->mock_session_handler
+ ->method( 'get_session_cookie' )
+ ->willReturn( false );
+
+ $this->assertNull( $this->session_service->get_cookie_session_id() );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_get_cookie_session_id() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->mock_session_handler;
+
+ $customer_id = 'customer_id';
+ $this->mock_session_handler
+ ->method( 'get_session_cookie' )
+ ->willReturn(
+ [
+ $customer_id,
+ time() + ( 60 * 60 * 48 ),
+ time() + ( 60 * 60 * 47 ),
+ 'hash',
+ ]
+ );
+
+ $this->assertEquals( $wpcom_blog_id . '_' . $customer_id, $this->session_service->get_cookie_session_id() );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function test_link_current_session_to_customer() {
+ $wpcom_blog_id = 'blog_id';
+ $this->mock_api_client->method( 'get_blog_id' )->willReturn( $wpcom_blog_id );
+
+ $tmp_session = WC()->session;
+ WC()->session = $this->mock_session_handler;
+
+ $customer_id = 'customer_id';
+ $this->mock_session_handler
+ ->method( 'get_session_cookie' )
+ ->willReturn(
+ [
+ $customer_id,
+ time() + ( 60 * 60 * 48 ),
+ time() + ( 60 * 60 * 47 ),
+ 'hash',
+ ]
+ );
+
+ $expected_response = [ 'success' => true ];
+ $this->mock_api_client
+ ->expects( $this->once() )
+ ->method( 'link_session_to_customer' )
+ ->with( $wpcom_blog_id . '_' . $customer_id, $customer_id )
+ ->willReturn( $expected_response );
+
+ $this->assertEquals( $expected_response, $this->session_service->link_current_session_to_customer( $customer_id ) );
+
+ WC()->session = $tmp_session;
+ }
+
+ public function return_stdclass() {
+ return stdClass::class;
+ }
+}
diff --git a/tests/unit/test-class-wc-payments-token-service.php b/tests/unit/test-class-wc-payments-token-service.php
index fe37bc5ef53..e1e489608dd 100644
--- a/tests/unit/test-class-wc-payments-token-service.php
+++ b/tests/unit/test-class-wc-payments-token-service.php
@@ -111,9 +111,10 @@ public function test_add_token_to_user_for_sepa() {
}
/**
- * Test add SEPA token to user with split UPE.
+ * Test add SEPA token to user with deferred intent creation UPE.
*/
- public function test_add_token_to_user_for_sepa_split_upe() {
+ public function test_add_token_to_user_for_sepa_deferred_intent_creation_upe() {
+ update_option( '_wcpay_feature_upe_deferred_intent', '1' );
update_option( WC_Payments_Features::UPE_SPLIT_FLAG_NAME, '1' );
$mock_payment_method = [
'id' => 'pm_mock',
@@ -542,7 +543,7 @@ public function test_woocommerce_get_customer_payment_tokens_not_added_twice_for
}
public function test_woocommerce_get_customer_payment_tokens_not_added_from_different_gateway() {
- update_option( '_wcpay_feature_upe_split', '1' );
+ update_option( '_wcpay_feature_upe_deferred_intent', '1' );
$gateway_id = WC_Payment_Gateway_WCPay::GATEWAY_ID;
$tokens = [];
$payment_methods = [ Payment_Method::CARD, Payment_Method::SEPA ];
diff --git a/tests/unit/test-class-wc-payments.php b/tests/unit/test-class-wc-payments.php
index beee6c8652e..c0f50592560 100644
--- a/tests/unit/test-class-wc-payments.php
+++ b/tests/unit/test-class-wc-payments.php
@@ -79,7 +79,7 @@ public function test_it_does_not_register_woopay_hooks_if_feature_flag_is_disabl
}
public function test_it_skips_stripe_link_gateway_registration() {
- update_option( WC_Payments_Features::UPE_SPLIT_FLAG_NAME, '1' );
+ update_option( WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME, '1' );
$card_gateway_mock = $this->createMock( UPE_Split_Payment_Gateway::class );
$card_gateway_mock
@@ -103,7 +103,7 @@ public function test_it_skips_stripe_link_gateway_registration() {
$this->assertInstanceOf( UPE_Split_Payment_Gateway::class, $registered_gateways[0] );
$this->assertEquals( $registered_gateways[0]->get_stripe_id(), 'card' );
- update_option( WC_Payments_Features::UPE_SPLIT_FLAG_NAME, '0' );
+ update_option( WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME, '0' );
}
public function test_rest_endpoints_validate_nonce() {
diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
index 04d19e6b6ed..2e7daeae81b 100644
--- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
+++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
@@ -250,6 +250,30 @@ public function test_update_customer_with_whitespace_customer_id() {
* @throws API_Exception
*/
public function test_get_onboarding_data() {
+ $site_data = [
+ 'site_username' => 'admin',
+ 'site_locale' => 'en_US',
+ ];
+
+ $user_data = [
+ 'user_id' => 1,
+ 'ip_address' => '0.0.0.0',
+ 'browser' => [
+ 'user_agent' => 'Unit Test Agent/0.1.0',
+ 'accept_language' => 'en-US,en;q=0.5',
+ 'content_language' => 'en-US,en;q=0.5',
+ ],
+ 'referer' => 'https://example.com',
+ ];
+
+ $account_data = [];
+
+ $actioned_notes = [
+ 'd' => 4,
+ 'e' => 5,
+ 'f' => 6,
+ ];
+
$this->mock_http_client
->expects( $this->once() )
->method( 'remote_request' )
@@ -265,19 +289,13 @@ function ( $data ): bool {
[
'test_mode' => false,
'return_url' => 'http://localhost',
- 'site_data' => [
- 'site_username' => 'admin',
- 'site_locale' => 'en_US',
- ],
+ 'site_data' => $site_data,
+ 'user_data' => $user_data,
+ 'account_data' => $account_data,
+ 'actioned_notes' => $actioned_notes,
'create_live_account' => true,
- 'actioned_notes' => [
- 'd' => 4,
- 'e' => 5,
- 'f' => 6,
- ],
'progressive' => false,
'collect_payout_requirements' => false,
- 'account_data' => [],
]
),
true,
@@ -296,15 +314,10 @@ function ( $data ): bool {
// Call the method under test.
$result = $this->payments_api_client->get_onboarding_data(
'http://localhost',
- [
- 'site_username' => 'admin',
- 'site_locale' => 'en_US',
- ],
- [
- 'd' => 4,
- 'e' => 5,
- 'f' => 6,
- ]
+ $site_data,
+ $user_data,
+ $account_data,
+ $actioned_notes
);
// Assert the response is correct.
diff --git a/tests/unit/woopay/test-class-woopay-session.php b/tests/unit/woopay/test-class-woopay-session.php
index 647637bb1bc..f17c094f192 100644
--- a/tests/unit/woopay/test-class-woopay-session.php
+++ b/tests/unit/woopay/test-class-woopay-session.php
@@ -5,10 +5,12 @@
* @package WooCommerce\Payments\Tests
*/
+use PHPUnit\Framework\MockObject\MockObject;
use WCPay\WooPay\WooPay_Session;
use WCPay\Platform_Checkout\WooPay_Store_Api_Token;
use WCPay\Platform_Checkout\SessionHandler;
use WCPay\WooPay\WooPay_Scheduler;
+use WCPay\MultiCurrency\MultiCurrency;
/**
* WooPay_Session unit tests.
@@ -19,6 +21,20 @@ class WooPay_Session_Test extends WCPAY_UnitTestCase {
*/
protected $mock_cache;
+ /**
+ * Mock WC_Payments_Customer_Service.
+ *
+ * @var WC_Payments_Customer_Service|MockObject
+ */
+ private $mock_customer_service;
+
+ /**
+ * WC_Payments_Customer_Service.
+ *
+ * @var WC_Payments_Customer_Service
+ */
+ private $original_customer_service;
+
public function set_up() {
parent::set_up();
@@ -33,6 +49,15 @@ public function set_up() {
$_SERVER['HTTP_USER_AGENT'] = 'WooPay';
$_SERVER['REQUEST_URI'] = '/wp-json/wc/store/v1/checkout';
+
+ $this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class );
+ $this->original_customer_service = WC_Payments::get_customer_service();
+ WC_Payments::set_customer_service( $this->mock_customer_service );
+ }
+
+ public function tear_down() {
+ WC_Payments::set_customer_service( $this->original_customer_service );
+ parent::tear_down();
}
public function test_get_user_id_from_cart_token_with_guest_user() {
@@ -188,6 +213,62 @@ public function test_remove_order_customer_id_on_requests_with_verified_email_wi
$this->assertEquals( $updated_order->get_customer_id(), 0 );
}
+ public function test_session_currency_set_for_multi_currency_enabled() {
+ $user_id = 1;
+ $this->mock_customer_service
+ ->expects( $this->once() )
+ ->method( 'get_customer_id_by_user_id' )
+ ->with( $user_id )
+ ->willReturn( $user_id );
+
+ // For multi-currency enabled.
+ update_option( '_wcpay_feature_customer_multi_currency', '1' );
+
+ // Set mismatched user and session currency codes.
+ WC()->session->set( MultiCurrency::CURRENCY_SESSION_KEY, 'ABC' );
+ wp_set_current_user( $user_id );
+ update_user_meta( $user_id, MultiCurrency::CURRENCY_META_KEY, 'DEF' );
+
+ WooPay_Session::get_frontend_init_session_request();
+
+ // Currency in session should have been modified.
+ $this->assertSame(
+ 'DEF',
+ WC()->session->get( MultiCurrency::CURRENCY_SESSION_KEY )
+ );
+
+ // Destroy session data.
+ WC()->session->set( MultiCurrency::CURRENCY_SESSION_KEY, null );
+ }
+
+ public function test_session_currency_not_set_for_multi_currency_disabled() {
+ $user_id = 1;
+ $this->mock_customer_service
+ ->expects( $this->once() )
+ ->method( 'get_customer_id_by_user_id' )
+ ->with( $user_id )
+ ->willReturn( $user_id );
+
+ // For multi-currency disabled.
+ update_option( '_wcpay_feature_customer_multi_currency', '0' );
+
+ // Set mismatched user and session currency codes.
+ WC()->session->set( MultiCurrency::CURRENCY_SESSION_KEY, 'ABC' );
+ wp_set_current_user( $user_id );
+ update_user_meta( $user_id, MultiCurrency::CURRENCY_META_KEY, 'DEF' );
+
+ WooPay_Session::get_frontend_init_session_request();
+
+ // Currency in session should NOT have been modified.
+ $this->assertSame(
+ 'ABC',
+ WC()->session->get( MultiCurrency::CURRENCY_SESSION_KEY )
+ );
+
+ // Destroy session data.
+ WC()->session->set( MultiCurrency::CURRENCY_SESSION_KEY, null );
+ }
+
private function setup_session( $customer_id, $customer_email = null ) {
$session_handler = new SessionHandler();
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index 457f5262caf..82845cf0a74 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -12,7 +12,7 @@
* WC tested up to: 7.8.0
* Requires at least: 6.0
* Requires PHP: 7.3
- * Version: 6.5.1
+ * Version: 6.6.0
*
* @package WooCommerce\Payments
*/