From 901e996cc55e0f2cf31f692c4f7b6a7278df6b39 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 13 Dec 2019 15:37:11 +0000 Subject: [PATCH] Add money formatting to API responses (https://github.com/woocommerce/woocommerce-blocks/pull/1367) * Implement basic version of MoneyValue with decimal conversion * Implement MoneyValue in cart classes * Add minor unit to schema * Update tests * Add tests * Tweak minor unit description * Replace pow * Dump rounding mode and use constant values * Only return strings * prepare_money_response method * Update types back to string * Remove unnecessary parentheses * Feedback; force integer rounding mode to prevent notices --- .../StoreApi/Schemas/AbstractSchema.php | 19 ++++ .../StoreApi/Schemas/CartItemSchema.php | 66 +++++++------ .../RestApi/StoreApi/Schemas/CartSchema.php | 96 ++++++++++--------- .../php/RestApi/StoreApi/Controllers/Cart.php | 5 +- .../StoreApi/Controllers/CartItems.php | 6 +- 5 files changed, 116 insertions(+), 76 deletions(-) diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/AbstractSchema.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/AbstractSchema.php index 75d6426284e..12d4c9c6a7a 100644 --- a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/AbstractSchema.php +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/AbstractSchema.php @@ -63,4 +63,23 @@ abstract class AbstractSchema { $properties ); } + + /** + * Convert monetary values from WooCommerce to string based integers, using + * the smallest unit of a currency. + * + * @param string|float $amount Monetary amount with decimals. + * @param int $decimals Number of decimals the amount is formatted with. + * @param int $rounding_mode Defaults to the PHP_ROUND_HALF_UP constant. + * @return string The new amount. + */ + protected function prepare_money_response( $amount, $decimals = 2, $rounding_mode = PHP_ROUND_HALF_UP ) { + return (string) intval( + round( + wc_format_decimal( $amount ) * ( 10 ** $decimals ), + 0, + absint( $rounding_mode ) + ) + ); + } } diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartItemSchema.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartItemSchema.php index 77ba0674c85..aa71d3a6a6b 100644 --- a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartItemSchema.php +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartItemSchema.php @@ -33,13 +33,13 @@ class CartItemSchema extends AbstractSchema { */ protected function get_properties() { return [ - 'key' => array( + 'key' => array( 'description' => __( 'Unique identifier for the item within the cart.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'id' => array( + 'id' => array( 'description' => __( 'The cart item product or variation ID.', 'woo-gutenberg-products-block' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), @@ -49,7 +49,7 @@ class CartItemSchema extends AbstractSchema { 'validate_callback' => array( $this, 'product_id_exists' ), ), ), - 'quantity' => array( + 'quantity' => array( 'description' => __( 'Quantity of this item in the cart.', 'woo-gutenberg-products-block' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), @@ -58,26 +58,26 @@ class CartItemSchema extends AbstractSchema { 'sanitize_callback' => 'wc_stock_amount', ), ), - 'name' => array( + 'name' => array( 'description' => __( 'Product name.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'sku' => array( + 'sku' => array( 'description' => __( 'Stock keeping unit, if applicable.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'permalink' => array( + 'permalink' => array( 'description' => __( 'Product URL.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'images' => array( + 'images' => array( 'description' => __( 'List of images.', 'woo-gutenberg-products-block' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), @@ -113,25 +113,37 @@ class CartItemSchema extends AbstractSchema { ), ), ), - 'product_price' => array( + 'product_price' => array( 'description' => __( 'Current product price.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'line_subtotal' => array( - 'description' => __( 'Line price subtotal (excluding coupons and discounts).', 'woo-gutenberg-products-block' ), + 'line_subtotal' => array( + 'description' => __( 'Line price subtotal (excluding coupons and discounts). Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'line_total' => array( - 'description' => __( 'Line price total (including coupons and discounts).', 'woo-gutenberg-products-block' ), + 'line_subtotal_tax' => array( + 'description' => __( 'Line price subtotal tax. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'variation' => array( + 'line_total' => array( + 'description' => __( 'Line price total (including coupons and discounts). Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'line_total_tax' => array( + 'description' => __( 'Line price total tax. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'variation' => array( 'description' => __( 'Chosen attributes (for variations).', 'woo-gutenberg-products-block' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), @@ -172,22 +184,22 @@ class CartItemSchema extends AbstractSchema { * @return array */ public function get_item_response( $cart_item ) { - $product = $cart_item['data']; - $line_subtotal = $product->get_price() * wc_stock_amount( $cart_item['quantity'] ); - $line_total_incl_coupons = isset( $cart_item['line_total'] ) ? $cart_item['line_total'] : $line_subtotal; + $product = $cart_item['data']; return [ - 'key' => $cart_item['key'], - 'id' => $product->get_id(), - 'quantity' => wc_stock_amount( $cart_item['quantity'] ), - 'name' => $product->get_title(), - 'sku' => $product->get_sku(), - 'permalink' => $product->get_permalink(), - 'images' => ( new ProductImages() )->images_to_array( $product ), - 'product_price' => wc_format_decimal( $product->get_price() ), - 'line_total' => wc_format_decimal( $line_total_incl_coupons ), - 'line_subtotal' => wc_format_decimal( $line_subtotal ), - 'variation' => $this->format_variation_data( $cart_item['variation'], $product ), + 'key' => $cart_item['key'], + 'id' => $product->get_id(), + 'quantity' => wc_stock_amount( $cart_item['quantity'] ), + 'name' => $product->get_title(), + 'sku' => $product->get_sku(), + 'permalink' => $product->get_permalink(), + 'images' => ( new ProductImages() )->images_to_array( $product ), + 'product_price' => $this->prepare_money_response( $product->get_price(), wc_get_price_decimals() ), + 'line_subtotal' => $this->prepare_money_response( $cart_item['line_subtotal'], wc_get_price_decimals() ), + 'line_subtotal_tax' => $this->prepare_money_response( $cart_item['line_subtotal_tax'], wc_get_price_decimals() ), + 'line_total' => $this->prepare_money_response( $cart_item['line_total'], wc_get_price_decimals() ), + 'line_total_tax' => $this->prepare_money_response( $cart_item['line_tax'], wc_get_price_decimals() ), + 'variation' => $this->format_variation_data( $cart_item['variation'], $product ), ]; } diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartSchema.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartSchema.php index a2a8e124811..744caabed3d 100644 --- a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartSchema.php +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartSchema.php @@ -29,7 +29,7 @@ class CartSchema extends AbstractSchema { */ protected function get_properties() { return [ - 'currency' => [ + 'currency' => [ 'description' => __( 'Currency code (in ISO format) of the cart item prices.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'default' => get_woocommerce_currency(), @@ -37,7 +37,14 @@ class CartSchema extends AbstractSchema { 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'items' => [ + 'currency_minor_unit' => [ + 'description' => __( 'Currency minor unit (number of digits after the decimal separator) used for cart item prices.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'default' => wc_get_price_decimals(), + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'items' => [ 'description' => __( 'List of cart items.', 'woo-gutenberg-products-block' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], @@ -47,85 +54,85 @@ class CartSchema extends AbstractSchema { 'properties' => $this->force_schema_readonly( ( new CartItemSchema() )->get_properties() ), ], ], - 'items_count' => [ + 'items_count' => [ 'description' => __( 'Number of items in the cart.', 'woo-gutenberg-products-block' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'items_weight' => [ + 'items_weight' => [ 'description' => __( 'Total weight (in grams) of all products in the cart.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'needs_shipping' => [ + 'needs_shipping' => [ 'description' => __( 'True if the cart needs shipping. False for carts with only digital goods or stores with no shipping methods set-up.', 'woo-gutenberg-products-block' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_items' => [ - 'description' => __( 'Total price of items in the cart.', 'woo-gutenberg-products-block' ), + 'total_items' => [ + 'description' => __( 'Total price of items in the cart. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_items_tax' => [ - 'description' => __( 'Total tax on items in the cart.', 'woo-gutenberg-products-block' ), + 'total_items_tax' => [ + 'description' => __( 'Total tax on items in the cart. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_fees' => [ - 'description' => __( 'Total price of any applied fees.', 'woo-gutenberg-products-block' ), + 'total_fees' => [ + 'description' => __( 'Total price of any applied fees. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_fees_tax' => [ - 'description' => __( 'Total tax on fees.', 'woo-gutenberg-products-block' ), + 'total_fees_tax' => [ + 'description' => __( 'Total tax on fees. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_discount' => [ - 'description' => __( 'Total discount from applied coupons.', 'woo-gutenberg-products-block' ), + 'total_discount' => [ + 'description' => __( 'Total discount from applied coupons. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_discount_tax' => [ - 'description' => __( 'Total tax removed due to discount from applied coupons.', 'woo-gutenberg-products-block' ), + 'total_discount_tax' => [ + 'description' => __( 'Total tax removed due to discount from applied coupons. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_shipping' => [ - 'description' => __( 'Total price of shipping.', 'woo-gutenberg-products-block' ), + 'total_shipping' => [ + 'description' => __( 'Total price of shipping. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_shipping_tax' => [ - 'description' => __( 'Total tax on shipping.', 'woo-gutenberg-products-block' ), + 'total_shipping_tax' => [ + 'description' => __( 'Total tax on shipping. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_tax' => [ - 'description' => __( 'Total tax applied to items and shipping.', 'woo-gutenberg-products-block' ), + 'total_tax' => [ + 'description' => __( 'Total tax applied to items and shipping. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'total_price' => [ - 'description' => __( 'Total price the customer will pay.', 'woo-gutenberg-products-block' ), + 'total_price' => [ + 'description' => __( 'Total price the customer will pay. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'tax_lines' => [ + 'tax_lines' => [ 'description' => __( 'Lines of taxes applied to items and shipping.', 'woo-gutenberg-products-block' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], @@ -140,7 +147,7 @@ class CartSchema extends AbstractSchema { 'readonly' => true, ], 'price' => [ - 'description' => __( 'The amount of tax charged.', 'woo-gutenberg-products-block' ), + 'description' => __( 'The amount of tax charged. Amount provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, @@ -160,22 +167,23 @@ class CartSchema extends AbstractSchema { public function get_item_response( $cart ) { $cart_item_schema = new CartItemSchema(); return [ - 'currency' => get_woocommerce_currency(), - 'items' => array_values( array_map( [ $cart_item_schema, 'get_item_response' ], array_filter( $cart->get_cart() ) ) ), - 'items_count' => $cart->get_cart_contents_count(), - 'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ), - 'needs_shipping' => $cart->needs_shipping(), - 'total_items' => wc_format_decimal( $cart->get_subtotal() ), - 'total_items_tax' => wc_format_decimal( $cart->get_subtotal_tax() ), - 'total_fees' => wc_format_decimal( $cart->get_fee_total() ), - 'total_fees_tax' => wc_format_decimal( $cart->get_fee_tax() ), - 'total_discount' => wc_format_decimal( $cart->get_discount_total() ), - 'total_discount_tax' => wc_format_decimal( $cart->get_discount_tax() ), - 'total_shipping' => wc_format_decimal( $cart->get_shipping_total() ), - 'total_shipping_tax' => wc_format_decimal( $cart->get_shipping_tax() ), - 'total_tax' => wc_format_decimal( $cart->get_total_tax() ), - 'total_price' => wc_format_decimal( $cart->get_total() ), - 'tax_lines' => $this->get_tax_lines( $cart ), + 'currency' => get_woocommerce_currency(), + 'currency_minor_unit' => wc_get_price_decimals(), + 'items' => array_values( array_map( [ $cart_item_schema, 'get_item_response' ], array_filter( $cart->get_cart() ) ) ), + 'items_count' => $cart->get_cart_contents_count(), + 'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ), + 'needs_shipping' => $cart->needs_shipping(), + 'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ), + 'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), wc_get_price_decimals() ), + 'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), wc_get_price_decimals() ), + 'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), wc_get_price_decimals() ), + 'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), wc_get_price_decimals() ), + 'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ), + 'total_shipping' => $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ), + 'total_shipping_tax' => $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ), + 'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ), + 'total_price' => $this->prepare_money_response( $cart->get_total(), wc_get_price_decimals() ), + 'tax_lines' => $this->get_tax_lines( $cart ), ]; } @@ -192,7 +200,7 @@ class CartSchema extends AbstractSchema { foreach ( $cart_tax_totals as $cart_tax_total ) { $tax_lines[] = array( 'name' => $cart_tax_total->label, - 'price' => wc_format_decimal( $cart_tax_total->amount ), + 'price' => $this->prepare_money_response( $cart_tax_total->amount, wc_get_price_decimals() ), ); } diff --git a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/Cart.php b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/Cart.php index 6ec756bfb4a..cb1ed0b7eb1 100644 --- a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/Cart.php +++ b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/Cart.php @@ -62,12 +62,13 @@ class Cart extends TestCase { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 'GBP', $data['currency'] ); + $this->assertEquals( 2, $data['currency_minor_unit'] ); $this->assertEquals( 3, $data['items_count'] ); $this->assertEquals( 2, count( $data['items'] ) ); $this->assertEquals( false, $data['needs_shipping'] ); $this->assertEquals( '30', $data['items_weight'] ); - $this->assertEquals( '30.00', $data['total_items'] ); + $this->assertEquals( '3000', $data['total_items'] ); $this->assertEquals( '0', $data['total_items_tax'] ); $this->assertEquals( '0', $data['total_fees'] ); $this->assertEquals( '0', $data['total_fees_tax'] ); @@ -76,7 +77,7 @@ class Cart extends TestCase { $this->assertEquals( '0', $data['total_shipping'] ); $this->assertEquals( '0', $data['total_shipping_tax'] ); $this->assertEquals( '0', $data['total_tax'] ); - $this->assertEquals( '30.00', $data['total_price'] ); + $this->assertEquals( '3000', $data['total_price'] ); } /** diff --git a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/CartItems.php b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/CartItems.php index f1050512b5e..33d46cf4124 100644 --- a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/CartItems.php +++ b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/CartItems.php @@ -79,9 +79,9 @@ class CartItems extends TestCase { $this->assertEquals( $this->products[0]->get_sku(), $data['sku'] ); $this->assertEquals( $this->products[0]->get_permalink(), $data['permalink'] ); $this->assertEquals( 2, $data['quantity'] ); - $this->assertEquals( '10.00', $data['product_price'] ); - $this->assertEquals( '20.00', $data['line_subtotal'] ); - $this->assertEquals( '20.00', $data['line_total'] ); + $this->assertEquals( '1000', $data['product_price'] ); + $this->assertEquals( '2000', $data['line_subtotal'] ); + $this->assertEquals( '2000', $data['line_total'] ); $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items/XXX815416f775098fe977004015c6193' ); $response = $this->server->dispatch( $request );