From 57865204aadfd864a5f99c21741ae0673bef2eb9 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 22 Aug 2017 15:17:58 +0100 Subject: [PATCH 01/10] Cart fees class and legacy --- includes/class-wc-cart-fees.php | 130 +++++++++++++++++++++++ includes/class-wc-cart.php | 71 +++++-------- includes/legacy/class-wc-legacy-cart.php | 7 +- 3 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 includes/class-wc-cart-fees.php diff --git a/includes/class-wc-cart-fees.php b/includes/class-wc-cart-fees.php new file mode 100644 index 00000000000..e8ed3f46f71 --- /dev/null +++ b/includes/class-wc-cart-fees.php @@ -0,0 +1,130 @@ +cart->fees_api which will reference this class. + * + * Fees can be added/removed at any time, however, before cart total calculations fees are purged + * so we suggest using the action woocommerce_cart_calculate_fees or woocommerce_before_calculate_totals. + * + * @author Automattic + * @package WooCommerce/Classes + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * WC_Cart_Fees class. + * + * @since 3.2.0 + */ +final class WC_Cart_Fees { + + /** + * An array of fee objects. + * + * @var object[] + */ + private $fees = array(); + + /** + * Reference to cart object. + * + * @since 3.2.0 + * @var array + */ + private $cart; + + /** + * New fees are made out of these props. + * + * @var array + */ + private $default_fee_props = array( + 'id' => '', + 'name' => '', + 'tax_class' => '', + 'taxable' => false, + 'amount' => 0, + ); + + /** + * Constructor. Reference to the cart. + * + * @since 3.2.0 + * @param object $cart Cart object. + */ + public function __construct( &$cart = null ) { + $this->cart = $cart; + add_action( 'woocommerce_cart_emptied', array( $this, 'remove_all_fees' ) ); + add_action( 'woocommerce_cart_reset', array( $this, 'remove_all_fees' ) ); + } + + /** + * Add a fee. Fee IDs must be unique. + * + * @since 3.2.0 + * @param array $args Array of fee properties. + * @return object Either a fee object if added, or a WP_Error if it failed. + */ + public function add_fee( $args = array() ) { + $fee_props = (object) wp_parse_args( $args, $this->default_fee_props ); + $fee_props->name = $fee_props->name ? $fee_props->name : __( 'Fee', 'woocommerce' ); + $fee_props->tax_class = in_array( $fee_props->tax_class, WC_Tax::get_tax_classes(), true ) ? $fee_props->tax_class: ''; + $fee_props->taxable = wc_string_to_bool( $fee_props->taxable ); + $fee_props->amount = wc_format_decimal( $fee_props->amount ); + + if ( empty( $fee_props->id ) ) { + $fee_props->id = $this->generate_id( $fee_props ); + } + + if ( array_key_exists( $fee_props->id, $this->fees ) ) { + return new WP_Error( 'fee_exists', __( 'Fee has already been added.', 'woocommerce' ) ); + } + + return $this->fees[ $fee_props->id ] = $fee_props; + } + + /** + * Get fees. + * + * @return array + */ + public function get_fees() { + return array_filter( (array) $this->fees ); + } + + /** + * Remove all fees. + * + * @since 3.2.0 + */ + public function remove_all_fees() { + $this->set_fees(); + } + + /** + * Generate a unique ID for the fee being added. + * + * @param string $fee Fee name. + * @return string fee key. + */ + private function generate_id( $fee ) { + return sanitize_title( $fee ); + } + + /** + * Set fees. + * + * @param object[] $raw_fees Array of fees. + */ + private function set_fees( $raw_fees = array() ) { + $this->fees = array(); + + foreach ( $raw_fees as $raw_fee ) { + $this->add_fee( $raw_fee ); + } + } +} diff --git a/includes/class-wc-cart.php b/includes/class-wc-cart.php index 2c4038cbe06..35978af773a 100644 --- a/includes/class-wc-cart.php +++ b/includes/class-wc-cart.php @@ -44,13 +44,6 @@ class WC_Cart extends WC_Legacy_Cart { */ public $applied_coupons = array(); - /** - * An array of fees. - * - * @var array - */ - public $fees = array(); - /** * Are prices in the cart displayed inc or excl tax. * @@ -101,11 +94,19 @@ class WC_Cart extends WC_Legacy_Cart { */ protected $session; + /** + * Reference to the cart fees API class. + * + * @var WC_Cart_Fees + */ + protected $fees_api; + /** * Constructor for the cart class. Loads options and hooks in the init method. */ public function __construct() { $this->session = new WC_Cart_Session( $this ); + $this->fees_api = new WC_Cart_Fees( $this ); $this->tax_display_cart = get_option( 'woocommerce_tax_display_cart' ); add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 ); @@ -609,7 +610,6 @@ class WC_Cart extends WC_Legacy_Cart { $this->coupon_discount_totals = array(); $this->coupon_discount_tax_totals = array(); $this->applied_coupons = array(); - $this->fees = array(); $this->totals = $this->default_totals; if ( $clear_persistent_cart ) { @@ -1695,61 +1695,44 @@ class WC_Cart extends WC_Legacy_Cart { } /** - * Add additional fee to the cart. + * Trigger an action so 3rd parties can add custom fees. * - * Fee is an amount of money charged for a particular piece of work - * or for a particular right or service, and not supposed to be negative. + * @since 2.0.0 + */ + public function calculate_fees() { + do_action( 'woocommerce_cart_calculate_fees', $this ); + } + + /** + * Add additional fee to the cart. * * This method should be called on a callback attached to the * woocommerce_cart_calculate_fees action during cart/checkout. Fees do not * persist. * + * @uses WC_Cart_Fees::add_fee * @param string $name Unique name for the fee. Multiple fees of the same name cannot be added. * @param float $amount Fee amount (do not enter negative amounts). * @param bool $taxable Is the fee taxable? (default: false). * @param string $tax_class The tax class for the fee if taxable. A blank string is standard tax class. (default: ''). */ public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) { - $new_fee_id = sanitize_title( $name ); - - // Only add each fee once. - foreach ( $this->fees as $fee ) { - if ( $fee->id === $new_fee_id ) { - return; - } - } - - $new_fee = new stdClass(); - $new_fee->id = $new_fee_id; - $new_fee->name = esc_attr( $name ); - $new_fee->amount = (float) esc_attr( $amount ); - $new_fee->tax_class = $tax_class; - $new_fee->taxable = $taxable ? true : false; - $new_fee->tax = 0; - $new_fee->tax_data = array(); - $this->fees[] = $new_fee; + $this->fees_api->add_fee( array( + 'name' => $name, + 'amount' => (float) $amount, + 'taxable' => $taxable, + 'tax_class' => $tax_class, + ) ); } /** - * Get fees. + * Return all added fees from the Fees API. * + * @uses WC_Cart_Fees::get_fees * @return array */ public function get_fees() { - return array_filter( (array) $this->fees ); - } - - /** - * Calculate fees. - */ - public function calculate_fees() { - $this->fees = array(); - $this->set_fee_total( 0 ); - $this->set_fee_tax( 0 ); - $this->set_fee_taxes( array() ); - - // Fire an action where developers can add their fees. - do_action( 'woocommerce_cart_calculate_fees', $this ); + return $this->fees_api->get_fees(); } /** diff --git a/includes/legacy/class-wc-legacy-cart.php b/includes/legacy/class-wc-legacy-cart.php index bd74a1ae4ec..90c66d675e8 100644 --- a/includes/legacy/class-wc-legacy-cart.php +++ b/includes/legacy/class-wc-legacy-cart.php @@ -58,7 +58,7 @@ abstract class WC_Legacy_Cart { * @param mixed $value Value to set. */ public function __isset( $name ) { - if ( array_key_exists( $name, $cart_session_data ) ) { + if ( array_key_exists( $name, $cart_session_data ) || 'fees' === $name ) { return true; } return false; @@ -114,6 +114,8 @@ abstract class WC_Legacy_Cart { return $this->get_cart_contents_weight(); case 'cart_contents_count' : return $this->get_cart_contents_count(); + case 'fees' : + return $this->fees_api->get_fees(); case 'tax' : wc_deprecated_argument( 'WC_Cart->tax', '2.3', 'Use WC_Tax:: directly' ); $this->tax = new WC_Tax(); @@ -177,6 +179,9 @@ abstract class WC_Legacy_Cart { case 'coupon_discount_tax_amounts' : $this->set_coupon_discount_tax_totals( $value ); break; + case 'fees' : + $this->fees_api->set_fees( $value ); + break; default : $this->$name = $value; break; From 367f08d79ffdab18bf80dc10208973e1a3d2620e Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 22 Aug 2017 16:12:37 +0100 Subject: [PATCH 02/10] Negative fee logic for cart --- includes/class-wc-cart-fees.php | 21 +++++++++--- includes/class-wc-cart-totals.php | 57 ++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/includes/class-wc-cart-fees.php b/includes/class-wc-cart-fees.php index e8ed3f46f71..ab7eeab392a 100644 --- a/includes/class-wc-cart-fees.php +++ b/includes/class-wc-cart-fees.php @@ -71,7 +71,7 @@ final class WC_Cart_Fees { */ public function add_fee( $args = array() ) { $fee_props = (object) wp_parse_args( $args, $this->default_fee_props ); - $fee_props->name = $fee_props->name ? $fee_props->name : __( 'Fee', 'woocommerce' ); + $fee_props->name = $fee_props->name ? $fee_props->name : __( 'Fee', 'woocommerce' ); $fee_props->tax_class = in_array( $fee_props->tax_class, WC_Tax::get_tax_classes(), true ) ? $fee_props->tax_class: ''; $fee_props->taxable = wc_string_to_bool( $fee_props->taxable ); $fee_props->amount = wc_format_decimal( $fee_props->amount ); @@ -93,7 +93,9 @@ final class WC_Cart_Fees { * @return array */ public function get_fees() { - return array_filter( (array) $this->fees ); + uasort( $this->fees, array( $this, 'sort_fees_callback' ) ); + + return $this->fees; } /** @@ -105,14 +107,25 @@ final class WC_Cart_Fees { $this->set_fees(); } + /** + * Sort fees by amount. + * + * @param WC_Coupon $a Coupon object. + * @param WC_Coupon $b Coupon object. + * @return int + */ + protected function sort_fees_callback( $a, $b ) { + return ( $a->amount > $b->amount ) ? -1 : 1; + } + /** * Generate a unique ID for the fee being added. * - * @param string $fee Fee name. + * @param string $fee Fee object. * @return string fee key. */ private function generate_id( $fee ) { - return sanitize_title( $fee ); + return sanitize_title( $fee->name ); } /** diff --git a/includes/class-wc-cart-totals.php b/includes/class-wc-cart-totals.php index 59ae13a5f42..4738cdc253f 100644 --- a/includes/class-wc-cart-totals.php +++ b/includes/class-wc-cart-totals.php @@ -230,6 +230,34 @@ final class WC_Cart_Totals { } } + /** + * Get item costs grouped by tax class. + * + * @since 3.2.0 + * @return array + */ + protected function get_tax_class_costs() { + $item_tax_classes = wp_list_pluck( $this->items, 'tax_class' ); + $shipping_tax_classes = wp_list_pluck( $this->shipping, 'tax_class' ); + $fee_tax_classes = wp_list_pluck( $this->fees, 'tax_class' ); + $costs = array_fill_keys( $item_tax_classes + $shipping_tax_classes + $fee_tax_classes, 0 ); + $costs['non-taxable'] = 0; + + foreach ( $this->items + $this->fees + $this->shipping as $item ) { + if ( 0 > $item->total ) { + continue; + } + if ( ! $item->taxable ) { + $costs['non-taxable'] += $item->total; + } elseif ( 'inherit' === $item->tax_class ) { + $costs[ reset( $item_tax_classes ) ] += $item->total; + } else { + $costs[ $item->tax_class ] += $item->total; + } + } + return array_filter( $costs ); + } + /** * Get fee objects from the cart. Normalises data * into the same format for use by this class. @@ -247,15 +275,34 @@ final class WC_Cart_Totals { $fee->taxable = $fee->object->taxable; $fee->total = wc_add_number_precision_deep( $fee->object->amount ); - if ( $this->calculate_tax && $fee->object->taxable ) { - $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->object->tax_class, $this->cart->get_customer() ), false ); - $fee->total_tax = array_sum( $fee->taxes ); + if ( $this->calculate_tax ) { + if ( 0 > $fee->total ) { + // Negative fees should have the taxes split between all items so it works as a true discount. + $tax_class_costs = $this->get_tax_class_costs( $order ); + $total_cost = array_sum( $tax_class_costs ); - if ( ! $this->round_at_subtotal() ) { - $fee->total_tax = wc_round_tax_total( $fee->total_tax, wc_get_rounding_precision() ); + if ( $total_cost ) { + foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) { + if ( 'non-taxable' === $tax_class ) { + continue; + } + $proportion = $tax_class_cost / $total_cost; + $cart_discount_proportion = $fee->total * $proportion; + $fee->taxes = wc_array_merge_recursive_numeric( $fee->taxes, WC_Tax::calc_tax( $fee->total * $proportion, WC_Tax::get_rates( $tax_class ) ) ); + } + } + + } elseif ( $fee->object->taxable ) { + $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->tax_class, $this->cart->get_customer() ), false ); } } + $fee->total_tax = array_sum( $fee->taxes ); + + if ( ! $this->round_at_subtotal() ) { + $fee->total_tax = wc_round_tax_total( $fee->total_tax, wc_get_rounding_precision() ); + } + $this->fees[ $fee_key ] = $fee; } } From 00cb48a5feef959562f1acf95d8e04967f8c78bc Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 22 Aug 2017 16:20:23 +0100 Subject: [PATCH 03/10] Pass through to order --- includes/class-wc-cart-fees.php | 28 ++++++++++++++-------------- includes/class-wc-cart-totals.php | 11 ++++++----- includes/class-wc-cart.php | 14 ++++++++++++-- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/includes/class-wc-cart-fees.php b/includes/class-wc-cart-fees.php index ab7eeab392a..44d9075d9ac 100644 --- a/includes/class-wc-cart-fees.php +++ b/includes/class-wc-cart-fees.php @@ -98,6 +98,19 @@ final class WC_Cart_Fees { return $this->fees; } + /** + * Set fees. + * + * @param object[] $raw_fees Array of fees. + */ + public function set_fees( $raw_fees = array() ) { + $this->fees = array(); + + foreach ( $raw_fees as $raw_fee ) { + $this->add_fee( $raw_fee ); + } + } + /** * Remove all fees. * @@ -121,23 +134,10 @@ final class WC_Cart_Fees { /** * Generate a unique ID for the fee being added. * - * @param string $fee Fee object. + * @param string $fee Fee object. * @return string fee key. */ private function generate_id( $fee ) { return sanitize_title( $fee->name ); } - - /** - * Set fees. - * - * @param object[] $raw_fees Array of fees. - */ - private function set_fees( $raw_fees = array() ) { - $this->fees = array(); - - foreach ( $raw_fees as $raw_fee ) { - $this->add_fee( $raw_fee ); - } - } } diff --git a/includes/class-wc-cart-totals.php b/includes/class-wc-cart-totals.php index 4738cdc253f..be09fd89017 100644 --- a/includes/class-wc-cart-totals.php +++ b/includes/class-wc-cart-totals.php @@ -303,6 +303,10 @@ final class WC_Cart_Totals { $fee->total_tax = wc_round_tax_total( $fee->total_tax, wc_get_rounding_precision() ); } + // Set totals within object. + $fee->object->tax_data = wc_remove_number_precision_deep( $fee->taxes ); + $fee->object->tax = wc_remove_number_precision_deep( $fee->total_tax ); + $this->fees[ $fee_key ] = $fee; } } @@ -700,14 +704,11 @@ final class WC_Cart_Totals { */ protected function calculate_fee_totals() { $this->get_fees_from_cart(); + $this->set_total( 'fees_total', array_sum( wp_list_pluck( $this->fees, 'total' ) ) ); $this->set_total( 'fees_total_tax', array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ); - foreach ( $this->fees as $fee_key => $fee ) { - $this->cart->fees[ $fee_key ]->tax = wc_remove_number_precision_deep( $fee->total_tax ); - $this->cart->fees[ $fee_key ]->tax_data = wc_remove_number_precision_deep( $fee->taxes ); - } - + $this->cart->fees_api()->set_fees( wp_list_pluck( $this->fees, 'object' ) ); $this->cart->set_fee_total( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total' ) ) ) ); $this->cart->set_fee_tax( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ) ); $this->cart->set_fee_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->fees, 'taxes' ) ) ) ); diff --git a/includes/class-wc-cart.php b/includes/class-wc-cart.php index 35978af773a..7ee13de4443 100644 --- a/includes/class-wc-cart.php +++ b/includes/class-wc-cart.php @@ -1703,6 +1703,16 @@ class WC_Cart extends WC_Legacy_Cart { do_action( 'woocommerce_cart_calculate_fees', $this ); } + /** + * Return reference to fees API. + * + * @since 3.2.0 + * @return WC_Cart_Fees + */ + public function fees_api() { + return $this->fees_api; + } + /** * Add additional fee to the cart. * @@ -1717,7 +1727,7 @@ class WC_Cart extends WC_Legacy_Cart { * @param string $tax_class The tax class for the fee if taxable. A blank string is standard tax class. (default: ''). */ public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) { - $this->fees_api->add_fee( array( + $this->fees_api()->add_fee( array( 'name' => $name, 'amount' => (float) $amount, 'taxable' => $taxable, @@ -1732,7 +1742,7 @@ class WC_Cart extends WC_Legacy_Cart { * @return array */ public function get_fees() { - return $this->fees_api->get_fees(); + return $this->fees_api()->get_fees(); } /** From 8dbd9b88a743dffa1ca8b9292415d24a9b0b2c49 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 22 Aug 2017 16:26:35 +0100 Subject: [PATCH 04/10] Admin calc --- includes/class-wc-order-item-fee.php | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/includes/class-wc-order-item-fee.php b/includes/class-wc-order-item-fee.php index 8974ba392f8..e5964b9176b 100644 --- a/includes/class-wc-order-item-fee.php +++ b/includes/class-wc-order-item-fee.php @@ -31,6 +31,70 @@ class WC_Order_Item_Fee extends WC_Order_Item { ), ); + /** + * Get item costs grouped by tax class. + * + * @since 3.2.0 + * @param WC_Order $order Order object. + * @return array + */ + protected function get_tax_class_costs( $order ) { + $costs = array_fill_keys( $order->get_items_tax_classes(), 0 ); + $costs['non-taxable'] = 0; + + foreach ( $order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) { + if ( 0 > $item->get_total() ) { + continue; + } + if ( 'taxable' !== $item->get_tax_status() ) { + $costs['non-taxable'] += $item->get_total(); + } elseif ( 'inherit' === $item->get_tax_class() ) { + $costs[ reset( $order->get_items_tax_classes() ) ] += $item->get_total(); + } else { + $costs[ $item->get_tax_class() ] += $item->get_total(); + } + } + + return array_filter( $costs ); + } + /** + * Calculate item taxes. + * + * @since 3.2.0 + * @param array $calculate_tax_for Location data to get taxes for. Required. + * @return bool True if taxes were calculated. + */ + public function calculate_taxes( $calculate_tax_for = array() ) { + if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { + return false; + } + // Use regular calculation unless the fee is negative. + if ( 0 <= $this->get_total() ) { + return parent::calculate_taxes(); + } + if ( wc_tax_enabled() && ( $order = $this->get_order() ) ) { + // Apportion taxes to order items, shipping, and fees. + $order = $this->get_order(); + $tax_class_costs = $this->get_tax_class_costs( $order ); + $total_costs = array_sum( $tax_class_costs ); + $discount_taxes = array(); + if ( $total_costs ) { + foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) { + if ( 'non-taxable' === $tax_class ) { + continue; + } + $proportion = $tax_class_cost / $total_costs; + $cart_discount_proportion = $this->get_total() * $proportion; + $discount_taxes = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, WC_Tax::get_rates( $tax_class ) ) ); + } + } + $this->set_taxes( array( 'total' => $discount_taxes ) ); + } else { + $this->set_taxes( false ); + } + return true; + } + /* |-------------------------------------------------------------------------- | Setters From c41fb8f00c20a0b74ea25d702e793ff99458475e Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 22 Aug 2017 16:31:45 +0100 Subject: [PATCH 05/10] docblock --- includes/class-wc-cart-fees.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/class-wc-cart-fees.php b/includes/class-wc-cart-fees.php index 44d9075d9ac..15bbe709b3d 100644 --- a/includes/class-wc-cart-fees.php +++ b/includes/class-wc-cart-fees.php @@ -2,10 +2,9 @@ /** * Cart fees API. * - * Developers can add fees to the cart via WC()->cart->fees_api which will reference this class. + * Developers can add fees to the cart via WC()->cart->fees_api() which will reference this class. * - * Fees can be added/removed at any time, however, before cart total calculations fees are purged - * so we suggest using the action woocommerce_cart_calculate_fees or woocommerce_before_calculate_totals. + * We suggest using the action woocommerce_cart_calculate_fees hook for adding fees. * * @author Automattic * @package WooCommerce/Classes From 559982fbe7757bfde95f9c5950a4bbb0730d2871 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 22 Aug 2017 17:02:48 +0100 Subject: [PATCH 06/10] Unused variable --- includes/class-wc-cart-totals.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-cart-totals.php b/includes/class-wc-cart-totals.php index be09fd89017..55b77227fec 100644 --- a/includes/class-wc-cart-totals.php +++ b/includes/class-wc-cart-totals.php @@ -278,7 +278,7 @@ final class WC_Cart_Totals { if ( $this->calculate_tax ) { if ( 0 > $fee->total ) { // Negative fees should have the taxes split between all items so it works as a true discount. - $tax_class_costs = $this->get_tax_class_costs( $order ); + $tax_class_costs = $this->get_tax_class_costs(); $total_cost = array_sum( $tax_class_costs ); if ( $total_cost ) { From e83758fe38506152cbb6b1602faadec3000e8b5b Mon Sep 17 00:00:00 2001 From: claudiulodro Date: Tue, 22 Aug 2017 13:26:55 -0700 Subject: [PATCH 07/10] Cart fee tests --- .../framework/helpers/class-wc-helper-fee.php | 82 +++++++++- tests/unit-tests/cart/cart.php | 154 ++++++++++++++++-- 2 files changed, 216 insertions(+), 20 deletions(-) diff --git a/tests/framework/helpers/class-wc-helper-fee.php b/tests/framework/helpers/class-wc-helper-fee.php index a20736c10ca..ac3b7927e0f 100644 --- a/tests/framework/helpers/class-wc-helper-fee.php +++ b/tests/framework/helpers/class-wc-helper-fee.php @@ -21,21 +21,87 @@ class WC_Helper_Fee { } /** - * Add a cart simple fee without taxes. - * Note: need to be added before add any product in the cart. + * Create a cart fee with taxes. * - * @since 2.3 + * @since 3.2 */ - public static function add_cart_fee() { - add_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_simple_fee' ) ); + public static function create_taxed_fee() { + if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { + return; + } + + WC()->cart->add_fee( 'Dummy Taxed Fee', 10, true ); } /** - * Remove a cart simple fee without taxes. + * Create a negative cart fee without taxes. + * + * @since 3.2 + */ + public static function create_negative_fee() { + if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { + return; + } + + WC()->cart->add_fee( 'Dummy Negative Fee', -10 ); + + } + + /** + * Create a negative cart fee with taxes. + * + * @since 3.2 + */ + public static function create_negative_taxed_fee() { + if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { + return; + } + + WC()->cart->add_fee( 'Dummy Negative Taxed Fee', -10, true ); + } + + /** + * Add a cart fee. + * Note: need to be added before add any product in the cart. * * @since 2.3 + * @param string $fee Type of fee to add (Default: simple) */ - public static function remove_cart_fee() { - remove_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_simple_fee' ) ); + public static function add_cart_fee( $fee = '' ) { + switch ( $fee ) { + case 'taxed': + add_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_taxed_fee' ) ); + break; + case 'negative': + add_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_negative_fee' ) ); + break; + case 'negative-taxed': + add_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_negative_taxed_fee' ) ); + break; + default: + add_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_simple_fee' ) ); + } + } + + /** + * Remove a cart fee. + * + * @since 2.3 + * @param string $fee Type of fee to remove (Default: simple) + */ + public static function remove_cart_fee( $fee = '' ) { + switch ( $fee ) { + case 'taxed': + remove_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_taxed_fee' ) ); + break; + case 'negative': + remove_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_negative_fee' ) ); + break; + case 'negative-taxed': + remove_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_negative_taxed_fee' ) ); + break; + default: + remove_action( 'woocommerce_cart_calculate_fees', array( __CLASS__, 'create_simple_fee' ) ); + } } } diff --git a/tests/unit-tests/cart/cart.php b/tests/unit-tests/cart/cart.php index af9d5eff115..99c49751e5a 100644 --- a/tests/unit-tests/cart/cart.php +++ b/tests/unit-tests/cart/cart.php @@ -601,38 +601,168 @@ class WC_Tests_Cart extends WC_Unit_Test_Case { * @since 2.3 */ public function test_cart_fee() { - // Create product + // Create product. $product = WC_Helper_Product::create_simple_product(); update_post_meta( $product->get_id(), '_price', '10' ); update_post_meta( $product->get_id(), '_regular_price', '10' ); - // We need this to have the calculate_totals() method calculate totals + // We need this to have the calculate_totals() method calculate totals. if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { define( 'WOOCOMMERCE_CHECKOUT', true ); } - // Add fee + // Add fee. WC_Helper_Fee::add_cart_fee(); - // Add product to cart + // Add product to cart. WC()->cart->add_to_cart( $product->get_id(), 1 ); - // Test if the cart total amount is equal 20 + // Test if the cart total amount is equal 20. $this->assertEquals( 20, WC()->cart->total ); - // Clearing WC notices + // Clean up. wc_clear_notices(); - - // Clean up the cart WC()->cart->empty_cart(); - - // Remove fee WC_Helper_Fee::remove_cart_fee(); - - // Delete product WC_Helper_Product::delete_product( $product->get_id() ); } + /** + * Test cart fee with taxes. + * + * @since 3.2 + */ + public function test_cart_fee_taxes() { + global $wpdb; + + // Set up taxes. + update_option( 'woocommerce_calc_taxes', 'yes' ); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + update_post_meta( $product->get_id(), '_price', '10' ); + update_post_meta( $product->get_id(), '_regular_price', '10' ); + + // We need this to have the calculate_totals() method calculate totals. + if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { + define( 'WOOCOMMERCE_CHECKOUT', true ); + } + + // Add fee. + WC_Helper_Fee::add_cart_fee( 'taxed' ); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test if the cart total amount is equal 22 ($10 item + $10 fee + 10% taxes). + $this->assertEquals( 22, WC()->cart->total ); + + // Clean up. + wc_clear_notices(); + WC()->cart->empty_cart(); + WC_Helper_Fee::remove_cart_fee( 'taxed' ); + WC_Helper_Product::delete_product( $product->get_id() ); + $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates" ); + $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations" ); + update_option( 'woocommerce_calc_taxes', 'no' ); + } + + /** + * Test negative cart fee. + * + * @since 3.2 + */ + public function test_cart_negative_fee() { + // Create product. + $product = WC_Helper_Product::create_simple_product(); + update_post_meta( $product->get_id(), '_price', '15' ); + update_post_meta( $product->get_id(), '_regular_price', '15' ); + + // We need this to have the calculate_totals() method calculate totals. + if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { + define( 'WOOCOMMERCE_CHECKOUT', true ); + } + + // Add fee. + WC_Helper_Fee::add_cart_fee( 'negative' ); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test if the cart total amount is equal 5. + $this->assertEquals( 5, WC()->cart->total ); + + // Clean up. + wc_clear_notices(); + WC()->cart->empty_cart(); + WC_Helper_Fee::remove_cart_fee( 'negative' ); + WC_Helper_Product::delete_product( $product->get_id() ); + } + + /** + * Test negative cart fee with taxes. + * + * @since 3.2 + */ + public function test_cart_negative_fee_taxes() { + global $wpdb; + + // Set up taxes. + update_option( 'woocommerce_calc_taxes', 'yes' ); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '10.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + // Create product. + $product = WC_Helper_Product::create_simple_product(); + update_post_meta( $product->get_id(), '_price', '15' ); + update_post_meta( $product->get_id(), '_regular_price', '15' ); + + // We need this to have the calculate_totals() method calculate totals. + if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { + define( 'WOOCOMMERCE_CHECKOUT', true ); + } + + // Add fee. + WC_Helper_Fee::add_cart_fee( 'negative-taxed' ); + + // Add product to cart. + WC()->cart->add_to_cart( $product->get_id(), 1 ); + + // Test if the cart total amount is equal 5.50 ($15 item - $10 negative fee + 10% tax). + $this->assertEquals( 5.50, WC()->cart->total ); + + // Clean up. + wc_clear_notices(); + WC()->cart->empty_cart(); + WC_Helper_Fee::remove_cart_fee( 'negative-taxed' ); + WC_Helper_Product::delete_product( $product->get_id() ); + $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates" ); + $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations" ); + update_option( 'woocommerce_calc_taxes', 'no' ); + } + /** * Test cart coupons. */ From 1215da6355eca0e342b7e0681647c66b3d758e2c Mon Sep 17 00:00:00 2001 From: claudiulodro Date: Tue, 22 Aug 2017 14:06:33 -0700 Subject: [PATCH 08/10] WC_Cart_Fees tests --- tests/unit-tests/cart/cart-fees.php | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/unit-tests/cart/cart-fees.php diff --git a/tests/unit-tests/cart/cart-fees.php b/tests/unit-tests/cart/cart-fees.php new file mode 100644 index 00000000000..b3cea7952d8 --- /dev/null +++ b/tests/unit-tests/cart/cart-fees.php @@ -0,0 +1,59 @@ +cart ); + + // Test add_fee. + $args = array( + 'name' => 'testfee', + 'amount' => 10, + ); + $cart_fees->add_fee( $args ); + $applied_fees = $cart_fees->get_fees(); + $this->assertEquals( 'testfee', $applied_fees['testfee']->name ); + $this->assertEquals( 10, $applied_fees['testfee']->amount ); + $this->assertEquals( 1, count( $applied_fees ) ); + + // Test remove_all_fees. + $cart_fees->remove_all_fees(); + $this->assertEquals( array(), $cart_fees->get_fees() ); + + // Test set_fees. + $args = array( + array( + 'name' => 'newfee', + 'amount' => -5, + ), + array( + 'name' => 'newfee2', + 'amount' => 10, + 'tax_class' => 'Reduced rate', + 'taxable' => true + ), + ); + $cart_fees->set_fees( $args ); + $applied_fees = $cart_fees->get_fees(); + $this->assertEquals( -5, $applied_fees['newfee']->amount ); + $this->assertEquals( 'Reduced rate', $applied_fees['newfee2']->tax_class ); + $this->assertEquals( 2, count( $applied_fees ) ); + + // Clean up. + WC()->cart->empty_cart(); + + // Test fees are removed when cart is emptied. + $this->assertEquals( array(), $cart_fees->get_fees() ); + } +} From 577e8f0e2b8ea73af5b6f72ae03908736051a592 Mon Sep 17 00:00:00 2001 From: claudiulodro Date: Tue, 22 Aug 2017 14:08:26 -0700 Subject: [PATCH 09/10] Formatting --- tests/unit-tests/cart/cart-fees.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit-tests/cart/cart-fees.php b/tests/unit-tests/cart/cart-fees.php index b3cea7952d8..9000c2b7809 100644 --- a/tests/unit-tests/cart/cart-fees.php +++ b/tests/unit-tests/cart/cart-fees.php @@ -34,14 +34,14 @@ class WC_Tests_WC_Cart_Fees extends WC_Unit_Test_Case { // Test set_fees. $args = array( array( - 'name' => 'newfee', - 'amount' => -5, + 'name' => 'newfee', + 'amount' => -5, ), array( - 'name' => 'newfee2', - 'amount' => 10, + 'name' => 'newfee2', + 'amount' => 10, 'tax_class' => 'Reduced rate', - 'taxable' => true + 'taxable' => true ), ); $cart_fees->set_fees( $args ); From b8af7164db59ccff1e88ccbf79b766061981f210 Mon Sep 17 00:00:00 2001 From: claudiulodro Date: Tue, 22 Aug 2017 15:03:54 -0700 Subject: [PATCH 10/10] Harden + fix plugin updates tests --- tests/unit-tests/util/plugin-updates.php | 85 +++++++++++++++--------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/tests/unit-tests/util/plugin-updates.php b/tests/unit-tests/util/plugin-updates.php index 3e1d5930766..e9939370d97 100644 --- a/tests/unit-tests/util/plugin-updates.php +++ b/tests/unit-tests/util/plugin-updates.php @@ -58,19 +58,19 @@ class WC_Tests_Plugin_Updates extends WC_Unit_Test_Case { $this->plugins = array( 'test/test.php' => array( - 'Name' => 'Test plugin', + 'Name' => 'Test plugin', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4.0.0', ), 'test2/test2.php' => array( - 'Name' => 'Test plugin 2', + 'Name' => 'Test plugin 2', WC_Plugin_Updates::VERSION_TESTED_HEADER => '5.0', ), 'test3/test3.php' => array( - 'Name' => 'Test plugin 3', + 'Name' => 'Test plugin 3', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4.1.0', ), 'test4/test4.php' => array( - 'Name' => 'Test plugin 4', + 'Name' => 'Test plugin 4', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4.0.1', ), ); @@ -113,26 +113,31 @@ class WC_Tests_Plugin_Updates extends WC_Unit_Test_Case { $this->plugins = array( 'test/test.php' => array( - 'Name' => 'Test plugin', + 'Name' => 'Test plugin', WC_Plugin_Updates::VERSION_TESTED_HEADER => '3.0.0', ), 'test2/test2.php' => array( - 'Name' => 'Test plugin 2', + 'Name' => 'Test plugin 2', WC_Plugin_Updates::VERSION_TESTED_HEADER => '3.9.9', ), 'test3/test3.php' => array( - 'Name' => 'Test plugin 3', + 'Name' => 'Test plugin 3', WC_Plugin_Updates::VERSION_TESTED_HEADER => '3.0', ), ); + $plugin_keys = array_keys( $this->plugins ); + $new_version = '4.0.0'; - $this->assertArraySubset( $this->plugins, $this->updates->get_untested_plugins( $new_version, $release ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertEquals( $plugin_keys, array_intersect( $plugin_keys, array_keys( $untested ) ) ); $new_version = '4.3.0'; - $this->assertArraySubset( $this->plugins, $this->updates->get_untested_plugins( $new_version, $release ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertEquals( $plugin_keys, array_intersect( $plugin_keys, array_keys( $untested ) ) ); $new_version = '4.0.2'; - $this->assertArraySubset( $this->plugins, $this->updates->get_untested_plugins( $new_version, $release ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertEquals( $plugin_keys, array_intersect( $plugin_keys, array_keys( $untested ) ) ); } /** @@ -145,30 +150,42 @@ class WC_Tests_Plugin_Updates extends WC_Unit_Test_Case { $this->plugins = array( 'test/test.php' => array( - 'Name' => 'Test plugin', + 'Name' => 'Test plugin', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4.1.0', ), 'test2/test2.php' => array( - 'Name' => 'Test plugin 2', + 'Name' => 'Test plugin 2', WC_Plugin_Updates::VERSION_TESTED_HEADER => '5.0.0', ), 'test3/test3.php' => array( - 'Name' => 'Test plugin 3', + 'Name' => 'Test plugin 3', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4.1.1', ), 'test4/test4.php' => array( - 'Name' => 'Test plugin 4', + 'Name' => 'Test plugin 4', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4.2.1', ), ); $new_version = '4.1.0'; - $this->assertEquals( array(), $this->updates->get_untested_plugins( $new_version, $release ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertArrayNotHasKey( 'test/test.php', $untested ); + $this->assertArrayNotHasKey( 'test2/test2.php', $untested ); + $this->assertArrayNotHasKey( 'test3/test3.php', $untested ); + $this->assertArrayNotHasKey( 'test4/test4.php', $untested ); $new_version = '4.2.0'; - $this->assertEquals( 2, count( $this->updates->get_untested_plugins( $new_version, $release ) ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertArrayHasKey( 'test/test.php', $untested ); + $this->assertArrayNotHasKey( 'test2/test2.php', $untested ); + $this->assertArrayHasKey( 'test3/test3.php', $untested ); + $this->assertArrayNotHasKey( 'test4/test4.php', $untested ); $new_version = '4.1.5'; - $this->assertEquals( array(), $this->updates->get_untested_plugins( $new_version, $release ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertArrayNotHasKey( 'test/test.php', $untested ); + $this->assertArrayNotHasKey( 'test2/test2.php', $untested ); + $this->assertArrayNotHasKey( 'test3/test3.php', $untested ); + $this->assertArrayNotHasKey( 'test4/test4.php', $untested ); } /** @@ -181,29 +198,33 @@ class WC_Tests_Plugin_Updates extends WC_Unit_Test_Case { $this->plugins = array( 'test/test.php' => array( - 'Name' => 'Test plugin', + 'Name' => 'Test plugin', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4.1.0', ), 'test2/test2.php' => array( - 'Name' => 'Test plugin 2', + 'Name' => 'Test plugin 2', WC_Plugin_Updates::VERSION_TESTED_HEADER => '3.9.0', ), 'test3/test3.php' => array( - 'Name' => 'Test plugin 3', + 'Name' => 'Test plugin 3', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4.2', ), ); + $plugin_keys = array_keys( $this->plugins ); + $new_version = '4.3.0'; - $this->assertEquals( $this->plugins, $this->updates->get_untested_plugins( $new_version, $release ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertEquals( $plugin_keys, array_intersect( $plugin_keys, array_keys( $untested ) ) ); $new_version = '4.3.1'; - $this->assertEquals( $this->plugins, $this->updates->get_untested_plugins( $new_version, $release ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertEquals( $plugin_keys, array_intersect( $plugin_keys, array_keys( $untested ) ) ); $new_version = '4.1.0'; - $this->assertEquals( array( 'test2/test2.php' => $this->plugins['test2/test2.php'] ), $this->updates->get_untested_plugins( $new_version, $release ) ); + $this->assertArrayHasKey( 'test2/test2.php', $this->updates->get_untested_plugins( $new_version, $release ) ); $new_version = '4.1.5'; - $this->assertEquals( array( 'test2/test2.php' => $this->plugins['test2/test2.php'] ), $this->updates->get_untested_plugins( $new_version, $release ) ); + $this->assertArrayHasKey( 'test2/test2.php', $this->updates->get_untested_plugins( $new_version, $release ) ); } /** @@ -217,29 +238,33 @@ class WC_Tests_Plugin_Updates extends WC_Unit_Test_Case { $this->plugins = array( 'test/test.php' => array( - 'Name' => 'Test plugin', + 'Name' => 'Test plugin', WC_Plugin_Updates::VERSION_TESTED_HEADER => '4', ), 'test2/test2.php' => array( - 'Name' => 'Test plugin 2', + 'Name' => 'Test plugin 2', WC_Plugin_Updates::VERSION_TESTED_HEADER => 'Latest release', ), 'test3/test3.php' => array( - 'Name' => 'Test plugin 3', + 'Name' => 'Test plugin 3', WC_Plugin_Updates::VERSION_TESTED_HEADER => 'WC 3.0.0', ), 'test4/test4.php' => array( - 'Name' => 'Test plugin 4', + 'Name' => 'Test plugin 4', WC_Plugin_Updates::VERSION_TESTED_HEADER => ' ', ), ); $release = 'major'; $new_version = '5.0.0'; - $this->assertArraySubset( array( 'test/test.php' => $this->plugins['test/test.php'] ), $this->updates->get_untested_plugins( $new_version, $release ) ); + $this->assertArrayHasKey( 'test/test.php', $this->updates->get_untested_plugins( $new_version, $release ) ); $release = 'minor'; $new_version = '4.1.0'; - $this->assertEquals( array(), $this->updates->get_untested_plugins( $new_version, $release ) ); + $untested = $this->updates->get_untested_plugins( $new_version, $release ); + $this->assertArrayNotHasKey( 'test/test.php', $untested ); + $this->assertArrayNotHasKey( 'test2/test2.php', $untested ); + $this->assertArrayNotHasKey( 'test3/test3.php', $untested ); + $this->assertArrayNotHasKey( 'test4/test4.php', $untested ); } }