Checkout processing and order creation APIs (https://github.com/woocommerce/woocommerce-blocks/pull/2044)
* CheckoutProcessing work add missing memoization and implement useRef strategically This prevents effects from firing unnecessarily. Tweak assets registration Order hydration and checkout/ endpoint updates Fix error handling Error handling * Missing isset in stripe * Fedeback * rename draft order ID action * Todos
This commit is contained in:
parent
5e78c47e4d
commit
2bca9840c6
|
@ -8,10 +8,7 @@
|
|||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* @type {BillingData}
|
||||
*/
|
||||
const HYDRATED_BILLING_DATA = getSetting( 'billingData' );
|
||||
const checkoutData = getSetting( 'checkoutData', {} );
|
||||
|
||||
/**
|
||||
* @type {BillingData}
|
||||
|
@ -35,7 +32,7 @@ export const DEFAULT_BILLING_DATA = {
|
|||
*/
|
||||
export const DEFAULT_STATE = {
|
||||
...DEFAULT_BILLING_DATA,
|
||||
...HYDRATED_BILLING_DATA,
|
||||
...checkoutData.billing_address,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,7 @@ const {
|
|||
SET_NO_ERROR,
|
||||
INCREMENT_CALCULATING,
|
||||
DECREMENT_CALCULATING,
|
||||
SET_ORDER_ID,
|
||||
} = TYPES;
|
||||
|
||||
/**
|
||||
|
@ -43,4 +44,8 @@ export const actions = {
|
|||
decrementCalculating: () => ( {
|
||||
type: DECREMENT_CALCULATING,
|
||||
} ),
|
||||
setOrderId: ( orderId ) => ( {
|
||||
type: SET_ORDER_ID,
|
||||
orderId,
|
||||
} ),
|
||||
};
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* @type {import("@woocommerce/type-defs/checkout").CheckoutStatusConstants}
|
||||
*/
|
||||
|
@ -9,6 +14,8 @@ export const STATUS = {
|
|||
COMPLETE: 'complete',
|
||||
};
|
||||
|
||||
const checkoutData = getSetting( 'checkoutData', { order_id: 0 } );
|
||||
|
||||
export const DEFAULT_STATE = {
|
||||
redirectUrl: '',
|
||||
status: STATUS.PRISTINE,
|
||||
|
@ -17,6 +24,7 @@ export const DEFAULT_STATE = {
|
|||
nextStatus: STATUS.IDLE,
|
||||
hasError: false,
|
||||
calculatingCount: 0,
|
||||
orderId: checkoutData.order_id,
|
||||
};
|
||||
|
||||
export const TYPES = {
|
||||
|
@ -26,6 +34,7 @@ export const TYPES = {
|
|||
SET_PROCESSING: 'set_checkout_is_processing',
|
||||
SET_HAS_ERROR: 'set_checkout_has_error',
|
||||
SET_NO_ERROR: 'set_checkout_no_error',
|
||||
SET_ORDER_ID: 'set_checkout_order_id',
|
||||
INCREMENT_CALCULATING: 'increment_calculating',
|
||||
DECREMENT_CALCULATING: 'decrement_calculating',
|
||||
};
|
||||
|
|
|
@ -43,6 +43,7 @@ const CheckoutContext = createContext( {
|
|||
isProcessing: false,
|
||||
hasError: false,
|
||||
redirectUrl: '',
|
||||
orderId: 0,
|
||||
onCheckoutCompleteSuccess: () => void null,
|
||||
onCheckoutCompleteError: () => void null,
|
||||
onCheckoutProcessing: () => void null,
|
||||
|
@ -53,6 +54,7 @@ const CheckoutContext = createContext( {
|
|||
clearError: () => void null,
|
||||
incrementCalculating: () => void null,
|
||||
decrementCalculating: () => void null,
|
||||
setOrderId: () => void null,
|
||||
},
|
||||
} );
|
||||
|
||||
|
@ -100,15 +102,15 @@ export const CheckoutProvider = ( {
|
|||
}, [ observers ] );
|
||||
const onCheckoutCompleteSuccess = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onCheckoutCompleteSuccess,
|
||||
[]
|
||||
[ subscriber ]
|
||||
);
|
||||
const onCheckoutCompleteError = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onCheckoutCompleteError,
|
||||
[]
|
||||
[ subscriber ]
|
||||
);
|
||||
const onCheckoutProcessing = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onCheckoutProcessing,
|
||||
[]
|
||||
[ subscriber ]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -125,6 +127,8 @@ export const CheckoutProvider = ( {
|
|||
void dispatch( actions.incrementCalculating() ),
|
||||
decrementCalculating: () =>
|
||||
void dispatch( actions.decrementCalculating() ),
|
||||
setOrderId: ( orderId ) =>
|
||||
void dispatch( actions.setOrderId( orderId ) ),
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
@ -195,6 +199,8 @@ export const CheckoutProvider = ( {
|
|||
onCheckoutProcessing,
|
||||
dispatchActions,
|
||||
isEditor,
|
||||
orderId: checkoutState.orderId,
|
||||
hasOrder: !! checkoutState.orderId,
|
||||
};
|
||||
return (
|
||||
<CheckoutContext.Provider value={ checkoutData }>
|
||||
|
|
|
@ -1,12 +1,112 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import triggerFetch from '@wordpress/api-fetch';
|
||||
import {
|
||||
useCheckoutContext,
|
||||
useShippingDataContext,
|
||||
useBillingDataContext,
|
||||
usePaymentMethodDataContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import { useEffect, useRef, useCallback } from '@wordpress/element';
|
||||
import { useStoreNotices } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* CheckoutProcessor component. @todo Needs to consume all contexts.
|
||||
*
|
||||
* Subscribes to checkout context and triggers processing when needed:
|
||||
* 1. Create an order (draft) when email address is provided
|
||||
* 2. Process an order when the place order button is pressed
|
||||
* (checkout processing)
|
||||
* Subscribes to checkout context and triggers processing via the API.
|
||||
*/
|
||||
const CheckoutProcessor = () => {
|
||||
const { onCheckoutProcessing, dispatchActions } = useCheckoutContext();
|
||||
const { shippingAddress } = useShippingDataContext();
|
||||
const { billingData } = useBillingDataContext();
|
||||
const { activePaymentMethod } = usePaymentMethodDataContext();
|
||||
const { addErrorNotice } = useStoreNotices();
|
||||
const currentBillingData = useRef( billingData );
|
||||
const currentShippingAddress = useRef( shippingAddress );
|
||||
|
||||
useEffect( () => {
|
||||
currentBillingData.current = billingData;
|
||||
currentShippingAddress.current = shippingAddress;
|
||||
}, [ billingData, shippingAddress ] );
|
||||
|
||||
/**
|
||||
* Process an order via the /wc/store/checkout endpoint.
|
||||
*
|
||||
* @return {boolean} True if everything was successful.
|
||||
*/
|
||||
const processCheckout = useCallback( async () => {
|
||||
await triggerFetch( {
|
||||
path: '/wc/store/checkout',
|
||||
method: 'POST',
|
||||
data: {
|
||||
payment_method: activePaymentMethod,
|
||||
// @todo Hook this up to payment method data.
|
||||
payment_data: [],
|
||||
billing_address: currentBillingData.current,
|
||||
shipping_address: currentShippingAddress.current,
|
||||
customer_note: '',
|
||||
},
|
||||
cache: 'no-store',
|
||||
parse: false,
|
||||
} )
|
||||
.then( ( fetchResponse ) => {
|
||||
// Update nonce.
|
||||
triggerFetch.setNonce( fetchResponse.headers );
|
||||
|
||||
// Handle response.
|
||||
fetchResponse.json().then( function( response ) {
|
||||
if ( ! fetchResponse.ok ) {
|
||||
// We received an error response.
|
||||
if ( response.body && response.body.message ) {
|
||||
addErrorNotice( response.body.message, {
|
||||
id: 'checkout',
|
||||
} );
|
||||
} else {
|
||||
addErrorNotice(
|
||||
__(
|
||||
'Something went wrong. Please check your payment details and try again.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
{
|
||||
id: 'checkout',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dispatchActions.setRedirectUrl(
|
||||
response.payment_result.redirect_url
|
||||
);
|
||||
|
||||
// @todo do we need to trigger more handling here?
|
||||
} );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
addErrorNotice( error.message, {
|
||||
id: 'checkout',
|
||||
} );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}, [
|
||||
addErrorNotice,
|
||||
activePaymentMethod,
|
||||
currentBillingData,
|
||||
currentShippingAddress,
|
||||
] );
|
||||
|
||||
/**
|
||||
* When the checkout is processing, process the order.
|
||||
*/
|
||||
useEffect( () => {
|
||||
const unsubscribeProcessing = onCheckoutProcessing( processCheckout );
|
||||
return () => {
|
||||
unsubscribeProcessing();
|
||||
};
|
||||
}, [ onCheckoutProcessing, processCheckout ] );
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ const {
|
|||
SET_NO_ERROR,
|
||||
INCREMENT_CALCULATING,
|
||||
DECREMENT_CALCULATING,
|
||||
SET_ORDER_ID,
|
||||
} = TYPES;
|
||||
|
||||
const { PRISTINE, IDLE, CALCULATING, PROCESSING, COMPLETE } = STATUS;
|
||||
|
@ -22,7 +23,7 @@ const { PRISTINE, IDLE, CALCULATING, PROCESSING, COMPLETE } = STATUS;
|
|||
* @param {Object} state Current state.
|
||||
* @param {Object} action Incoming action object.
|
||||
*/
|
||||
export const reducer = ( state = DEFAULT_STATE, { url, type } ) => {
|
||||
export const reducer = ( state = DEFAULT_STATE, { url, type, orderId } ) => {
|
||||
let status, nextStatus, newState;
|
||||
switch ( type ) {
|
||||
case SET_PRISTINE:
|
||||
|
@ -120,6 +121,12 @@ export const reducer = ( state = DEFAULT_STATE, { url, type } ) => {
|
|||
calculatingCount: Math.max( 0, state.calculatingCount - 1 ),
|
||||
};
|
||||
break;
|
||||
case SET_ORDER_ID:
|
||||
newState = {
|
||||
...state,
|
||||
orderId,
|
||||
};
|
||||
break;
|
||||
}
|
||||
// automatically update state to idle from pristine as soon as it
|
||||
// initially changes.
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { useStoreNoticesContext } from '@woocommerce/base-context';
|
||||
import { useMemo } from '@wordpress/element';
|
||||
|
||||
export const useStoreNotices = () => {
|
||||
const {
|
||||
|
@ -11,40 +12,43 @@ export const useStoreNotices = () => {
|
|||
createSnackbarNotice,
|
||||
} = useStoreNoticesContext();
|
||||
|
||||
const noticesApi = {
|
||||
addDefaultNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'default', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
addErrorNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'error', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
addWarningNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'warning', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
addInfoNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'info', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
addSuccessNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'success', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
removeNotices: ( type = null ) => {
|
||||
notices.map( ( notice ) => {
|
||||
if ( type === null || notice.status === type ) {
|
||||
removeNotice( notice.id );
|
||||
}
|
||||
return true;
|
||||
} );
|
||||
},
|
||||
removeNotice,
|
||||
addSnackbarNotice: ( text, noticeProps = {} ) => {
|
||||
createSnackbarNotice( text, noticeProps );
|
||||
},
|
||||
};
|
||||
const noticesApi = useMemo(
|
||||
() => ( {
|
||||
addDefaultNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'default', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
addErrorNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'error', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
addWarningNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'warning', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
addInfoNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'info', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
addSuccessNotice: ( text, noticeProps = {} ) =>
|
||||
void createNotice( 'success', text, {
|
||||
...noticeProps,
|
||||
} ),
|
||||
removeNotices: ( type = null ) => {
|
||||
notices.map( ( notice ) => {
|
||||
if ( type === null || notice.status === type ) {
|
||||
removeNotice( notice.id );
|
||||
}
|
||||
return true;
|
||||
} );
|
||||
},
|
||||
removeNotice,
|
||||
addSnackbarNotice: ( text, noticeProps = {} ) => {
|
||||
createSnackbarNotice( text, noticeProps );
|
||||
},
|
||||
} ),
|
||||
[ createNotice, createSnackbarNotice ]
|
||||
);
|
||||
|
||||
return {
|
||||
notices,
|
||||
|
|
|
@ -59,7 +59,7 @@ const Checkout = ( {
|
|||
shippingRates = [],
|
||||
scrollToTop,
|
||||
} ) => {
|
||||
const { isEditor } = useCheckoutContext();
|
||||
const { isEditor, hasOrder } = useCheckoutContext();
|
||||
const {
|
||||
shippingRatesLoading,
|
||||
shippingAddress,
|
||||
|
@ -126,6 +126,12 @@ const Checkout = ( {
|
|||
setBillingData( shippingAddress );
|
||||
}
|
||||
}, [ shippingAsBilling, setBillingData ] );
|
||||
|
||||
if ( ! isEditor && ! hasOrder ) {
|
||||
// @todo add state here to handle this type of error.
|
||||
return <div>No draft order - add error state.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarLayout className="wc-block-checkout">
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
/**
|
||||
* @typedef {Object} CheckoutDispatchActions
|
||||
*
|
||||
* @property {function()} resetCheckout Dispatches an action that resets
|
||||
* the checkout to a pristine state.
|
||||
* @property {function()} setRedirectUrl Dispatches an action that sets the
|
||||
* redirectUrl to the given value.
|
||||
* @property {function()} setHasError Dispatches an action that sets the
|
||||
* checkout status to having an error.
|
||||
* @property {function()} clearError Dispatches an action that clears the
|
||||
* hasError status for the checkout.
|
||||
* @property {function()} incrementCalculating Dispatches an action that increments
|
||||
* the calculating state for checkout by one.
|
||||
* @property {function()} decrementCalculating Dispatches an action that decrements
|
||||
* the calculating state for checkout by one.
|
||||
* @property {function()} resetCheckout Dispatches an action that resets
|
||||
* the checkout to a pristine state.
|
||||
* @property {function( string )} setRedirectUrl Dispatches an action that sets the
|
||||
* redirectUrl to the given value.
|
||||
* @property {function()} setHasError Dispatches an action that sets the
|
||||
* checkout status to having an error.
|
||||
* @property {function()} clearError Dispatches an action that clears the
|
||||
* hasError status for the checkout.
|
||||
* @property {function()} incrementCalculating Dispatches an action that increments
|
||||
* the calculating state for checkout by one.
|
||||
* @property {function()} decrementCalculating Dispatches an action that decrements
|
||||
* the calculating state for checkout by one.
|
||||
* @property {function( number, string )} setOrderId Dispatches an action that stores the draft
|
||||
* order ID and key to state.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -236,6 +236,10 @@
|
|||
* @property {boolean} isEditor Indicates whether in
|
||||
* the editor context
|
||||
* (true) or not (false).
|
||||
* @property {number} orderId This is the ID for the
|
||||
* draft order if one exists.
|
||||
* @property {boolean} hasOrder True when the checkout has
|
||||
* a draft order from the API.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -28548,7 +28548,7 @@
|
|||
}
|
||||
},
|
||||
"woocommerce": {
|
||||
"version": "git+https://github.com/woocommerce/woocommerce.git#1926a5042622980ce9b520e63b8cc30d917d4a07",
|
||||
"version": "git+https://github.com/woocommerce/woocommerce.git#6f2b232fc7fa53417691a742a419147bb0134a5c",
|
||||
"from": "git+https://github.com/woocommerce/woocommerce.git",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
|
|
@ -52,7 +52,7 @@ class Cart extends AbstractBlock {
|
|||
if ( ! empty( $attributes['checkoutPageId'] ) && ! $data_registry->exists( 'page-' . $attributes['checkoutPageId'] ) ) {
|
||||
$permalink = get_permalink( $attributes['checkoutPageId'] );
|
||||
if ( $permalink ) {
|
||||
$data_registry->add( 'page-' . $attributes['checkoutPageId'], get_permalink( $attributes['checkoutPageId'] ) );
|
||||
$data_registry->add( 'page-' . $attributes['checkoutPageId'], $permalink );
|
||||
}
|
||||
}
|
||||
if ( ! $data_registry->exists( 'shippingCountries' ) ) {
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Assets;
|
||||
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
|
@ -48,63 +50,74 @@ class Checkout extends AbstractBlock {
|
|||
*/
|
||||
public function render( $attributes = array(), $content = '' ) {
|
||||
$data_registry = Package::container()->get(
|
||||
\Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class
|
||||
AssetDataRegistry::class
|
||||
);
|
||||
if ( ! empty( $attributes['cartPageId'] ) && ! $data_registry->exists( 'page-' . $attributes['cartPageId'] ) ) {
|
||||
$permalink = get_permalink( $attributes['cartPageId'] );
|
||||
if ( $permalink ) {
|
||||
$data_registry->add( 'page-' . $attributes['cartPageId'], get_permalink( $attributes['cartPageId'] ) );
|
||||
|
||||
$block_data = [
|
||||
'allowedCountries' => [ WC()->countries, 'get_allowed_countries' ],
|
||||
'shippingCountries' => [ WC()->countries, 'get_shipping_countries' ],
|
||||
'allowedStates' => [ WC()->countries, 'get_allowed_country_states' ],
|
||||
'shippingStates' => [ WC()->countries, 'get_shipping_country_states' ],
|
||||
];
|
||||
|
||||
foreach ( $block_data as $key => $callback ) {
|
||||
if ( ! $data_registry->exists( $key ) ) {
|
||||
$data_registry->add( $key, call_user_func( $callback ) );
|
||||
}
|
||||
}
|
||||
if ( ! $data_registry->exists( 'allowedCountries' ) ) {
|
||||
$data_registry->add( 'allowedCountries', WC()->countries->get_allowed_countries() );
|
||||
}
|
||||
if ( ! $data_registry->exists( 'shippingCountries' ) ) {
|
||||
$data_registry->add( 'shippingCountries', WC()->countries->get_shipping_countries() );
|
||||
}
|
||||
if ( ! $data_registry->exists( 'allowedStates' ) ) {
|
||||
$data_registry->add( 'allowedStates', WC()->countries->get_allowed_country_states() );
|
||||
}
|
||||
if ( ! $data_registry->exists( 'shippingStates' ) ) {
|
||||
$data_registry->add( 'shippingStates', WC()->countries->get_shipping_country_states() );
|
||||
}
|
||||
if ( ! $data_registry->exists( 'cartData' ) ) {
|
||||
$data_registry->add( 'cartData', WC()->api->get_endpoint_data( '/wc/store/cart' ) );
|
||||
}
|
||||
if ( ! $data_registry->exists( 'billingData' ) && WC()->customer instanceof \WC_Customer ) {
|
||||
$data_registry->add( 'billingData', WC()->customer->get_billing() );
|
||||
}
|
||||
if ( function_exists( 'get_current_screen' ) ) {
|
||||
$screen = get_current_screen();
|
||||
if ( $screen && $screen->is_block_editor() && ! $data_registry->exists( 'shippingMethodsExist' ) ) {
|
||||
$methods_exist = wc_get_shipping_method_count() > 0;
|
||||
$data_registry->add( 'shippingMethodsExist', $methods_exist );
|
||||
}
|
||||
}
|
||||
if ( is_user_logged_in() && ! $data_registry->exists( 'customerPaymentMethods' ) ) {
|
||||
add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
|
||||
$data_registry->add(
|
||||
'customerPaymentMethods',
|
||||
wc_get_customer_saved_methods_list( get_current_user_id() )
|
||||
);
|
||||
remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
|
||||
}
|
||||
|
||||
if ( is_user_logged_in() && ! $data_registry->exists( 'customerPaymentMethods' ) ) {
|
||||
add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
|
||||
$data_registry->add(
|
||||
'customerPaymentMethods',
|
||||
wc_get_customer_saved_methods_list( get_current_user_id() )
|
||||
);
|
||||
remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
|
||||
$this->hydrate_from_api( $data_registry );
|
||||
$this->hydrate_customer_payment_methods( $data_registry );
|
||||
|
||||
$permalink = ! empty( $attributes['cartPageId'] ) ? get_permalink( $attributes['cartPageId'] ) : false;
|
||||
|
||||
if ( $permalink && ! $data_registry->exists( 'page-' . $attributes['cartPageId'] ) ) {
|
||||
$data_registry->add( 'page-' . $attributes['cartPageId'], $permalink );
|
||||
}
|
||||
|
||||
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;
|
||||
|
||||
if ( $screen && $screen->is_block_editor() && ! $data_registry->exists( 'shippingMethodsExist' ) ) {
|
||||
$methods_exist = wc_get_shipping_method_count() > 0;
|
||||
$data_registry->add( 'shippingMethodsExist', $methods_exist );
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' );
|
||||
\Automattic\WooCommerce\Blocks\Assets::register_block_script( $this->block_name . '-frontend', $this->block_name . '-block-frontend' );
|
||||
Assets::register_block_script( $this->block_name . '-frontend', $this->block_name . '-block-frontend' );
|
||||
do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after' );
|
||||
return $content . $this->get_skeleton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer payment methods for use in checkout.
|
||||
*
|
||||
* @param AssetDataRegistry $data_registry Data registry instance.
|
||||
*/
|
||||
protected function hydrate_customer_payment_methods( AssetDataRegistry $data_registry ) {
|
||||
if ( ! is_user_logged_in() || $data_registry->exists( 'customerPaymentMethods' ) ) {
|
||||
return;
|
||||
}
|
||||
add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
|
||||
$data_registry->add(
|
||||
'customerPaymentMethods',
|
||||
wc_get_customer_saved_methods_list( get_current_user_id() )
|
||||
);
|
||||
remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the checkout block with data from the API.
|
||||
*
|
||||
* @param AssetDataRegistry $data_registry Data registry instance.
|
||||
*/
|
||||
protected function hydrate_from_api( AssetDataRegistry $data_registry ) {
|
||||
if ( ! $data_registry->exists( 'cartData' ) ) {
|
||||
$data_registry->add( 'cartData', WC()->api->get_endpoint_data( '/wc/store/cart' ) );
|
||||
}
|
||||
if ( ! $data_registry->exists( 'checkoutData' ) ) {
|
||||
$data_registry->add( 'checkoutData', WC()->api->get_endpoint_data( '/wc/store/checkout' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render skeleton markup for the checkout block.
|
||||
|
|
|
@ -243,10 +243,7 @@ class Library {
|
|||
|
||||
// Handle result.
|
||||
$result->set_status( isset( $gateway_result['result'] ) && 'success' === $gateway_result['result'] ? 'success' : 'failure' );
|
||||
$result->set_payment_details(
|
||||
[
|
||||
'redirect' => $gateway_result['redirect'],
|
||||
]
|
||||
);
|
||||
$result->set_payment_details( [] );
|
||||
$result->set_redirect_url( $gateway_result['redirect'] );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ class Stripe {
|
|||
*/
|
||||
public function enqueue_data() {
|
||||
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
|
||||
$stripe_gateway = $available_gateways['stripe'] ? $available_gateways['stripe'] : null;
|
||||
$stripe_gateway = isset( $available_gateways['stripe'] ) && is_a( $available_gateways['stripe'], '\WC_Gateway_Stripe' ) ? $available_gateways['stripe'] : null;
|
||||
$data = [
|
||||
'stripeTotalLabel' => $this->get_total_label(),
|
||||
'publicKey' => $this->get_publishable_key(),
|
||||
|
|
|
@ -34,6 +34,13 @@ class PaymentResult {
|
|||
*/
|
||||
protected $payment_details = [];
|
||||
|
||||
/**
|
||||
* Redirect URL for checkout.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirect_url = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
@ -51,7 +58,7 @@ class PaymentResult {
|
|||
* @param string $name Property name.
|
||||
*/
|
||||
public function __get( $name ) {
|
||||
if ( in_array( $name, [ 'status', 'payment_details' ], true ) ) {
|
||||
if ( in_array( $name, [ 'status', 'payment_details', 'redirect_url' ], true ) ) {
|
||||
return $this->$name;
|
||||
}
|
||||
return null;
|
||||
|
@ -83,4 +90,13 @@ class PaymentResult {
|
|||
$this->payment_details[ (string) $key ] = (string) $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set redirect URL.
|
||||
*
|
||||
* @param array $redirect_url URL to redirect the customer to after checkout.
|
||||
*/
|
||||
public function set_redirect_url( $redirect_url = [] ) {
|
||||
$this->redirect_url = esc_url_raw( $redirect_url );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ abstract class AbstractRoute implements RouteInterface {
|
|||
} catch ( RouteException $error ) {
|
||||
$response = new \WP_Error( $error->getErrorCode(), $error->getMessage(), [ 'status' => $error->getCode() ] );
|
||||
} catch ( Exception $error ) {
|
||||
$response = new WP_Error( 'unknown_server_error', $error->getMessage(), [ 'status' => '500' ] );
|
||||
$response = new \WP_Error( 'unknown_server_error', $error->getMessage(), [ 'status' => '500' ] );
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
|
|
@ -1,271 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Cart order route.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
|
||||
|
||||
/**
|
||||
* CartCreateOrder class.
|
||||
*/
|
||||
class CartCreateOrder extends AbstractRoute {
|
||||
/**
|
||||
* Get the namespace for this route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace() {
|
||||
return 'wc/store';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/cart/create-order';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'args' => $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response.
|
||||
*
|
||||
* @throws RouteException On error.
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
$this->draft_order = WC()->session->get(
|
||||
'store_api_draft_order',
|
||||
[
|
||||
'id' => 0,
|
||||
'hashes' => [
|
||||
'line_items' => false,
|
||||
'shipping' => false,
|
||||
'fees' => false,
|
||||
'coupons' => false,
|
||||
'taxes' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Update session based on posted data.
|
||||
$this->update_session( $request );
|
||||
|
||||
// Create or retrieve the draft order for the current cart.
|
||||
$order_object = $this->create_order_from_cart( $request );
|
||||
|
||||
// Try to reserve stock for 10 mins, if available.
|
||||
// @todo Remove once min support for WC reaches 4.0.0.
|
||||
if ( \class_exists( '\Automattic\WooCommerce\Checkout\Helpers\ReserveStock' ) ) {
|
||||
$reserve_stock = new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock();
|
||||
} else {
|
||||
$reserve_stock = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock();
|
||||
}
|
||||
|
||||
$reserve_stock->reserve_stock_for_order( $order_object, 10 );
|
||||
$response = $this->prepare_item_for_response( $order_object, $request );
|
||||
$response->set_status( 201 );
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Before creating anything, this method ensures the cart session is up to date and matches the data we're going
|
||||
* to be adding to the order.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return void
|
||||
*/
|
||||
protected function update_session( \WP_REST_Request $request ) {
|
||||
$schema = $this->get_item_schema();
|
||||
|
||||
if ( isset( $request['billing_address'] ) ) {
|
||||
$allowed_billing_values = array_intersect_key( $request['billing_address'], $schema['properties']['billing_address']['properties'] );
|
||||
foreach ( $allowed_billing_values as $key => $value ) {
|
||||
WC()->customer->{"set_billing_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['shipping_address'] ) ) {
|
||||
$allowed_shipping_values = array_intersect_key( $request['shipping_address'], $schema['properties']['shipping_address']['properties'] );
|
||||
foreach ( $allowed_shipping_values as $key => $value ) {
|
||||
WC()->customer->{"set_shipping_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
WC()->customer->save();
|
||||
WC()->cart->calculate_shipping();
|
||||
WC()->cart->calculate_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order and set props based on global settings.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return \WC_Order A new order object.
|
||||
*/
|
||||
protected function create_order_from_cart( \WP_REST_Request $request ) {
|
||||
add_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
|
||||
|
||||
$order = $this->get_order_object();
|
||||
$order->set_status( 'checkout-draft' );
|
||||
$order->set_created_via( 'store-api' );
|
||||
$order->set_currency( get_woocommerce_currency() );
|
||||
$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
|
||||
$order->set_customer_id( get_current_user_id() );
|
||||
$order->set_customer_ip_address( \WC_Geolocation::get_ip_address() );
|
||||
$order->set_customer_user_agent( wc_get_user_agent() );
|
||||
$order->set_cart_hash( WC()->cart->get_cart_hash() );
|
||||
$order->update_meta_data( 'is_vat_exempt', WC()->cart->get_customer()->get_is_vat_exempt() ? 'yes' : 'no' );
|
||||
|
||||
$this->set_props_from_request( $order, $request );
|
||||
$this->create_line_items_from_cart( $order, $request );
|
||||
|
||||
// Calc totals, taxes, and save.
|
||||
$order->calculate_totals();
|
||||
|
||||
remove_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
|
||||
|
||||
// Store Order details in session so we can look it up later.
|
||||
WC()->session->set(
|
||||
'store_api_draft_order',
|
||||
[
|
||||
'id' => $order->get_id(),
|
||||
'hashes' => $this->get_cart_hashes(),
|
||||
]
|
||||
);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashes for items in the current cart. Useful for tracking changes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_cart_hashes() {
|
||||
return [
|
||||
'line_items' => md5( wp_json_encode( WC()->cart->get_cart() ) ),
|
||||
'shipping' => md5( wp_json_encode( WC()->cart->shipping_methods ) ),
|
||||
'fees' => md5( wp_json_encode( WC()->cart->get_fees() ) ),
|
||||
'coupons' => md5( wp_json_encode( WC()->cart->get_applied_coupons() ) ),
|
||||
'taxes' => md5( wp_json_encode( WC()->cart->get_taxes() ) ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an order object, either using a current draft order, or returning a new one.
|
||||
*
|
||||
* @return \WC_Order A new order object.
|
||||
*/
|
||||
protected function get_order_object() {
|
||||
$draft_order = $this->draft_order['id'] ? wc_get_order( $this->draft_order['id'] ) : false;
|
||||
|
||||
if ( $draft_order && $draft_order->has_status( 'checkout-draft' ) && 'store-api' === $draft_order->get_created_via() ) {
|
||||
return $draft_order;
|
||||
}
|
||||
|
||||
return new \WC_Order();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes default order status to draft for orders created via this API.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function default_order_status() {
|
||||
return 'checkout-draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order line items.
|
||||
*
|
||||
* @internal Knowing if items changed between the order and cart can be complex. Line items are ok because there is a
|
||||
* hash, but no hash exists for other line item types. Having a normalized set of data between cart and order, or
|
||||
* additional hashes, would be useful in the future and to help refactor this code. In the meantime, we're relying
|
||||
* on custom hashes in $this->draft_order to track if things changed.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
*/
|
||||
protected function create_line_items_from_cart( \WC_Order $order, \WP_REST_Request $request ) {
|
||||
$new_hashes = $this->get_cart_hashes();
|
||||
$old_hashes = $this->draft_order['hashes'];
|
||||
$force = $this->draft_order['id'] !== $order->get_id();
|
||||
|
||||
if ( $force || $new_hashes['line_items'] !== $old_hashes['line_items'] ) {
|
||||
$order->remove_order_items( 'line_item' );
|
||||
WC()->checkout->create_order_line_items( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $force || $new_hashes['shipping'] !== $old_hashes['shipping'] ) {
|
||||
$order->remove_order_items( 'shipping' );
|
||||
WC()->checkout->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping()->get_packages() );
|
||||
}
|
||||
|
||||
if ( $force || $new_hashes['coupons'] !== $old_hashes['coupons'] ) {
|
||||
$order->remove_order_items( 'coupon' );
|
||||
WC()->checkout->create_order_coupon_lines( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $force || $new_hashes['fees'] !== $old_hashes['fees'] ) {
|
||||
$order->remove_order_items( 'fee' );
|
||||
WC()->checkout->create_order_fee_lines( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $force || $new_hashes['taxes'] !== $old_hashes['taxes'] ) {
|
||||
$order->remove_order_items( 'tax' );
|
||||
WC()->checkout->create_order_tax_lines( $order, WC()->cart );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set props from API request.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
*/
|
||||
protected function set_props_from_request( \WC_Order $order, \WP_REST_Request $request ) {
|
||||
$schema = $this->get_item_schema();
|
||||
|
||||
if ( isset( $request['billing_address'] ) ) {
|
||||
$allowed_billing_values = array_intersect_key( $request['billing_address'], $schema['properties']['billing_address']['properties'] );
|
||||
foreach ( $allowed_billing_values as $key => $value ) {
|
||||
$order->{"set_billing_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['shipping_address'] ) ) {
|
||||
$allowed_shipping_values = array_intersect_key( $request['shipping_address'], $schema['properties']['shipping_address']['properties'] );
|
||||
foreach ( $allowed_shipping_values as $key => $value ) {
|
||||
$order->{"set_shipping_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['customer_note'] ) ) {
|
||||
$order->set_customer_note( $request['customer_note'] );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,8 @@ namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes;
|
|||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\OrderController;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock;
|
||||
use Automattic\WooCommerce\Blocks\Payments\PaymentResult;
|
||||
use Automattic\WooCommerce\Blocks\Payments\PaymentContext;
|
||||
|
||||
|
@ -43,63 +45,116 @@ class Checkout extends AbstractRoute {
|
|||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'args' => [
|
||||
'order_id' => [
|
||||
'description' => __( 'The order ID being processed.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'number',
|
||||
],
|
||||
'order_key' => [
|
||||
'description' => __( 'The order key; used to validate the order is valid.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'payment_method' => [
|
||||
'description' => __( 'The ID of the payment method being used to process the payment.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'payment_data' => [
|
||||
'description' => __( 'Data needed to take payment via the chosen payment method. This is passed through to the gateway when processing the payment for the order.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'key' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
'value' => [
|
||||
'type' => 'string',
|
||||
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
|
||||
],
|
||||
],
|
||||
[
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'get_response' ),
|
||||
'args' => $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
|
||||
],
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'args' => array_merge(
|
||||
[
|
||||
'payment_data' => [
|
||||
'description' => __( 'Data to pass through to the payment method when processing payment.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'key' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
'value' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
$this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE )
|
||||
),
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a given order and generate a response for the endpoint.
|
||||
* Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response.
|
||||
*
|
||||
* @throws RouteException On error.
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
protected function get_route_response( \WP_REST_Request $request ) {
|
||||
$order_object = $this->create_or_update_draft_order();
|
||||
|
||||
return $this->prepare_item_for_response(
|
||||
(object) [
|
||||
'order' => $order_object,
|
||||
'payment_result' => new PaymentResult(),
|
||||
],
|
||||
$request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the order.
|
||||
*
|
||||
* @throws RouteException On error.
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
protected function get_route_update_response( \WP_REST_Request $request ) {
|
||||
$order_object = $this->create_or_update_draft_order();
|
||||
|
||||
$this->update_order_from_request( $order_object, $request );
|
||||
|
||||
return $this->prepare_item_for_response(
|
||||
(object) [
|
||||
'order' => $order_object,
|
||||
'payment_result' => new PaymentResult(),
|
||||
],
|
||||
$request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update and process payment for the order.
|
||||
*
|
||||
* @throws RouteException On error.
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
$order = $this->get_request_order_object( $request );
|
||||
$order_controller = new OrderController();
|
||||
$order_object = $this->get_draft_order_object( $this->get_draft_order_id() );
|
||||
|
||||
$this->update_order_before_payment( $order, $request );
|
||||
if ( ! $order_object ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_checkout_invalid_order',
|
||||
__( 'This session has no orders pending payment.', 'woo-gutenberg-products-block' ),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $order->needs_payment() ) {
|
||||
$payment_result = $this->process_without_payment( $order, $request );
|
||||
$this->update_order_from_request( $order_object, $request );
|
||||
$order_controller->sync_customer_data_with_order( $order_object );
|
||||
|
||||
if ( ! $order_object->needs_payment() ) {
|
||||
$payment_result = $this->process_without_payment( $order_object, $request );
|
||||
} else {
|
||||
$payment_result = $this->process_payment( $order, $request );
|
||||
$payment_result = $this->process_payment( $order_object, $request );
|
||||
}
|
||||
|
||||
$response = $this->prepare_item_for_response(
|
||||
[
|
||||
'order_id' => $order->get_id(),
|
||||
(object) [
|
||||
'order' => wc_get_order( $order_object ),
|
||||
'payment_result' => $payment_result,
|
||||
],
|
||||
$request
|
||||
|
@ -123,6 +178,111 @@ class Checkout extends AbstractRoute {
|
|||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets draft order data from the customer session.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_draft_order_id() {
|
||||
return WC()->session->get( 'store_api_draft_order', 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates draft order data in the customer session.
|
||||
*
|
||||
* @param integer $order_id Draft order ID.
|
||||
*/
|
||||
protected function set_draft_order_id( $order_id ) {
|
||||
WC()->session->set( 'store_api_draft_order', $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an order object, either using a current draft order, or returning a new one.
|
||||
*
|
||||
* @param integer $order_id Draft order ID.
|
||||
* @return \WC_Order|boolean Either the draft order, or false if one has not yet been created.
|
||||
*/
|
||||
protected function get_draft_order_object( $order_id ) {
|
||||
$draft_order_object = $order_id ? wc_get_order( $order_id ) : false;
|
||||
|
||||
if ( ! $draft_order_object ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Draft orders are okay.
|
||||
if ( $draft_order_object->has_status( 'checkout-draft' ) ) {
|
||||
return $draft_order_object;
|
||||
}
|
||||
|
||||
// Pending and failed orders can be retried if the cart hasn't changed.
|
||||
if ( $draft_order_object->needs_payment() && $draft_order_object->has_cart_hash( WC()->cart->get_cart_hash() ) ) {
|
||||
return $draft_order_object;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a draft order based on the cart.
|
||||
*
|
||||
* @return \WC_Order Order object.
|
||||
*/
|
||||
protected function create_or_update_draft_order() {
|
||||
$order_controller = new OrderController();
|
||||
$reserve_stock = \class_exists( '\Automattic\WooCommerce\Checkout\Helpers\ReserveStock' ) ? new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() : new ReserveStock();
|
||||
$order_object = $this->get_draft_order_object( $this->get_draft_order_id() );
|
||||
$created = false;
|
||||
|
||||
if ( ! $order_object ) {
|
||||
$order_object = $order_controller->create_order_from_cart();
|
||||
$created = true;
|
||||
} else {
|
||||
$order_controller->update_order_from_cart( $order_object );
|
||||
}
|
||||
|
||||
// Store order ID to session.
|
||||
$this->set_draft_order_id( $order_object->get_id() );
|
||||
|
||||
// Try to reserve stock for 10 mins, if available.
|
||||
$reserve_stock->reserve_stock_for_order( $order_object, 10 );
|
||||
|
||||
return $order_object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an order using the posted values from the request.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
*/
|
||||
protected function update_order_from_request( \WC_Order $order, \WP_REST_Request $request ) {
|
||||
$schema = $this->get_item_schema();
|
||||
|
||||
if ( isset( $request['billing_address'] ) ) {
|
||||
$allowed_billing_values = array_intersect_key( $request['billing_address'], $schema['properties']['billing_address']['properties'] );
|
||||
foreach ( $allowed_billing_values as $key => $value ) {
|
||||
$order->{"set_billing_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['shipping_address'] ) ) {
|
||||
$allowed_shipping_values = array_intersect_key( $request['shipping_address'], $schema['properties']['shipping_address']['properties'] );
|
||||
foreach ( $allowed_shipping_values as $key => $value ) {
|
||||
$order->{"set_shipping_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['customer_note'] ) ) {
|
||||
$order->set_customer_note( $request['customer_note'] );
|
||||
}
|
||||
|
||||
if ( isset( $request['payment_method'] ) ) {
|
||||
$order->set_payment_method( $this->get_request_payment_method( $request ) );
|
||||
}
|
||||
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* For orders which do not require payment, just update status.
|
||||
*
|
||||
|
@ -148,6 +308,8 @@ class Checkout extends AbstractRoute {
|
|||
$context = new PaymentContext();
|
||||
$result = new PaymentResult();
|
||||
|
||||
$order->update_status( 'pending' );
|
||||
|
||||
$context->set_order( $order );
|
||||
$context->set_payment_method( $this->get_request_payment_method_id( $request ) );
|
||||
$context->set_payment_data( $this->get_request_payment_data( $request ) );
|
||||
|
@ -164,7 +326,7 @@ class Checkout extends AbstractRoute {
|
|||
* @param PaymentContext $context Holds context for the payment, including order ID and payment method.
|
||||
* @param PaymentResult $result Result object for the transaction.
|
||||
*/
|
||||
do_action( 'woocommerce_rest_checkout_process_payment_with_context', $context, $result );
|
||||
do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$result ] );
|
||||
|
||||
if ( ! $result instanceof PaymentResult ) {
|
||||
throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woo-gutenberg-products-block' ), 500 );
|
||||
|
@ -176,93 +338,6 @@ class Checkout extends AbstractRoute {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the order object before processing payment.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
*/
|
||||
protected function update_order_before_payment( \WC_Order $order, \WP_REST_Request $request ) {
|
||||
$this->update_customer_data_from_order( $order );
|
||||
|
||||
$order->set_payment_method( $this->get_request_payment_method( $request ) );
|
||||
$order->set_status( 'pending' );
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies order data to customer data, so values persist for future checkouts.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
protected function update_customer_data_from_order( \WC_Order $order ) {
|
||||
if ( $order->get_customer_id() ) {
|
||||
$customer = new \WC_Customer( $order->get_customer_id() );
|
||||
$customer->set_props(
|
||||
[
|
||||
'billing_first_name' => $order->get_billing_first_name(),
|
||||
'billing_last_name' => $order->get_billing_last_name(),
|
||||
'billing_company' => $order->get_billing_company(),
|
||||
'billing_address_1' => $order->get_billing_address_1(),
|
||||
'billing_address_2' => $order->get_billing_address_2(),
|
||||
'billing_city' => $order->get_billing_city(),
|
||||
'billing_state' => $order->get_billing_state(),
|
||||
'billing_postcode' => $order->get_billing_postcode(),
|
||||
'billing_country' => $order->get_billing_country(),
|
||||
'billing_email' => $order->get_billing_email(),
|
||||
'billing_phone' => $order->get_billing_phone(),
|
||||
'shipping_address_1' => $order->get_shipping_address_1(),
|
||||
'shipping_address_2' => $order->get_shipping_address_2(),
|
||||
'shipping_city' => $order->get_shipping_city(),
|
||||
'shipping_state' => $order->get_shipping_state(),
|
||||
'shipping_postcode' => $order->get_shipping_postcode(),
|
||||
'shipping_country' => $order->get_shipping_country(),
|
||||
]
|
||||
);
|
||||
$customer->save();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the order object for the request, or throws an exception if invalid.
|
||||
*
|
||||
* @throws RouteException On error.
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WC_Order
|
||||
*/
|
||||
protected function get_request_order_object( \WP_REST_Request $request ) {
|
||||
$order_id = absint( $request['order_id'] );
|
||||
|
||||
if ( ! $order_id ) {
|
||||
throw new RouteException( 'woocommerce_rest_checkout_missing_order_id', __( 'An order ID is required.', 'woo-gutenberg-products-block' ), 404 );
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
$order_key = wp_unslash( $request['order_key'] );
|
||||
$order_key_is_valid = $order && hash_equals( $order->get_order_key(), $order_key );
|
||||
|
||||
if ( ! $order_key_is_valid ) {
|
||||
throw new RouteException( 'woocommerce_rest_checkout_invalid_order', __( 'Invalid order. Please provide a valid order ID and key.', 'woo-gutenberg-products-block' ), 400 );
|
||||
}
|
||||
|
||||
$statuses_for_payment = array_unique( apply_filters( 'woocommerce_valid_order_statuses_for_payment', [ 'checkout-draft', 'pending', 'failed' ] ) );
|
||||
|
||||
if ( ! $order->has_status( $statuses_for_payment ) ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_checkout_invalid_order',
|
||||
sprintf(
|
||||
// Translators: %1$s list of order stati. %2$s Current order status.
|
||||
__( 'Only orders with status %1$s can be paid for. This order is %2$s.', 'woo-gutenberg-products-block' ),
|
||||
'`' . implode( '`, `', $statuses_for_payment ) . '`',
|
||||
$order->get_status()
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the chosen payment method ID from the request.
|
||||
*
|
||||
|
@ -274,6 +349,14 @@ class Checkout extends AbstractRoute {
|
|||
$payment_method = wc_clean( wp_unslash( $request['payment_method'] ) );
|
||||
$valid_methods = WC()->payment_gateways->get_payment_gateway_ids();
|
||||
|
||||
if ( empty( $payment_method ) ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_checkout_missing_payment_method',
|
||||
__( 'No payment method provided.', 'woo-gutenberg-products-block' ),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! in_array( $payment_method, $valid_methods, true ) ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_checkout_invalid_payment_method',
|
||||
|
|
|
@ -35,7 +35,6 @@ class RoutesController {
|
|||
new Routes\CartApplyCoupon( $schemas['cart'] ),
|
||||
new Routes\CartCoupons( $schemas['coupon'] ),
|
||||
new Routes\CartCouponsByCode( $schemas['coupon'] ),
|
||||
new Routes\CartCreateOrder( $schemas['order'] ),
|
||||
new Routes\CartItems( $schemas['cart-item'] ),
|
||||
new Routes\CartItemsByKey( $schemas['cart-item'] ),
|
||||
new Routes\CartRemoveCoupon( $schemas['cart'] ),
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
/**
|
||||
* Billing Address Schema.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\Routes;
|
||||
|
||||
/**
|
||||
* BillingAddressSchema class.
|
||||
*
|
||||
* Provides a generic billing address schema for composition in other schemas.
|
||||
*/
|
||||
class BillingAddressSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'billing_address';
|
||||
|
||||
/**
|
||||
* Term properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
return [
|
||||
'first_name' => [
|
||||
'description' => __( 'First name', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'last_name' => [
|
||||
'description' => __( 'Last name', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'company' => [
|
||||
'description' => __( 'Company', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'address_1' => [
|
||||
'description' => __( 'Address', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'address_2' => [
|
||||
'description' => __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'city' => [
|
||||
'description' => __( 'City', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'state' => [
|
||||
'description' => __( 'State/County code, or name of the state, county, province, or district.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'postcode' => [
|
||||
'description' => __( 'Postal code', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'country' => [
|
||||
'description' => __( 'Country/Region code in ISO 3166-1 alpha-2 format.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'email' => [
|
||||
'description' => __( 'Email', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'format' => 'email',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'phone' => [
|
||||
'description' => __( 'Phone', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a term object into an object suitable for the response.
|
||||
*
|
||||
* @param \WC_Order|\WC_Customer $address An object with billing address.
|
||||
*
|
||||
* @throws RouteException When the invalid object types are provided.
|
||||
* @return stdClass
|
||||
*/
|
||||
public function get_item_response( $address ) {
|
||||
if ( ( $address instanceof \WC_Customer || $address instanceof \WC_Order ) ) {
|
||||
return (object) $this->prepare_html_response(
|
||||
[
|
||||
'first_name' => $address->get_billing_first_name(),
|
||||
'last_name' => $address->get_billing_last_name(),
|
||||
'company' => $address->get_billing_company(),
|
||||
'address_1' => $address->get_billing_address_1(),
|
||||
'address_2' => $address->get_billing_address_2(),
|
||||
'city' => $address->get_billing_city(),
|
||||
'state' => $address->get_billing_state(),
|
||||
'postcode' => $address->get_billing_postcode(),
|
||||
'country' => $address->get_billing_country(),
|
||||
'email' => $address->get_billing_email(),
|
||||
'phone' => $address->get_billing_phone(),
|
||||
]
|
||||
);
|
||||
}
|
||||
throw new RouteException(
|
||||
'invalid_object_type',
|
||||
sprintf(
|
||||
/* translators: Placeholders are class and method names */
|
||||
__( '%1$s requires an instance of %2$s or %3$s for the address', 'woo-gutenberg-products-block' ),
|
||||
'BillingAddressSchema::get_item_response',
|
||||
'WC_Customer',
|
||||
'WC_Order'
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
|
@ -396,7 +396,7 @@ class CartItemSchema extends AbstractSchema {
|
|||
return null;
|
||||
}
|
||||
|
||||
$draft_order = WC()->session->get( 'store_api_draft_order' );
|
||||
$draft_order = WC()->session->get( 'store_api_draft_order', 0 );
|
||||
|
||||
// @todo Remove once min support for WC reaches 4.0.0.
|
||||
if ( \class_exists( '\Automattic\WooCommerce\Checkout\Helpers\ReserveStock' ) ) {
|
||||
|
@ -405,7 +405,7 @@ class CartItemSchema extends AbstractSchema {
|
|||
$reserve_stock = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock();
|
||||
}
|
||||
|
||||
$reserved_stock = $reserve_stock->get_reserved_stock( $product, isset( $draft_order['id'] ) ? $draft_order['id'] : 0 );
|
||||
$reserved_stock = $reserve_stock->get_reserved_stock( $product, $draft_order );
|
||||
return $product->get_stock_quantity() - $reserved_stock;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,12 +31,6 @@ class CartSchema extends AbstractSchema {
|
|||
*/
|
||||
public function get_properties() {
|
||||
return [
|
||||
'order_id' => [
|
||||
'description' => __( 'The draft order ID associated with this cart if one has been created. 0 if no draft exists.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'integer',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'coupons' => [
|
||||
'description' => __( 'List of applied cart coupons.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
|
@ -207,7 +201,6 @@ class CartSchema extends AbstractSchema {
|
|||
$context = 'edit';
|
||||
|
||||
return [
|
||||
'order_id' => $this->get_order_id(),
|
||||
'coupons' => array_values( array_map( [ $cart_coupon_schema, 'get_item_response' ], array_filter( $cart->get_applied_coupons() ) ) ),
|
||||
'shipping_rates' => array_values( array_map( [ $shipping_rate_schema, 'get_item_response' ], $controller->get_shipping_packages() ) ),
|
||||
'shipping_address' => $shipping_address_schema,
|
||||
|
@ -236,23 +229,6 @@ class CartSchema extends AbstractSchema {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a draft order ID from the session for current cart.
|
||||
*
|
||||
* @return int Draft order ID, or 0 if there isn't one yet.
|
||||
*/
|
||||
protected function get_order_id() {
|
||||
$draft_order_session = WC()->session->get( 'store_api_draft_order' );
|
||||
$draft_order_id = isset( $draft_order_session['id'] ) ? absint( $draft_order_session['id'] ) : 0;
|
||||
$draft_order = $draft_order_id ? wc_get_order( $draft_order_id ) : false;
|
||||
|
||||
if ( $draft_order && $draft_order->has_status( 'checkout-draft' ) && 'store-api' === $draft_order->get_created_via() ) {
|
||||
return $draft_order_id;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tax lines from the cart and format to match schema.
|
||||
*
|
||||
|
|
|
@ -29,33 +29,79 @@ class CheckoutSchema extends AbstractSchema {
|
|||
*/
|
||||
public function get_properties() {
|
||||
return [
|
||||
'payment_status' => [
|
||||
'description' => __( 'Status of the payment returned by the gateway. One of success, pending, failure, error.', 'woo-gutenberg-products-block' ),
|
||||
'order_id' => [
|
||||
'description' => __( 'The order ID to process during checkout.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'integer',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'status' => [
|
||||
'description' => __( 'Order status. Payment providers will update this value after payment.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'payment_details' => [
|
||||
'description' => __( 'An array of data being returned from the payment gateway.', 'woo-gutenberg-products-block' ),
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'key' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
'value' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'order' => [
|
||||
'description' => __( 'The order that was processed.', 'woo-gutenberg-products-block' ),
|
||||
'order_key' => [
|
||||
'description' => __( 'Order key used to check validity or protect access to certain order data.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'customer_note' => [
|
||||
'description' => __( 'Note added to the order by the customer during checkout.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'billing_address' => [
|
||||
'description' => __( 'Billing address.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'properties' => ( new BillingAddressSchema() )->get_properties(),
|
||||
],
|
||||
'shipping_address' => [
|
||||
'description' => __( 'Shipping address.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'properties' => ( new ShippingAddressSchema() )->get_properties(),
|
||||
],
|
||||
'payment_method' => [
|
||||
'description' => __( 'The ID of the payment method being used to process the payment.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
],
|
||||
'payment_result' => [
|
||||
'description' => __( 'Result of payment processing, or false if not yet processed.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
'properties' => $this->force_schema_readonly( ( new OrderSchema() )->get_properties() ),
|
||||
'properties' => [
|
||||
'payment_status' => [
|
||||
'description' => __( 'Status of the payment returned by the gateway. One of success, pending, failure, error.', 'woo-gutenberg-products-block' ),
|
||||
'readonly' => true,
|
||||
'type' => 'string',
|
||||
],
|
||||
'payment_details' => [
|
||||
'description' => __( 'An array of data being returned from the payment gateway.', 'woo-gutenberg-products-block' ),
|
||||
'readonly' => true,
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'key' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
'value' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'redirect_url' => [
|
||||
'description' => __( 'A URL to redirect the customer after checkout. This could be, for example, a link to the payment processors website.', 'woo-gutenberg-products-block' ),
|
||||
'readonly' => true,
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -63,23 +109,34 @@ class CheckoutSchema extends AbstractSchema {
|
|||
/**
|
||||
* Return the response for checkout.
|
||||
*
|
||||
* @throws Exception On invalid response.
|
||||
*
|
||||
* @param array $item Results from checkout action.
|
||||
* @param object $item Results from checkout action.
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $item ) {
|
||||
$order = wc_get_order( absint( $item['order_id'] ) );
|
||||
$payment_result = $item['payment_result'] instanceof PaymentResult ? $item['payment_result'] : false;
|
||||
|
||||
if ( ! $order || ! $payment_result ) {
|
||||
throw new Exception( 'Invalid response.' );
|
||||
}
|
||||
return $this->get_checkout_response( $item->order, $item->payment_result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkout response based on the current order and any payments.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param PaymentResult $payment_result Payment result object.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_checkout_response( \WC_Order $order, PaymentResult $payment_result = null ) {
|
||||
return [
|
||||
'payment_status' => $payment_result->status,
|
||||
'payment_details' => $payment_result->payment_details,
|
||||
'order' => ( new OrderSchema() )->get_item_response( $order ),
|
||||
'order_id' => $order->get_id(),
|
||||
'status' => $order->get_status(),
|
||||
'order_key' => $order->get_order_key(),
|
||||
'customer_note' => $order->get_customer_note(),
|
||||
'billing_address' => ( new BillingAddressSchema() )->get_item_response( $order ),
|
||||
'shipping_address' => ( new ShippingAddressSchema() )->get_item_response( $order ),
|
||||
'payment_method' => $order->get_payment_method(),
|
||||
'payment_result' => [
|
||||
'payment_status' => $payment_result->status,
|
||||
'payment_details' => $payment_result->payment_details,
|
||||
'redirect_url' => $payment_result->redirect_url,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ class ShippingAddressSchema extends AbstractSchema {
|
|||
sprintf(
|
||||
/* translators: Placeholders are class and method names */
|
||||
__( '%1$s requires an instance of %2$s or %3$s for the address', 'woo-gutenberg-products-block' ),
|
||||
'ShippingAddress::get_item_response',
|
||||
'ShippingAddressSchema::get_item_response',
|
||||
'WC_Customer',
|
||||
'WC_Order'
|
||||
),
|
||||
|
|
|
@ -184,6 +184,21 @@ class CartController {
|
|||
return $callback ? array_filter( wc()->cart->get_cart(), $callback ) : array_filter( wc()->cart->get_cart() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashes for items in the current cart. Useful for tracking changes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_cart_hashes() {
|
||||
return [
|
||||
'line_items' => WC()->cart->get_cart_hash(),
|
||||
'shipping' => md5( wp_json_encode( WC()->cart->shipping_methods ) ),
|
||||
'fees' => md5( wp_json_encode( WC()->cart->get_fees() ) ),
|
||||
'coupons' => md5( wp_json_encode( WC()->cart->get_applied_coupons() ) ),
|
||||
'taxes' => md5( wp_json_encode( WC()->cart->get_taxes() ) ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty cart contents.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
/**
|
||||
* Helper class which creates and syncs orders with the cart.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes\RouteException;
|
||||
|
||||
/**
|
||||
* OrderController class.
|
||||
*/
|
||||
class OrderController {
|
||||
|
||||
/**
|
||||
* Create order and set props based on global settings.
|
||||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*
|
||||
* @return \WC_Order A new order object.
|
||||
*/
|
||||
public function create_order_from_cart() {
|
||||
if ( WC()->cart->is_empty() ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_empty',
|
||||
__( 'Cannot create order from empty cart.', 'woo-gutenberg-products-block' ),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
add_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
|
||||
|
||||
$order = new \WC_Order();
|
||||
$order->set_status( 'checkout-draft' );
|
||||
$order->set_created_via( 'store-api' );
|
||||
$this->update_order_from_cart( $order );
|
||||
|
||||
remove_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an order using data from the current cart.
|
||||
*
|
||||
* @param \WC_Order $order The order object to update.
|
||||
*/
|
||||
public function update_order_from_cart( \WC_Order $order ) {
|
||||
$this->update_line_items_from_cart( $order );
|
||||
$this->update_addresses_from_cart( $order );
|
||||
$order->set_currency( get_woocommerce_currency() );
|
||||
$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
|
||||
$order->set_customer_id( get_current_user_id() );
|
||||
$order->set_customer_ip_address( \WC_Geolocation::get_ip_address() );
|
||||
$order->set_customer_user_agent( wc_get_user_agent() );
|
||||
$order->update_meta_data( 'is_vat_exempt', WC()->cart->get_customer()->get_is_vat_exempt() ? 'yes' : 'no' );
|
||||
$order->calculate_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies order data to customer object (not the session), so values persist for future checkouts.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
public function sync_customer_data_with_order( \WC_Order $order ) {
|
||||
if ( $order->get_customer_id() ) {
|
||||
$customer = new \WC_Customer( $order->get_customer_id() );
|
||||
$customer->set_props(
|
||||
[
|
||||
'billing_first_name' => $order->get_billing_first_name(),
|
||||
'billing_last_name' => $order->get_billing_last_name(),
|
||||
'billing_company' => $order->get_billing_company(),
|
||||
'billing_address_1' => $order->get_billing_address_1(),
|
||||
'billing_address_2' => $order->get_billing_address_2(),
|
||||
'billing_city' => $order->get_billing_city(),
|
||||
'billing_state' => $order->get_billing_state(),
|
||||
'billing_postcode' => $order->get_billing_postcode(),
|
||||
'billing_country' => $order->get_billing_country(),
|
||||
'billing_email' => $order->get_billing_email(),
|
||||
'billing_phone' => $order->get_billing_phone(),
|
||||
'shipping_address_1' => $order->get_shipping_address_1(),
|
||||
'shipping_address_2' => $order->get_shipping_address_2(),
|
||||
'shipping_city' => $order->get_shipping_city(),
|
||||
'shipping_state' => $order->get_shipping_state(),
|
||||
'shipping_postcode' => $order->get_shipping_postcode(),
|
||||
'shipping_country' => $order->get_shipping_country(),
|
||||
]
|
||||
);
|
||||
$customer->save();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes default order status to draft for orders created via this API.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function default_order_status() {
|
||||
return 'checkout-draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order line items.
|
||||
*
|
||||
* @internal Knowing if items changed between the order and cart can be complex. Line items are ok because there is a
|
||||
* hash, but no hash exists for other line item types. Having a normalized set of data between cart and order, or
|
||||
* additional hashes, would be useful in the future and to help refactor this code. In the meantime, we're relying
|
||||
* on custom hashes in $this->draft_order to track if things changed.
|
||||
*
|
||||
* @param \WC_Order $order The order object to update.
|
||||
*/
|
||||
protected function update_line_items_from_cart( \WC_Order $order ) {
|
||||
$cart_controller = new CartController();
|
||||
$cart = $cart_controller->get_cart_instance();
|
||||
$cart_hashes = $cart_controller->get_cart_hashes();
|
||||
|
||||
if ( $order->get_cart_hash() !== $cart_hashes['line_items'] ) {
|
||||
$order->remove_order_items( 'line_item' );
|
||||
$order->set_cart_hash( $cart_hashes['line_items'] );
|
||||
WC()->checkout->create_order_line_items( $order, $cart );
|
||||
}
|
||||
|
||||
if ( $order->get_meta_data( 'shipping_hash' ) !== $cart_hashes['shipping'] ) {
|
||||
$order->remove_order_items( 'shipping' );
|
||||
$order->update_meta_data( 'shipping_hash', $cart_hashes['shipping'] );
|
||||
WC()->checkout->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping()->get_packages() );
|
||||
}
|
||||
|
||||
if ( $order->get_meta_data( 'coupons_hash' ) !== $cart_hashes['coupons'] ) {
|
||||
$order->remove_order_items( 'coupon' );
|
||||
$order->update_meta_data( 'coupons_hash', $cart_hashes['coupons'] );
|
||||
WC()->checkout->create_order_coupon_lines( $order, $cart );
|
||||
}
|
||||
|
||||
if ( $order->get_meta_data( 'fees_hash' ) !== $cart_hashes['fees'] ) {
|
||||
$order->remove_order_items( 'fee' );
|
||||
$order->update_meta_data( 'fees_hash', $cart_hashes['fees'] );
|
||||
WC()->checkout->create_order_fee_lines( $order, $cart );
|
||||
}
|
||||
|
||||
if ( $order->get_meta_data( 'taxes_hash' ) !== $cart_hashes['taxes'] ) {
|
||||
$order->remove_order_items( 'tax' );
|
||||
$order->update_meta_data( 'taxes_hash', $cart_hashes['taxes'] );
|
||||
WC()->checkout->create_order_tax_lines( $order, $cart );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update address data from cart and/or customer session data.
|
||||
*
|
||||
* @param \WC_Order $order The order object to update.
|
||||
*/
|
||||
protected function update_addresses_from_cart( \WC_Order $order ) {
|
||||
$order->set_props( WC()->customer->get_billing() );
|
||||
$order->set_props( WC()->customer->get_shipping() );
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Create Order Tests.
|
||||
*
|
||||
* @package WooCommerce\Blocks\Tests
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Tests\RestApi\StoreApi\Controllers;
|
||||
|
||||
use \WP_REST_Request;
|
||||
use \WC_REST_Unit_Test_Case as TestCase;
|
||||
use \WC_Helper_Product as ProductHelper;
|
||||
use \WC_Helper_Order as OrderHelper;
|
||||
use \WC_Helper_Coupon as CouponHelper;
|
||||
use Automattic\WooCommerce\Blocks\Tests\Helpers\ValidateSchema;
|
||||
|
||||
/**
|
||||
* CartCreateOrder tests.
|
||||
*/
|
||||
class CartCreateOrder extends TestCase {
|
||||
/**
|
||||
* Setup test data. Called before every test.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
wp_set_current_user( 0 );
|
||||
|
||||
$this->products = [];
|
||||
|
||||
// Create some test products.
|
||||
$this->products[0] = ProductHelper::create_simple_product( false );
|
||||
$this->products[0]->set_weight( 10 );
|
||||
$this->products[0]->set_regular_price( 10 );
|
||||
$this->products[0]->save();
|
||||
|
||||
$this->products[1] = ProductHelper::create_simple_product( false );
|
||||
$this->products[1]->set_weight( 10 );
|
||||
$this->products[1]->set_regular_price( 10 );
|
||||
$this->products[1]->save();
|
||||
|
||||
wc_empty_cart();
|
||||
|
||||
$this->keys = [];
|
||||
$this->keys[] = wc()->cart->add_to_cart( $this->products[0]->get_id(), 2 );
|
||||
$this->keys[] = wc()->cart->add_to_cart( $this->products[1]->get_id(), 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test route registration.
|
||||
*/
|
||||
public function test_register_routes() {
|
||||
$routes = $this->server->get_routes();
|
||||
$this->assertArrayHasKey( '/wc/store/cart/create-order', $routes );
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/store/cart/create-order' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$this->assertEquals( 404, $response->get_status() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test order creation from cart data.
|
||||
*/
|
||||
public function test_create_item() {
|
||||
$request = new WP_REST_Request( 'POST', '/wc/store/cart/create-order' );
|
||||
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
|
||||
$request->set_param(
|
||||
'billing_address',
|
||||
[
|
||||
'first_name' => 'Margaret',
|
||||
'last_name' => 'Thatchcroft',
|
||||
'address_1' => '123 South Street',
|
||||
'address_2' => 'Apt 1',
|
||||
'city' => 'Philadelphia',
|
||||
'state' => 'PA',
|
||||
'postcode' => '19123',
|
||||
'country' => 'US',
|
||||
'email' => 'test@test.com',
|
||||
'phone' => '',
|
||||
]
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 201, $response->get_status() );
|
||||
|
||||
$this->assertArrayHasKey( 'id', $data );
|
||||
$this->assertArrayHasKey( 'number', $data );
|
||||
$this->assertArrayHasKey( 'status', $data );
|
||||
$this->assertArrayHasKey( 'order_key', $data );
|
||||
$this->assertArrayHasKey( 'created_via', $data );
|
||||
$this->assertArrayHasKey( 'prices_include_tax', $data );
|
||||
$this->assertArrayHasKey( 'events', $data );
|
||||
$this->assertArrayHasKey( 'customer', $data );
|
||||
$this->assertArrayHasKey( 'billing_address', $data );
|
||||
$this->assertArrayHasKey( 'shipping_address', $data );
|
||||
$this->assertArrayHasKey( 'customer_note', $data );
|
||||
$this->assertArrayHasKey( 'items', $data );
|
||||
$this->assertArrayHasKey( 'totals', $data );
|
||||
|
||||
$this->assertEquals( 'Margaret', $data['billing_address']->first_name );
|
||||
$this->assertEquals( 'Thatchcroft', $data['billing_address']->last_name );
|
||||
$this->assertEquals( '123 South Street', $data['billing_address']->address_1 );
|
||||
$this->assertEquals( 'Apt 1', $data['billing_address']->address_2 );
|
||||
$this->assertEquals( 'Philadelphia', $data['billing_address']->city );
|
||||
$this->assertEquals( 'PA', $data['billing_address']->state );
|
||||
$this->assertEquals( '19123', $data['billing_address']->postcode );
|
||||
$this->assertEquals( 'US', $data['billing_address']->country );
|
||||
$this->assertEquals( 'test@test.com', $data['billing_address']->email );
|
||||
$this->assertEquals( '', $data['billing_address']->phone );
|
||||
|
||||
$this->assertEquals( 'checkout-draft', $data['status'] );
|
||||
$this->assertEquals( 2, count( $data['items'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test conversion of cart item to rest response.
|
||||
*/
|
||||
public function test_prepare_item_for_response() {
|
||||
$schema = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas\OrderSchema();
|
||||
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes\CartCreateOrder( $schema );
|
||||
$order = OrderHelper::create_order();
|
||||
$response = $controller->prepare_item_for_response( $order, new \WP_REST_Request() );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertArrayHasKey( 'id', $data );
|
||||
$this->assertArrayHasKey( 'number', $data );
|
||||
$this->assertArrayHasKey( 'status', $data );
|
||||
$this->assertArrayHasKey( 'order_key', $data );
|
||||
$this->assertArrayHasKey( 'created_via', $data );
|
||||
$this->assertArrayHasKey( 'prices_include_tax', $data );
|
||||
$this->assertArrayHasKey( 'events', $data );
|
||||
$this->assertArrayHasKey( 'customer', $data );
|
||||
$this->assertArrayHasKey( 'billing_address', $data );
|
||||
$this->assertArrayHasKey( 'shipping_address', $data );
|
||||
$this->assertArrayHasKey( 'customer_note', $data );
|
||||
$this->assertArrayHasKey( 'items', $data );
|
||||
$this->assertArrayHasKey( 'totals', $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test schema matches responses.
|
||||
*/
|
||||
public function test_schema_matches_response() {
|
||||
$schema = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas\OrderSchema();
|
||||
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes\CartCreateOrder( $schema );
|
||||
|
||||
$order = OrderHelper::create_order();
|
||||
$coupon = CouponHelper::create_coupon();
|
||||
$order->apply_coupon( $coupon );
|
||||
|
||||
$response = $controller->prepare_item_for_response( $order, new \WP_REST_Request() );
|
||||
$schema = $controller->get_item_schema();
|
||||
$validate = new ValidateSchema( $schema );
|
||||
|
||||
$diff = $validate->get_diff_from_object( $response->get_data() );
|
||||
$this->assertEmpty( $diff, print_r( $diff, true ) );
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue