Skip to content

Commit

Permalink
fix issues with decimal product quantities
Browse files Browse the repository at this point in the history
  • Loading branch information
kilbot committed Jun 7, 2023
1 parent 9620731 commit 6b225b4
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 82 deletions.
18 changes: 7 additions & 11 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,18 @@
"require-dev": {
"automattic/vipwpcs": "2.3.3",
"dealerdirect/phpcodesniffer-composer-installer": "v0.7.2",
"friendsofphp/php-cs-fixer": "v3.14.4",
"friendsofphp/php-cs-fixer": "v3.17.0",
"phpcompatibility/phpcompatibility-wp": "2.1.4",
"sirbrillig/phpcs-variable-analysis": "v2.11.10",
"squizlabs/php_codesniffer": "3.7.1",
"woocommerce/woocommerce-sniffs": "^0.1.3",
"sirbrillig/phpcs-variable-analysis": "v2.11.16",
"squizlabs/php_codesniffer": "3.7.2",
"woocommerce/woocommerce-sniffs": "0.1.3",
"wp-coding-standards/wpcs": "2.3.0",
"wp-phpunit/wp-phpunit": "6.1.1",
"wp-phpunit/wp-phpunit": "6.2.0",
"yoast/phpunit-polyfills": "^1.0.5"
},
"require": {
"php": ">=7.2.0",
"ext-json": "*",
"firebase/php-jwt": "v6.4.0",
"php": ">=7.2",
"firebase/php-jwt": "v6.5.0",
"ramsey/uuid": "^4.2.3",
"salesforce/handlebars-php": "^3.0.1",
"vlucas/phpdotenv": "^v5.5.0"
Expand All @@ -36,9 +35,6 @@
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
},
"platform": {
"php": "7.4"
}
},
"scripts": {
Expand Down
61 changes: 5 additions & 56 deletions includes/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,10 @@ public function __construct() {
/*
* These filters allow changes to the WC REST API response
* Note: I needed to init WC API patches earlier than rest_dispatch_request for validation patch
* Note: The rest_request_before_callbacks filter needs any early priority because we may use in the loaded classes
*/
// add_filter( 'rest_pre_dispatch', array( $this, 'rest_pre_dispatch' ), 10, 3 );
add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 );
add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 5, 3 );
add_filter( 'rest_dispatch_request', array( $this, 'rest_dispatch_request' ), 10, 4 );
add_filter( 'rest_endpoints', array( $this, 'rest_endpoints' ), 99, 1 );

$this->prevent_messages();
}
Expand All @@ -78,6 +77,7 @@ private function prevent_messages() {
*/
public function rest_allowed_cors_headers( array $allow_headers ): array {
$allow_headers[] = 'X-WCPOS';
$allow_headers[] = 'X-HTTP-Method-Override';

return $allow_headers;
}
Expand Down Expand Up @@ -180,14 +180,9 @@ public function rest_pre_dispatch( $result, $server, $request ) {
/**
* Filters the response before executing any REST API callbacks.
*
* Allows plugins to perform additional validation after a
* request is initialized and matched to a registered route,
* but before it is executed.
* NOTE: route matching and authentication have run at this point.
*
* Note that this filter will not be called for requests that
* fail to authenticate or match to a registered route.
*
* @since 4.7.0
* We use this hook to determine the controller class and load our duck punches.
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
Expand Down Expand Up @@ -262,50 +257,4 @@ public function rest_dispatch_request( $dispatch_result, $request, $route, $hand

return $dispatch_result;
}

/**
* Filters the array of available REST API endpoints.
*
* @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
* to an array of callbacks for the endpoint. These take the format
* `'/path/regex' => array( $callback, $bitmask )` or
* `'/path/regex' => array( array( $callback, $bitmask ).
*
* @return array
*/
public function rest_endpoints( array $endpoints ): array {
// This is a hack to allow order creation without an email address
// @TODO - there must be a better way to this?
// @NOTE - WooCommercePOS\API\Orders is loaded after validation checks, so can't put it there
if ( isset( $endpoints['/wc/v3/orders'] ) ) {
$endpoints['/wc/v3/orders'][1]['args']['billing']['properties']['email']['format'] = '';
// add ordering by status, customer_id, payment_method and total to orders endpoint
$endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'status';
$endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'customer_id';
$endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'payment_method';
$endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'total';
}
if ( isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'] ) ) {
$endpoints['/wc/v3/orders/(?P<id>[\d]+)'][1]['args']['billing']['properties']['email']['format'] = '';
}


// add ordering by meta_value to customers endpoint
if ( isset( $endpoints['/wc/v3/customers'] ) ) {
// allow ordering by first_name, last_name, email, role, username
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'first_name';
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'last_name';
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'email';
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'role';
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'username';
}

// add ordering by stock_quantity to products endpoint
if ( isset( $endpoints['/wc/v3/products'] ) ) {
// allow ordering by meta_value
$endpoints['/wc/v3/products'][0]['args']['orderby']['enum'][] = 'stock_quantity';
}

return $endpoints;
}
}
57 changes: 57 additions & 0 deletions includes/API/Customers.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,68 @@ class Customers {
public function __construct( WP_REST_Request $request ) {
$this->request = $request;

add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 );
add_filter( 'woocommerce_rest_customer_query', array( $this, 'customer_query' ), 10, 2 );
add_filter( 'woocommerce_rest_prepare_customer', array( $this, 'customer_response' ), 10, 3 );
add_filter( 'users_where', array( $this, 'users_where' ), 10, 2 );
}

/**
* Filters the response before executing any REST API callbacks.
*
* We can use this filter to bypass data validation checks
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*/
public function rest_request_before_callbacks( $response, $handler, $request ) {
if ( is_wp_error( $response ) ) {
// Check if the error code 'rest_invalid_param' exists
if ( $response->get_error_message( 'rest_invalid_param' ) ) {
// Get the error data for 'rest_invalid_param'
$error_data = $response->get_error_data( 'rest_invalid_param' );

// Check if the invalid parameter was 'orderby'
if ( array_key_exists( 'orderby', $error_data['params'] ) ) {
// Get the 'orderby' details
$orderby_details = $error_data['details']['orderby'];

// Get the 'orderby' request
$orderby_request = $request->get_param( 'orderby' );

// Extended 'orderby' values
$orderby_extended = array(
'first_name',
'last_name',
'email',
'role',
'username',
);

// Check if 'orderby' has 'rest_not_in_enum', but is in the extended 'orderby' values
if ( $orderby_details['code'] === 'rest_not_in_enum' && in_array( $orderby_request, $orderby_extended, true ) ) {
unset( $error_data['params']['orderby'], $error_data['details']['orderby'] );
}
}

// Check if $error_data['params'] is empty
if ( empty( $error_data['params'] ) ) {
return null;
} else {
// Remove old error data and add new error data
$error_message = 'Invalid parameter(s): ' . implode( ', ', array_keys( $error_data['params'] ) ) . '.';

$response->remove( 'rest_invalid_param' );
$response->add( 'rest_invalid_param', $error_message, $error_data );
}
}
}

return $response;
}


/**
* Filter customer data returned from the REST API.
Expand Down
116 changes: 108 additions & 8 deletions includes/API/Orders.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
use WCPOS\WooCommercePOS\Logger;
use WP_REST_Request;
use WP_REST_Response;
use function in_array;
use function is_array;
use function is_callable;
use const WCPOS\WooCommercePOS\PLUGIN_NAME;
use WC_Order_Query;
use WP_Query;
Expand All @@ -33,6 +36,8 @@ public function __construct( WP_REST_Request $request ) {
$this->incoming_shop_order();
}

add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 );
add_filter( 'woocommerce_rest_shop_order_object_query', array( $this, 'order_query' ), 10, 2 );
add_filter('woocommerce_rest_pre_insert_shop_order_object', array(
$this,
'pre_insert_shop_order_object',
Expand All @@ -50,6 +55,88 @@ public function __construct( WP_REST_Request $request ) {
add_filter( 'option_woocommerce_tax_based_on', array( $this, 'tax_based_on' ), 10, 2 );
}

/**
* Filters the response before executing any REST API callbacks.
*
* We can use this filter to bypass data validation checks
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*/
public function rest_request_before_callbacks( $response, $handler, $request ) {
if ( is_wp_error( $response ) ) {
// Check if the error code 'rest_invalid_param' exists
if ( $response->get_error_message( 'rest_invalid_param' ) ) {
// Get the error data for 'rest_invalid_param'
$error_data = $response->get_error_data( 'rest_invalid_param' );

// Check if the invalid parameter was 'line_items'
if ( array_key_exists( 'line_items', $error_data['params'] ) ) {
// Get the 'line_items' details
$line_items_details = $error_data['details']['line_items'];

// Check if 'line_items[X][quantity]' has 'rest_invalid_type'
// Use a regular expression to match 'line_items[X][quantity]', where X is a number
if ( $line_items_details['code'] === 'rest_invalid_type' &&
preg_match( '/^line_items\[\d+\]\[quantity\]$/', $line_items_details['data']['param'] ) ) {
if ( woocommerce_pos_get_settings( 'general', 'decimal_qty' ) ) {
unset( $error_data['params']['line_items'], $error_data['details']['line_items'] );
}
}
}

// Check if the invalid parameter was 'billing'
if ( array_key_exists( 'billing', $error_data['params'] ) ) {
// Get the 'billing' details
$billing_details = $error_data['details']['billing'];

// Check if 'billing' has 'rest_invalid_email'
if ( $billing_details['code'] === 'rest_invalid_email' ) {
unset( $error_data['params']['billing'], $error_data['details']['billing'] );
}
}

// Check if the invalid parameter was 'orderby'
if ( array_key_exists( 'orderby', $error_data['params'] ) ) {
// Get the 'orderby' details
$orderby_details = $error_data['details']['orderby'];

// Get the 'orderby' request
$orderby_request = $request->get_param( 'orderby' );

// Extended 'orderby' values
$orderby_extended = array(
'status',
'customer_id',
'payment_method',
'total',
);

// Check if 'orderby' has 'rest_not_in_enum', but is in the extended 'orderby' values
if ( $orderby_details['code'] === 'rest_not_in_enum' && in_array( $orderby_request, $orderby_extended, true ) ) {
unset( $error_data['params']['orderby'], $error_data['details']['orderby'] );
}
}

// Check if $error_data['params'] is empty
if ( empty( $error_data['params'] ) ) {
return null;
} else {
// Remove old error data and add new error data
$error_message = 'Invalid parameter(s): ' . implode( ', ', array_keys( $error_data['params'] ) ) . '.';

$response->remove( 'rest_invalid_param' );
$response->add( 'rest_invalid_param', $error_message, $error_data );
}
}
}

return $response;
}



public function incoming_shop_order(): void {
$raw_data = $this->request->get_json_params();
Expand Down Expand Up @@ -97,6 +184,19 @@ public function test_email() {
return true;
}

/**
* Filter the query arguments for a request.
*
* @param array $args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*
* @return array $args Key value array of query var to query value.
*/
public function order_query( $args, WP_REST_Request $request ) {

return $args;
}

/**
* @param $order
* @param $request
Expand Down Expand Up @@ -191,18 +291,18 @@ public function rest_set_order_item( $item, $posted ): void {
$variation = wc_get_product( (int) $posted['variation_id'] );
$valid_keys = array();

if ( \is_callable( array( $variation, 'get_variation_attributes' ) ) ) {
if ( is_callable( array( $variation, 'get_variation_attributes' ) ) ) {
foreach ( $variation->get_variation_attributes() as $attribute_name => $attribute ) {
$valid_keys[] = str_replace( 'attribute_', '', $attribute_name );
}

if ( isset( $posted['meta_data'] ) && \is_array( $posted['meta_data'] ) ) {
if ( isset( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) {
foreach ( $posted['meta_data'] as $meta ) {
// fix initial item creation
if ( isset( $meta['attr_id'] ) ) {
if ( 0 == $meta['attr_id'] ) {
// not a taxonomy
if ( \in_array( strtolower( $meta['display_key'] ), $valid_keys, true ) ) {
if ( in_array( strtolower( $meta['display_key'] ), $valid_keys, true ) ) {
$item->add_meta_data( strtolower( $meta['display_key'] ), $meta['display_value'], true );
}
} else {
Expand All @@ -219,7 +319,7 @@ public function rest_set_order_item( $item, $posted ): void {
}
}
// fix subsequent overwrites
if ( wc_attribute_taxonomy_id_by_name( $meta['key'] ) || \in_array( $meta['key'], $valid_keys, true ) ) {
if ( wc_attribute_taxonomy_id_by_name( $meta['key'] ) || in_array( $meta['key'], $valid_keys, true ) ) {
$item->add_meta_data( $meta['key'], $meta['value'], true );
}
}
Expand Down Expand Up @@ -295,28 +395,28 @@ public function orderby_additions( array $clauses, WP_Query $wp_query ): array {
// add option to order by status
if ( 'status' === $wp_query->query_vars['orderby'] ) {
$clauses['join'] .= " LEFT JOIN {$wpdb->prefix}posts AS order_posts ON {$wpdb->prefix}posts.ID = order_posts.ID ";
$clauses['orderby'] = " order_posts.post_status " . $order;
$clauses['orderby'] = ' order_posts.post_status ' . $order;
}

// add option to order by customer_id
if ( 'customer_id' === $wp_query->query_vars['orderby'] ) {
$clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS customer_meta ON {$wpdb->prefix}posts.ID = customer_meta.post_id ";
$clauses['where'] .= " AND customer_meta.meta_key = '_customer_user' ";
$clauses['orderby'] = " customer_meta.meta_value " . $order;
$clauses['orderby'] = ' customer_meta.meta_value ' . $order;
}

// add option to order by payment_method
if ( 'payment_method' === $wp_query->query_vars['orderby'] ) {
$clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS payment_method_meta ON {$wpdb->prefix}posts.ID = payment_method_meta.post_id ";
$clauses['where'] .= " AND payment_method_meta.meta_key = '_payment_method' ";
$clauses['orderby'] = " payment_method_meta.meta_value " . $order;
$clauses['orderby'] = ' payment_method_meta.meta_value ' . $order;
}

// add option to order by total
if ( 'total' === $wp_query->query_vars['orderby'] ) {
$clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS total_meta ON {$wpdb->prefix}posts.ID = total_meta.post_id ";
$clauses['where'] .= " AND total_meta.meta_key = '_order_total' ";
$clauses['orderby'] = " total_meta.meta_value+0 " . $order;
$clauses['orderby'] = ' total_meta.meta_value+0 ' . $order;
}
}

Expand Down
Loading

0 comments on commit 6b225b4

Please sign in to comment.