More tests and precision fixes
This commit is contained in:
parent
3a76e4492e
commit
dd7fe5f158
|
@ -29,12 +29,36 @@ class WC_Discounts {
|
|||
*/
|
||||
protected $discounts = array();
|
||||
|
||||
/**
|
||||
* Precision so we can work in cents.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $precision = 1;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->precision = pow( 10, wc_get_price_decimals() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove precision from a price.
|
||||
*
|
||||
* @param int $value
|
||||
* @return float
|
||||
*/
|
||||
protected function remove_precision( $value ) {
|
||||
return wc_format_decimal( $value / $this->precision, wc_get_price_decimals() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset items and discounts to 0.
|
||||
*/
|
||||
protected function reset() {
|
||||
$this->items = array();
|
||||
$this->discounts = array( 'cart' => 0 );
|
||||
$this->discounts = array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,6 +71,48 @@ class WC_Discounts {
|
|||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get discount by key without precision.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return array
|
||||
*/
|
||||
public function get_discount( $key ) {
|
||||
return isset( $this->discounts[ $key ] ) ? $this->remove_precision( $this->discounts[ $key ] ) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all discount totals without precision.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return array
|
||||
*/
|
||||
public function get_discounts() {
|
||||
return array_map( array( $this, 'remove_precision' ), $this->discounts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get discounted price of an item without precision.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param object $item
|
||||
* @return float
|
||||
*/
|
||||
public function get_discounted_price( $item ) {
|
||||
return $this->remove_precision( $this->get_discounted_price_in_cents( $item ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get discounted price of an item to precision (in cents).
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param object $item
|
||||
* @return float
|
||||
*/
|
||||
public function get_discounted_price_in_cents( $item ) {
|
||||
return $item->price - $this->discounts[ $item->key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cart/order items which will be discounted.
|
||||
*
|
||||
|
@ -59,7 +125,7 @@ class WC_Discounts {
|
|||
if ( ! empty( $raw_items ) && is_array( $raw_items ) ) {
|
||||
foreach ( $raw_items as $raw_item ) {
|
||||
$item = (object) array(
|
||||
'price' => 0, // Line price without discounts.
|
||||
'price' => 0, // Line price without discounts, in cents.
|
||||
'quantity' => 0, // Line qty.
|
||||
'product' => false,
|
||||
);
|
||||
|
@ -72,12 +138,12 @@ class WC_Discounts {
|
|||
} elseif ( is_a( $raw_item, 'WC_Order_Item_Product' ) ) {
|
||||
$item->key = $raw_item->get_id();
|
||||
$item->quantity = $raw_item->get_quantity();
|
||||
$item->price = $raw_item->get_subtotal() * $this->precision;
|
||||
$item->product = $raw_item->get_product();
|
||||
} else {
|
||||
$item->key = $raw_item['key'];
|
||||
// @todo remove when we implement WC_Cart_Item. This is the old cart item schema.
|
||||
$item->quantity = $raw_item['quantity'];
|
||||
$item->price = $raw_item['data']->get_price() * $raw_item['quantity'];
|
||||
$item->price = $raw_item['data']->get_price() * $this->precision * $raw_item['quantity'];
|
||||
$item->product = $raw_item['data'];
|
||||
}
|
||||
$this->items[ $item->key ] = $item;
|
||||
|
@ -86,26 +152,6 @@ class WC_Discounts {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all discount totals.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return array
|
||||
*/
|
||||
public function get_discounts() {
|
||||
return $this->discounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get discount by key.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return array
|
||||
*/
|
||||
public function get_discount( $key ) {
|
||||
return isset( $this->discounts[ $key ] ) ? $this->discounts[ $key ] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a discount to all items using a coupon.
|
||||
*
|
||||
|
@ -143,51 +189,29 @@ class WC_Discounts {
|
|||
$this->apply_percentage_discount( $coupon->get_amount() );
|
||||
break;
|
||||
case 'fixed_product' :
|
||||
$this->apply_fixed_product_discount( $coupon->get_amount() );
|
||||
$this->apply_fixed_product_discount( $coupon->get_amount() * $this->precision );
|
||||
break;
|
||||
case 'fixed_cart' :
|
||||
$this->apply_fixed_cart_discount( $coupon->get_amount() );
|
||||
$this->apply_fixed_cart_discount( $coupon->get_amount() * $this->precision );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get discounted price of an item.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param object $item
|
||||
* @return float
|
||||
*/
|
||||
public function get_discounted_price( $item ) {
|
||||
return $item->price - $this->discounts[ $item->key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a discount amount to an item and ensure it does not go negative.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param object $item
|
||||
* @param float $discount
|
||||
* @return float
|
||||
* @param int $discount
|
||||
* @return int Amount discounted.
|
||||
*/
|
||||
protected function apply_discount_to_item( &$item, $discount ) {
|
||||
$discounted_price = $this->get_discounted_price( $item );
|
||||
protected function add_item_discount( &$item, $discount ) {
|
||||
$discounted_price = $this->get_discounted_price_in_cents( $item );
|
||||
$discount = $discount > $discounted_price ? $discounted_price : $discount;
|
||||
$this->discounts[ $item->key ] = $this->discounts[ $item->key ] + $discount;
|
||||
return $discount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a discount amount to the cart.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param object $item
|
||||
* @param float $discount
|
||||
*/
|
||||
protected function apply_discount_to_cart( $discount ) {
|
||||
$this->discounts['cart'] += $discount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply percent discount to items.
|
||||
*
|
||||
|
@ -196,7 +220,7 @@ class WC_Discounts {
|
|||
*/
|
||||
protected function apply_percentage_discount( $amount ) {
|
||||
foreach ( $this->items as $item ) {
|
||||
$this->apply_discount_to_item( $item, (float) $amount * ( $this->get_discounted_price( $item ) / 100 ) );
|
||||
$this->add_item_discount( $item, $amount * ( $this->get_discounted_price_in_cents( $item ) / 100 ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,39 +228,72 @@ class WC_Discounts {
|
|||
* Apply fixed product discount to items.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param float $amount
|
||||
* @param int $amount
|
||||
*/
|
||||
protected function apply_fixed_product_discount( $discount ) {
|
||||
foreach ( $this->items as $item ) {
|
||||
$this->apply_discount_to_item( $item, $discount * $item->quantity );
|
||||
$this->add_item_discount( $item, $discount * $item->quantity );
|
||||
}
|
||||
}
|
||||
|
||||
/*protected function filter_taxable_items( $item ) {
|
||||
return $item->is_taxable;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Apply fixed cart discount to items. Cart discounts will be stored and
|
||||
* displayed separately to items, and taxes will be apportioned.
|
||||
* Filter out all products which have been fully discounted to 0.
|
||||
* Used as array_filter callback.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param float $cart_discount
|
||||
* @param object $item
|
||||
* @return bool
|
||||
*/
|
||||
protected function filter_products_with_price( $item ) {
|
||||
return $this->get_discounted_price_in_cents( $item ) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply fixed cart discount to items.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @param int $cart_discount
|
||||
*/
|
||||
protected function apply_fixed_cart_discount( $cart_discount ) {
|
||||
// @todo Fixed cart discounts will be apportioned based on tax class.
|
||||
/*$tax_class_counts = array_count_values( wp_list_pluck( array_filter( $this->items, array( $this, 'filter_taxable_items' ) ), 'tax_class' ) );
|
||||
$item_count = array_sum( wp_list_pluck( $this->items, 'quantity' ) );
|
||||
$cart_discount_taxes = array();
|
||||
$items_to_discount = array_filter( $this->items, array( $this, 'filter_products_with_price' ) );
|
||||
|
||||
foreach ( $tax_class_counts as $tax_class => $tax_class_count ) {
|
||||
$proportion = $tax_class_count / $item_count;
|
||||
$cart_discount_proportion = $cart_discount * $proportion;
|
||||
$tax_rates = WC_Tax::get_rates( $tax_class );
|
||||
$cart_discount_taxes[ $tax_class ] = WC_Tax::calc_tax( $cart_discount_proportion, $tax_rates );
|
||||
if ( ! $item_count = array_sum( wp_list_pluck( $items_to_discount, 'quantity' ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var_dump($cart_discount_taxes);*/
|
||||
$this->apply_discount_to_cart( $cart_discount );
|
||||
$per_item_discount = floor( $cart_discount / $item_count ); // round it down to the nearest cent number.
|
||||
$amount_discounted = 0;
|
||||
|
||||
if ( $per_item_discount > 0 ) {
|
||||
foreach ( $items_to_discount as $item ) {
|
||||
$amount_discounted += $this->add_item_discount( $item, $per_item_discount * $item->quantity );
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is still discount remaining, repeat the process.
|
||||
*/
|
||||
if ( $amount_discounted > 0 && $amount_discounted < $cart_discount ) {
|
||||
$this->apply_fixed_cart_discount( $cart_discount - $amount_discounted );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deal with remaining fractional discounts by splitting it over items
|
||||
* until the amount is expired, discounting 1 cent at a time.
|
||||
*/
|
||||
if ( $cart_discount > 0 ) {
|
||||
foreach ( $items_to_discount as $item ) {
|
||||
for ( $i = 0; $i < $item->quantity; $i ++ ) {
|
||||
$amount_discounted += $this->add_item_discount( $item, 1 );
|
||||
|
||||
if ( $amount_discounted >= $cart_discount ) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
if ( $amount_discounted >= $cart_discount ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,12 +49,48 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case {
|
|||
}
|
||||
|
||||
/**
|
||||
* Test applying a coupon to a set of items.
|
||||
* Test applying a coupon (make sure it changes prices).
|
||||
*/
|
||||
public function test_apply_coupon() {
|
||||
$discounts = new WC_Discounts();
|
||||
|
||||
// Create dummy content.
|
||||
$product = WC_Helper_Product::create_simple_product();
|
||||
$product->set_tax_status( 'taxable' );
|
||||
$product->save();
|
||||
WC()->cart->empty_cart();
|
||||
WC()->cart->add_to_cart( $product->get_id(), 1 );
|
||||
$coupon = new WC_Coupon;
|
||||
$coupon->set_code( 'test' );
|
||||
$coupon->set_amount( 10 );
|
||||
|
||||
// Apply a percent discount.
|
||||
$coupon->set_discount_type( 'percent' );
|
||||
$discounts->set_items( WC()->cart->get_cart() );
|
||||
$discounts->apply_coupon( $coupon );
|
||||
$this->assertEquals( 9, $discounts->get_discounted_price( current( $discounts->get_items() ) ) );
|
||||
|
||||
// Apply a fixed cart coupon.
|
||||
$coupon->set_discount_type( 'fixed_cart' );
|
||||
$discounts->set_items( WC()->cart->get_cart() );
|
||||
$discounts->apply_coupon( $coupon );
|
||||
$this->assertEquals( 0, $discounts->get_discounted_price( current( $discounts->get_items() ) ) );
|
||||
|
||||
// Apply a fixed product coupon.
|
||||
$coupon->set_discount_type( 'fixed_product' );
|
||||
$discounts->set_items( WC()->cart->get_cart() );
|
||||
$discounts->apply_coupon( $coupon );
|
||||
$this->assertEquals( 0, $discounts->get_discounted_price( current( $discounts->get_items() ) ) );
|
||||
|
||||
// Cleanup.
|
||||
WC()->cart->empty_cart();
|
||||
$product->delete( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test various discount calculations are working correctly and produding expected results.
|
||||
*/
|
||||
public function test_calculations() {
|
||||
$tax_rate = array(
|
||||
'tax_rate_country' => '',
|
||||
'tax_rate_state' => '',
|
||||
|
@ -68,51 +104,203 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case {
|
|||
);
|
||||
$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
|
||||
update_option( 'woocommerce_calc_taxes', 'yes' );
|
||||
$product = WC_Helper_Product::create_simple_product();
|
||||
$product->set_tax_status( 'taxable' );
|
||||
$product->save();
|
||||
WC()->cart->empty_cart();
|
||||
WC()->cart->add_to_cart( $product->get_id(), 1 );
|
||||
$coupon = new WC_Coupon;
|
||||
$coupon->set_code( 'test' );
|
||||
$coupon->set_discount_type( 'percent' );
|
||||
$coupon->set_amount( 20 );
|
||||
|
||||
// Apply a percent discount.
|
||||
$coupon->set_discount_type( 'percent' );
|
||||
$discounts->set_items( WC()->cart->get_cart() );
|
||||
$discounts->apply_coupon( $coupon );
|
||||
$this->assertEquals( 8, $discounts->get_discounted_price( current( $discounts->get_items() ) ) );
|
||||
$tests = array(
|
||||
array(
|
||||
'prices_include_tax' => false,
|
||||
'cart' => array(
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
)
|
||||
),
|
||||
'coupons' => array(
|
||||
array(
|
||||
'code' => 'test',
|
||||
'discount_type' => 'percent',
|
||||
'amount' => '20',
|
||||
)
|
||||
),
|
||||
'expected_total_discount' => 2,
|
||||
),
|
||||
array(
|
||||
'prices_include_tax' => false,
|
||||
'cart' => array(
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 2,
|
||||
)
|
||||
),
|
||||
'coupons' => array(
|
||||
array(
|
||||
'code' => 'test',
|
||||
'discount_type' => 'fixed_cart',
|
||||
'amount' => '10',
|
||||
)
|
||||
),
|
||||
'expected_total_discount' => 10,
|
||||
),
|
||||
array(
|
||||
'prices_include_tax' => false,
|
||||
'cart' => array(
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
)
|
||||
),
|
||||
'coupons' => array(
|
||||
array(
|
||||
'code' => 'test',
|
||||
'discount_type' => 'fixed_cart',
|
||||
'amount' => '10',
|
||||
)
|
||||
),
|
||||
'expected_total_discount' => 10,
|
||||
),
|
||||
array(
|
||||
'prices_include_tax' => false,
|
||||
'cart' => array(
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
)
|
||||
),
|
||||
'coupons' => array(
|
||||
array(
|
||||
'code' => 'test',
|
||||
'discount_type' => 'fixed_cart',
|
||||
'amount' => '10',
|
||||
)
|
||||
),
|
||||
'expected_total_discount' => 10,
|
||||
),
|
||||
array(
|
||||
'prices_include_tax' => false,
|
||||
'cart' => array(
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 2,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 3,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 2,
|
||||
)
|
||||
),
|
||||
'coupons' => array(
|
||||
array(
|
||||
'code' => 'test',
|
||||
'discount_type' => 'fixed_cart',
|
||||
'amount' => '10',
|
||||
)
|
||||
),
|
||||
'expected_total_discount' => 10,
|
||||
),
|
||||
array(
|
||||
'prices_include_tax' => false,
|
||||
'cart' => array(
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
),
|
||||
array(
|
||||
'price' => 10,
|
||||
'qty' => 1,
|
||||
)
|
||||
),
|
||||
'coupons' => array(
|
||||
array(
|
||||
'code' => 'test',
|
||||
'discount_type' => 'fixed_cart',
|
||||
'amount' => '10',
|
||||
)
|
||||
),
|
||||
'expected_total_discount' => 10,
|
||||
)
|
||||
);
|
||||
|
||||
// Apply a fixed cart coupon.
|
||||
$coupon->set_discount_type( 'fixed_cart' );
|
||||
$discounts->set_items( WC()->cart->get_cart() );
|
||||
$discounts->apply_coupon( $coupon );
|
||||
$this->assertEquals( 20, $discounts->get_discount( 'cart' ) );
|
||||
foreach ( $tests as $test_index => $test ) {
|
||||
$discounts = new WC_Discounts();
|
||||
$products = array();
|
||||
|
||||
// Apply a fixed cart coupon.
|
||||
$coupon->set_discount_type( 'fixed_cart' );
|
||||
WC()->cart->empty_cart();
|
||||
WC()->cart->add_to_cart( $product->get_id(), 4 );
|
||||
$discounts->set_items( WC()->cart->get_cart() );
|
||||
$discounts->apply_coupon( $coupon );
|
||||
$this->assertEquals( 20, $discounts->get_discount( 'cart' ) );
|
||||
foreach ( $test['cart'] as $item ) {
|
||||
$product = WC_Helper_Product::create_simple_product();
|
||||
$product->set_regular_price( $item['price'] );
|
||||
$product->set_tax_status( 'taxable' );
|
||||
$product->save();
|
||||
WC()->cart->add_to_cart( $product->get_id(), $item['qty'] );
|
||||
$products[] = $product;
|
||||
}
|
||||
|
||||
$discounts->apply_coupon( $coupon );
|
||||
$this->assertEquals( 40, $discounts->get_discount( 'cart' ) );
|
||||
$discounts->set_items( WC()->cart->get_cart() );
|
||||
|
||||
// Apply a fixed product coupon.
|
||||
$coupon->set_discount_type( 'fixed_product' );
|
||||
$coupon->set_amount( 1 );
|
||||
WC()->cart->empty_cart();
|
||||
WC()->cart->add_to_cart( $product->get_id(), 4 );
|
||||
$discounts->set_items( WC()->cart->get_cart() );
|
||||
$discounts->apply_coupon( $coupon );
|
||||
$this->assertEquals( 36, $discounts->get_discounted_price( current( $discounts->get_items() ) ) );
|
||||
foreach ( $test['coupons'] as $coupon_props ) {
|
||||
$coupon = new WC_Coupon;
|
||||
$coupon->set_props( $coupon_props );
|
||||
$discounts->apply_coupon( $coupon );
|
||||
}
|
||||
|
||||
$this->assertEquals( $test['expected_total_discount'], array_sum( $discounts->get_discounts() ), 'Test case ' . $test_index . ' failed (' . print_r( $test, true ) . ' - ' . print_r( $discounts->get_discounts(), true ) . ')' );
|
||||
|
||||
// Clean.
|
||||
WC()->cart->empty_cart();
|
||||
|
||||
foreach ( $products as $product ) {
|
||||
$product->delete( true );
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
WC()->cart->empty_cart();
|
||||
$product->delete( true );
|
||||
WC_Tax::_delete_tax_rate( $tax_rate_id );
|
||||
update_option( 'woocommerce_calc_taxes', 'no' );
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue