From de1f75df1201a9568ba5dd5deac84bf40a6eb78c Mon Sep 17 00:00:00 2001 From: Steffen Mahler Date: Tue, 5 May 2020 15:46:02 +0200 Subject: [PATCH] fix discount calulation for percentage coupon, various bug fixes in tax rounding, add test --- .../Webkul/CartRule/src/Helpers/CartRule.php | 18 ++- packages/Webkul/Checkout/src/Cart.php | 22 ++++ packages/Webkul/Tax/src/Helpers/Tax.php | 16 +-- tests/unit/CartRule/CartRuleCest.php | 124 +++++++++++++++--- tests/unit/Tax/Helpers/TaxCest.php | 8 +- 5 files changed, 151 insertions(+), 37 deletions(-) diff --git a/packages/Webkul/CartRule/src/Helpers/CartRule.php b/packages/Webkul/CartRule/src/Helpers/CartRule.php index 2ea493156..83e47f6d2 100644 --- a/packages/Webkul/CartRule/src/Helpers/CartRule.php +++ b/packages/Webkul/CartRule/src/Helpers/CartRule.php @@ -324,16 +324,14 @@ class CartRule break; } - $item->discount_amount = round( - min( - $item->discount_amount + $discountAmount, - $item->price * $quantity + $item->tax_amount - ),2); - $item->base_discount_amount = round( - min( - $item->base_discount_amount + $baseDiscountAmount, - $item->base_price * $quantity + $item->base_tax_amount - ), 2); + $item->discount_amount = min( + $item->discount_amount + $discountAmount, + $item->price * $quantity + $item->tax_amount + ); + $item->base_discount_amount = min( + $item->base_discount_amount + $baseDiscountAmount, + $item->base_price * $quantity + $item->base_tax_amount + ); $appliedRuleIds[$rule->id] = $rule->id; diff --git a/packages/Webkul/Checkout/src/Cart.php b/packages/Webkul/Checkout/src/Cart.php index 947d68ef1..41896fa8e 100755 --- a/packages/Webkul/Checkout/src/Cart.php +++ b/packages/Webkul/Checkout/src/Cart.php @@ -2,6 +2,7 @@ namespace Webkul\Checkout; +use Webkul\Checkout\Models\Cart as CartModel; use Webkul\Checkout\Models\CartAddress; use Webkul\Checkout\Repositories\CartRepository; use Webkul\Checkout\Repositories\CartItemRepository; @@ -600,6 +601,8 @@ class Cart $cart->base_discount_amount += $shipping->base_discount_amount; } + $cart = $this->finalizeCartTotals($cart); + $quantities = 0; foreach ($cart->items as $item) { @@ -1052,6 +1055,25 @@ class Cart } } + /** + * Round cart totals + * + * @param \Webkul\Checkout\Models\Cart $cart + * + * @return \Webkul\Checkout\Models\Cart + */ + private function finalizeCartTotals(CartModel $cart): CartModel + { + $cart->discount_amount = round($cart->discount_amount, 2); + $cart->base_discount_amount = round($cart->base_discount_amount, 2); + + $cart->grand_total = round($cart->grand_total, 2); + $cart->grand_total = round($cart->grand_total, 2); + $cart->base_grand_total = round($cart->base_grand_total, 2); + + return $cart; + } + /** * @param $user * diff --git a/packages/Webkul/Tax/src/Helpers/Tax.php b/packages/Webkul/Tax/src/Helpers/Tax.php index 0c91acb6f..a52e056e9 100644 --- a/packages/Webkul/Tax/src/Helpers/Tax.php +++ b/packages/Webkul/Tax/src/Helpers/Tax.php @@ -13,16 +13,16 @@ class Tax /** * Returns an array with tax rates and tax amount * - * @param \Webkul\Checkout\Contracts\Cart $cart - * @param bool $asBase + * @param object $that + * @param bool $asBase * * @return array */ - public static function getTaxRatesWithAmount(\Webkul\Checkout\Contracts\Cart $cart, bool $asBase = false): array + public static function getTaxRatesWithAmount(object $that, bool $asBase = false): array { $taxes = []; - foreach ($cart->items as $item) { + foreach ($that->items as $item) { $taxRate = (string) round((float) $item->tax_percent, self::TAX_RATE_PRECISION); if (! array_key_exists($taxRate, $taxes)) { @@ -43,14 +43,14 @@ class Tax /** * Returns the total tax amount * - * @param \Webkul\Checkout\Contracts\Cart $cart - * @param bool $asBase + * @param object $that + * @param bool $asBase * * @return float */ - public static function getTaxTotal(\Webkul\Checkout\Contracts\Cart $cart, bool $asBase = false): float + public static function getTaxTotal(object $that, bool $asBase = false): float { - $taxes = self::getTaxRatesWithAmount($cart, $asBase); + $taxes = self::getTaxRatesWithAmount($that, $asBase); $result = 0; diff --git a/tests/unit/CartRule/CartRuleCest.php b/tests/unit/CartRule/CartRuleCest.php index cf8e4b683..0202929b8 100644 --- a/tests/unit/CartRule/CartRuleCest.php +++ b/tests/unit/CartRule/CartRuleCest.php @@ -36,7 +36,7 @@ class cartRuleWithCoupon class expectedCartItem { - public const ITEM_DISCOUNT_AMOUNT_PRECISION = 2; + public const ITEM_DISCOUNT_AMOUNT_PRECISION = 4; public const ITEM_TAX_AMOUNT_PRECISION = 4; public $cart_id; @@ -106,6 +106,7 @@ class expectedCartItem class expectedCart { public const CART_TOTAL_PRECISION = 2; + public const CART_GRAND_TOTAL_PRECISION = 4; public $customer_id; public $id; @@ -138,11 +139,15 @@ class expectedCart { $this->sub_total = round($this->sub_total, self::CART_TOTAL_PRECISION); $this->tax_total = round($this->tax_total, self::CART_TOTAL_PRECISION); - $this->grand_total = round($this->sub_total + $this->tax_total - $this->discount_amount, self::CART_TOTAL_PRECISION); + $this->discount_amount = round($this->discount_amount, self::CART_TOTAL_PRECISION); + $this->grand_total = round($this->sub_total + $this->tax_total - $this->discount_amount, + self::CART_GRAND_TOTAL_PRECISION); $this->base_sub_total = round($this->base_sub_total, self::CART_TOTAL_PRECISION); $this->base_tax_total = round($this->base_tax_total, self::CART_TOTAL_PRECISION); - $this->base_grand_total = round($this->base_sub_total + $this->base_tax_total - $this->base_discount_amount, self::CART_TOTAL_PRECISION); + $this->base_discount_amount = round($this->base_discount_amount, self::CART_TOTAL_PRECISION); + $this->base_grand_total = round($this->base_sub_total + $this->base_tax_total - $this->base_discount_amount, + self::CART_GRAND_TOTAL_PRECISION); } public function toArray(): array @@ -579,6 +584,102 @@ class CartRuleCest } } + public function checkExampleCase(UnitTester $I) + { + config(['app.default_country' => 'DE']); + + $faker = Factory::create(); + + $customer = $I->have(Customer::class); + + auth()->guard('customer')->loginUsingId($customer->id); + Event::dispatch('customer.after.login', $customer['email']); + + $this->sessionToken = $faker->uuid; + session(['_token' => $this->sessionToken]); + + $tax = $I->have(TaxRate::class, [ + 'country' => 'DE', + 'tax_rate' => 19.0, + ]); + + $taxCategorie = $I->have(TaxCategory::class); + + $I->have(TaxMap::class, [ + 'tax_rate_id' => $tax->id, + 'tax_category_id' => $taxCategorie->id, + ]); + + $productConfig = [ + 'attributeValues' => [ + 'price' => 23.92, + 'tax_category_id' => $taxCategorie->id, + ], + ]; + $product = $I->haveProduct(Laravel5Helper::SIMPLE_PRODUCT, $productConfig); + + $ruleConfig = [ + 'action_type' => self::ACTION_TYPE_PERCENTAGE, + 'discount_amount' => 100, + 'conditions' => [ + [ + 'attribute' => 'product|sku', + 'value' => $product->sku, + 'operator' => '==', + 'attribute_type' => 'text', + ], + ], + ]; + $cartRule = $I->have(CartRule::class, $ruleConfig); + + DB::table('cart_rule_channels')->insert([ + 'cart_rule_id' => $cartRule->id, + 'channel_id' => core()->getCurrentChannel()->id, + ]); + + $guestCustomerGroup = $I->grabRecord('customer_groups', ['code' => 'guest']); + DB::table('cart_rule_customer_groups')->insert([ + 'cart_rule_id' => $cartRule->id, + 'customer_group_id' => $guestCustomerGroup['id'], + ]); + + $generalCustomerGroup = $I->grabRecord('customer_groups', ['code' => 'general']); + DB::table('cart_rule_customer_groups')->insert([ + 'cart_rule_id' => $cartRule->id, + 'customer_group_id' => $generalCustomerGroup['id'], + ]); + + $coupon = $I->have(CartRuleCoupon::class, [ + 'code' => 'AWESOME', + 'cart_rule_id' => $cartRule->id, + ]); + + + $data = [ + '_token' => session('_token'), + 'product_id' => $product->id, + 'quantity' => 1, + ]; + cart()->addProduct($product->id, $data); + cart()->setCouponCode('AWESOME')->collectTotals(); + + $cart = cart()->getCart(); + $cartItem = $cart->items()->first(); + + $I->assertEquals('AWESOME', $cartItem['coupon_code']); + $I->assertEquals(23.92, $cartItem['price']); + $I->assertEquals(19.0, $cartItem['tax_percent']); + $I->assertEquals(4.5448, $cartItem['tax_amount']); + $I->assertEquals(28.4648, $cartItem['discount_amount']); + + $I->assertEquals('AWESOME', $cart->coupon_code); + $I->assertEquals(23.92, $cart->sub_total); + $I->assertEquals(4.54, $cart->tax_total); + $I->assertEquals(28.46, $cart->discount_amount); + // 23.92 + 4.54 - 28.46 = 0.00 + $I->assertEquals(0.00, $cart->grand_total); + } + /** * @param \Codeception\Example $scenario * @param \Tests\Unit\Category\cartRuleWithCoupon $cartRuleWithCoupon @@ -586,19 +687,15 @@ class CartRuleCest * * @return array */ - private function getExpectedCartItems( - Example $scenario, - ?cartRuleWithCoupon $cartRuleWithCoupon, - int $cartID - ): array { + private function getExpectedCartItems(Example $scenario, ?cartRuleWithCoupon $cartRuleWithCoupon, int $cartID): array + { $cartItems = []; foreach ($scenario['productSequence'] as $key => $item) { $pos = $this->array_find( 'product_id', $this->products[$scenario['productSequence'][$key]]->id, - $cartItems, - true + $cartItems ); if ($pos === null) { @@ -793,11 +890,8 @@ class CartRuleCest * * @return \Tests\Unit\Category\expectedCart */ - private function getExpectedCart( - int $cartId, - array $expectedCartItems, - ?cartRuleWithCoupon $cartRuleWithCoupon - ): expectedCart { + private function getExpectedCart(int $cartId, array $expectedCartItems, ?cartRuleWithCoupon $cartRuleWithCoupon): expectedCart + { $cart = new expectedCart( $cartId, auth()->guard('customer')->user()->id diff --git a/tests/unit/Tax/Helpers/TaxCest.php b/tests/unit/Tax/Helpers/TaxCest.php index 4dd4c843a..dc775952a 100644 --- a/tests/unit/Tax/Helpers/TaxCest.php +++ b/tests/unit/Tax/Helpers/TaxCest.php @@ -17,7 +17,6 @@ class TaxCest private const PRODUCT2_QTY = 7; private const CART_TOTAL_PRECISION = 2; - private const TAX_AMOUNT_PRECISION = 2; public function _before(UnitTester $I) { @@ -73,16 +72,17 @@ class TaxCest 'quantity' => self::PRODUCT2_QTY, ]); + // rounded by precision of 2 because this are sums of corresponding tax categories $expectedTaxAmount1 = round( round(self::PRODUCT1_QTY * $product1->price, self::CART_TOTAL_PRECISION) * $tax1->tax_rate / 100, - self::TAX_AMOUNT_PRECISION + self::CART_TOTAL_PRECISION ); $expectedTaxAmount2 = round( round(self::PRODUCT2_QTY * $product2->price, self::CART_TOTAL_PRECISION) * $tax2->tax_rate / 100, - self::TAX_AMOUNT_PRECISION + self::CART_TOTAL_PRECISION ); $this->scenario = [ @@ -92,7 +92,7 @@ class TaxCest (string)round((float)$tax2->tax_rate, 4) => $expectedTaxAmount2, ], 'expectedTaxTotal' => - round($expectedTaxAmount1 + $expectedTaxAmount2, self::TAX_AMOUNT_PRECISION), + round($expectedTaxAmount1 + $expectedTaxAmount2, self::CART_TOTAL_PRECISION), ]; }