fees pass

This commit is contained in:
Mike Jolley 2017-07-25 14:05:49 +01:00
parent ee545e7793
commit e8e200195f
2 changed files with 174 additions and 94 deletions

View File

@ -1,14 +1,26 @@
<?php
/**
* Order/cart totals calculation class.
*
* Methods are private and class is final to keep this as an internal API.
* May be opened in the future once structure is stable.
*
* @author Automattic
* @package WooCommerce/Classes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Order/cart totals calculation class.
* WC_Totals class.
*
* @author Automattic
* @package WooCommerce/Classes
* @version 3.2.0
* @todo consider extending this for cart vs orders if lots of conditonal logic is needed.
* @todo Instead of setting cart totals from here, do it from a subclass.
* @todo woocommerce_tax_round_at_subtotal option - how should we handle this with precision?
* @todo woocommerce_calculate_totals action for carts.
* @todo woocommerce_calculated_total filter for carts.
* @since 3.2.0
*/
class WC_Totals {
@ -19,7 +31,7 @@ class WC_Totals {
* @since 3.2.0
* @var array
*/
protected $object;
private $object;
/**
* Line items to calculate.
@ -27,37 +39,23 @@ class WC_Totals {
* @since 3.2.0
* @var array
*/
protected $items = array(); // @todo ?
private $items = array();
/**
* Discount amounts in cents after calculation.
* Fees to calculate.
*
* @since 3.2.0
* @var array
*/
protected $discounts = array();
private $fees = array();
/**
* Stores totals.
* Discount amounts in cents after calculation for the cart.
*
* @since 3.2.0
* @var array
*/
protected $totals = array(
'fees_total' => 0,
'fees_total_tax' => 0,
'items_subtotal' => 0,
'items_subtotal_tax' => 0,
'items_total' => 0,
'items_total_tax' => 0,
'total' => 0,
'taxes' => array(),
'tax_total' => 0,
'shipping_total' => 0,
'shipping_tax_total' => 0,
'discounts_total' => 0,
'discounts_tax_total' => 0,
);
private $discount_totals = array();
/**
* Precision so we can work in cents.
@ -65,12 +63,35 @@ class WC_Totals {
* @since 3.2.0
* @var int
*/
protected $precision = 1;
private $precision = 1;
/**
* Stores totals.
*
* @since 3.2.0
* @var array
*/
private $totals = array(
'fees_total' => 0,
'fees_total_tax' => 0,
'items_subtotal' => 0,
'items_subtotal_tax' => 0,
'items_total' => 0,
'items_total_tax' => 0,
'total' => 0,
'taxes' => array(),
'tax_total' => 0,
'shipping_total' => 0,
'shipping_tax_total' => 0,
'discounts_total' => 0,
'discounts_tax_total' => 0,
);
/**
* Sets up the items provided, and calculate totals.
*
* @since 3.2.0
* @param object $cart Cart or order object to calculate totals for.
*/
public function __construct( &$cart = null ) {
$this->precision = pow( 10, wc_get_price_decimals() );
@ -80,11 +101,12 @@ class WC_Totals {
}
/**
* Handles a cart or order object passed in for calculation. Normalises data.
* Handles a cart or order object passed in for calculation. Normalises data
* into the same format for use by this class.
*
* @since 3.2.0
*/
protected function set_items() {
private function set_items() {
if ( is_a( $this->object, 'WC_Cart' ) ) {
foreach ( $this->object->get_cart() as $cart_item_key => $cart_item ) {
$item = $this->get_default_item_props();
@ -98,13 +120,13 @@ class WC_Totals {
}
/**
* Remove precision from a price.
* Remove precision (deep) from a price.
*
* @since 3.2.0
* @param int $value
* @param int|array $value Value to remove precision from.
* @return float
*/
protected function remove_precision( $value ) {
private function remove_precision( $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $key => $subvalue ) {
$value[ $key ] = $this->remove_precision( $subvalue );
@ -121,8 +143,12 @@ class WC_Totals {
* @since 3.2.0
* @return array
*/
protected function get_default_item_props() {
private function get_default_item_props() {
return (object) array(
'key' => '',
'quantity' => 0,
'price' => 0,
'product' => false,
'price_includes_tax' => wc_prices_include_tax(),
'subtotal' => 0,
'subtotal_tax' => 0,
@ -134,6 +160,19 @@ class WC_Totals {
);
}
/**
* Get default blank set of props used per fee.
*
* @since 3.2.0
* @return array
*/
private function get_default_fee_props() {
return (object) array(
'total_tax' => 0,
'taxes' => array(),
);
}
/**
* Only ran if woocommerce_adjust_non_base_location_prices is true.
*
@ -141,16 +180,18 @@ class WC_Totals {
* taxes. This is off by default unless the filter is used.
*
* @since 3.2.0
* @param object $item Item to adjust the prices of.
* @return object
*/
protected function adjust_non_base_location_price( $item ) {
private function adjust_non_base_location_price( $item ) {
$base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->tax_class );
$item_tax_rates = $this->get_item_tax_rates( $item );
if ( $item_tax_rates !== $base_tax_rates ) {
// Work out a new base price without the shop's base tax
// Work out a new base price without the shop's base tax.
$taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true, true );
// Now we have a new item price (excluding TAX)
// Now we have a new item price (excluding TAX).
$item->price = $item->price - array_sum( $taxes );
$item->price_includes_tax = false;
}
@ -161,20 +202,20 @@ class WC_Totals {
* Get discounted price of an item with precision (in cents).
*
* @since 3.2.0
* @param object $item
* @param object $item Item to get the price of.
* @return int
*/
protected function get_discounted_price_in_cents( $item ) {
return $item->price - $this->totals['discounts'][ $item->key ];
private function get_discounted_price_in_cents( $item ) {
return $item->price - $this->discount_totals[ $item->key ];
}
/**
* Get tax rates for an item. Caches rates in class to avoid multiple look ups.
*
* @param object $item
* @param object $item Item to get tax rates for.
* @return array of taxes
*/
protected function get_item_tax_rates( $item ) {
private function get_item_tax_rates( $item ) {
$tax_class = $item->product->get_tax_class();
return isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class() );
}
@ -185,7 +226,7 @@ class WC_Totals {
* @since 3.2.0
* @return array
*/
protected function get_coupons() {
private function get_coupons() {
if ( is_a( $this->object, 'WC_Cart' ) ) {
return $this->object->get_coupons();
}
@ -197,27 +238,27 @@ class WC_Totals {
* @since 3.2.0
* @return array
*/
protected function get_shipping() {
private function get_shipping() {
// @todo get this somehow. Where does calc occur?
return array();
}
protected function get_discounts() {
/**
* Get discounts.
*
* @return array
*/
private function get_discounts() {
// @todo fee style API for discounts in cart/checkout.
return array();
}
protected function get_fees() {
// @todo where should fee api be located? New class?
return array();
}
/**
* Get a single total with or without precision (in cents).
*
* @since 3.2.0
* @param string $key Total to get.
* @param bool $in_cents
* @param bool $in_cents Should the totals be returned in cents, or without precision.
* @return int|float
*/
public function get_total( $key = 'total', $in_cents = false ) {
@ -229,10 +270,10 @@ class WC_Totals {
* Set a single total.
*
* @since 3.2.0
* @param string $key
* @param int $total
* @param string $key Total name you want to set.
* @param int $total Total to set.
*/
protected function set_total( $key = 'total', $total ) {
private function set_total( $key = 'total', $total ) {
$this->totals[ $key ] = $total;
}
@ -240,7 +281,7 @@ class WC_Totals {
* Get all totals with or without precision (in cents).
*
* @since 3.2.0
* @param $in_cents bool
* @param bool $in_cents Should the totals be returned in cents, or without precision.
* @return array.
*/
public function get_totals( $in_cents = false ) {
@ -255,22 +296,29 @@ class WC_Totals {
* @since 3.2.0
* @return array
*/
protected function get_merged_taxes() {
private function get_merged_taxes() {
$taxes = array();
foreach ( array_merge( $this->items, $this->fees, $this->get_shipping() ) as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
$taxes[ $rate_id ] = array( 'tax_total' => 0, 'shipping_tax_total' => 0 );
}
}
foreach ( $this->items as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
if ( ! isset( $taxes[ $rate_id ] ) ) {
$taxes[ $rate_id ] = array( 'tax_total' => 0, 'shipping_tax_total' => 0 );
}
$taxes[ $rate_id ]['tax_total'] = $taxes[ $rate_id ]['tax_total'] + $rate;
}
}
foreach ( $this->fees as $fee ) {
foreach ( $fee->taxes as $rate_id => $rate ) {
$taxes[ $rate_id ]['tax_total'] = $taxes[ $rate_id ]['tax_total'] + $rate;
}
}
foreach ( $this->get_shipping() as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
if ( ! isset( $taxes[ $rate_id ] ) ) {
$taxes[ $rate_id ] = array( 'tax_total' => 0, 'shipping_tax_total' => 0 );
}
$taxes[ $rate_id ]['shipping_tax_total'] = $taxes[ $rate_id ]['shipping_tax_total'] + $rate;
}
}
@ -284,11 +332,11 @@ class WC_Totals {
*/
/**
* Run all calculations methods on the given items.
* Run all calculations methods on the given items in sequence.
*
* @since 3.2.0
*/
protected function calculate() {
private function calculate() {
$this->calculate_item_subtotals();
$this->calculate_discounts();
$this->calculate_item_totals();
@ -311,7 +359,7 @@ class WC_Totals {
*
* @since 3.2.0
*/
protected function calculate_item_subtotals() {
private function calculate_item_subtotals() {
foreach ( $this->items as $item ) {
if ( $item->price_includes_tax && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
$item = $this->adjust_non_base_location_price( $item );
@ -336,23 +384,21 @@ class WC_Totals {
/**
* Calculate all discount and coupon amounts.
*
* @todo Manual discounts.
*
* @since 3.2.0
* @uses WC_Discounts class.
*/
protected function calculate_discounts() {
private function calculate_discounts() {
$discounts = new WC_Discounts( $this->items );
foreach ( $this->get_coupons() as $coupon ) {
$discounts->apply_coupon( $coupon );
}
foreach ( $this->get_discounts() as $discount ) {
//$discounts->apply_discount( $coupon ); @todo
}
$this->totals['discounts'] = $discounts->get_discounts();
$this->totals['discounts_total'] = array_sum( $this->totals['discounts'] );
// $this->totals['discounts_tax_total'] = $value;
$this->discount_totals = $discounts->get_discounts();
$this->totals['discounts_total'] = array_sum( $this->discount_totals );
// @todo $this->totals['discounts_tax_total'] = $value;
/*$this->set_coupon_totals( wp_list_pluck( $this->coupons, 'total' ) );
//$this->set_coupon_tax_totals( wp_list_pluck( $this->coupons, 'total_tax' ) );
@ -360,11 +406,11 @@ class WC_Totals {
}
/**
* Totals are costs after discounts.
* Totals are costs after discounts. @todo move cart specific setters to subclass?
*
* @since 3.2.0
*/
protected function calculate_item_totals() {
private function calculate_item_totals() {
foreach ( $this->items as $item ) {
$item->total = $this->get_discounted_price_in_cents( $item );
$item->total_tax = 0;
@ -382,20 +428,45 @@ class WC_Totals {
}
$this->set_total( 'items_total', array_sum( array_values( wp_list_pluck( $this->items, 'total' ) ) ) );
$this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) );
$this->object->subtotal = array_sum( wp_list_pluck( $this->items, 'total' ) ) + array_sum( wp_list_pluck( $this->items, 'total_tax' ) );
$this->object->subtotal_ex_tax = array_sum( wp_list_pluck( $this->items, 'total' ) );
}
/**
* Calculate any fees taxes.
* Triggers the cart fees API, grabs the list of fees, and calculates taxes.
*
* Note: This class sets the totals for the 'object' as they are calculated. This is so that APIs like the fees API can see these totals if needed.
*
* @since 3.2.0
* @todo logic is unqiue to carts.
*/
protected function calculate_fee_totals() {
foreach ( $this->get_fees() as $fee_key => $fee ) {
if ( wc_tax_enabled() && $fee->taxable ) {
$fee->taxes = WC_Tax::calc_tax( $fee->total, $tax_rates, false );
private function calculate_fee_totals() {
$this->object->calculate_fees();
foreach ( $this->object->get_fees() as $fee_key => $fee_object ) {
$fee = $this->get_default_fee_props();
$fee->object = $fee_object;
$fee->total = $fee->object->amount * $this->precision;
if ( wc_tax_enabled() && $fee->object->taxable ) {
$fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->object->tax_class ), false );
$fee->total_tax = array_sum( $fee->taxes );
}
$this->fees[ $fee_key ] = $fee;
}
// Store totals to self.
$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' ) ) );
// Transfer totals to the cart.
foreach ( $this->fees as $fee_key => $fee ) {
$this->object->fees[ $fee_key ]->tax = $this->remove_precision( $fee->total_tax );
$this->object->fees[ $fee_key ]->tax_data = $this->remove_precision( $fee->taxes );
}
$this->object->fee_total = $this->remove_precision( array_sum( wp_list_pluck( $this->fees, 'total' ) ) );
}
/**
@ -403,7 +474,7 @@ class WC_Totals {
*
* @since 3.2.0
*/
protected function calculate_shipping_totals() {
private function calculate_shipping_totals() {
//$this->set_shipping_total( array_sum( array_values( wp_list_pluck( $this->shipping, 'total' ) ) ) );
}
@ -412,20 +483,10 @@ class WC_Totals {
*
* @since 3.2.0
*/
protected function calculate_totals() {
private function calculate_totals() {
$this->set_total( 'taxes', $this->get_merged_taxes() );
$this->set_total( 'tax_total', array_sum( wp_list_pluck( $this->get_total( 'taxes', true ), 'tax_total' ) ) );
$this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->get_total( 'taxes', true ), 'shipping_tax_total' ) ) );
//$this->set_fees_total( array_sum( array_values( wp_list_pluck( $this->fees, 'total' ) ) ) );
//$this->set_fees_total_tax( array_sum( array_values( wp_list_pluck( $this->fees, 'total_tax' ) ) ) );
$this->set_total( 'total', round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + $this->get_total( 'tax_total', true ) + $this->get_total( 'shipping_tax_total', true ) ) );
// @todo woocommerce_tax_round_at_subtotal option - how should we handle this with precision?
// @todo woocommerce_calculate_totals action for carts.
// @todo woocommerce_calculated_total filter for carts.
}
}

View File

@ -35,6 +35,9 @@ class WC_Tests_Totals extends WC_Unit_Test_Case {
$product = WC_Helper_Product::create_simple_product();
$product2 = WC_Helper_Product::create_simple_product();
WC_Helper_Shipping::create_simple_flat_rate();
WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) );
$coupon = new WC_Coupon;
$coupon->set_code( 'test-coupon-10' );
$coupon->set_amount( 10 );
@ -48,21 +51,32 @@ class WC_Tests_Totals extends WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_to_cart( $product2->get_id(), 2 );
WC()->cart->add_fee( "test fee", 10, true );
WC()->cart->add_fee( "test fee 2", 20, true );
WC()->cart->add_fee( "test fee non-taxable", 10, false );
WC()->cart->add_discount( $coupon->get_code() );
add_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_cart_fees_callback' ) );
// @todo manual discounts
$this->totals = new WC_Totals( WC()->cart );
}
/**
* Add fees when the fees API is called.
*/
public function add_cart_fees_callback() {
WC()->cart->add_fee( "test fee", 10, true );
WC()->cart->add_fee( "test fee 2", 20, true );
WC()->cart->add_fee( "test fee non-taxable", 10, false );
}
/**
* Clean up after test.
*/
public function tearDown() {
WC()->cart->empty_cart();
WC()->session->set( 'chosen_shipping_methods', array() );
WC_Helper_Shipping::delete_simple_flat_rate();
update_option( 'woocommerce_calc_taxes', 'no' );
remove_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_cart_fees_callback' ) );
foreach ( $this->products as $product ) {
$product->delete( true );
@ -88,8 +102,13 @@ class WC_Tests_Totals extends WC_Unit_Test_Case {
'items_subtotal_tax' => 6.00,
'items_total' => 27.00,
'items_total_tax' => 5.40,
'total' => 72.40,
'taxes' => array(), // @todo ?
'total' => 78.40,
'taxes' => array(
1 => array(
'tax_total' => 11.40,
'shipping_tax_total' => 0.00,
)
),
'tax_total' => 11.40,
'shipping_total' => 0, // @todo ?
'shipping_tax_total' => 0, // @todo ?