Skip to content

Commit

Permalink
Added the CalculateTaxes listener to cart update and shipping addre…
Browse files Browse the repository at this point in the history
…ss change events
  • Loading branch information
fulopattila122 committed Feb 27, 2024
1 parent 483392f commit 20acf2f
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 0 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
- Added the `Taxable` implementation to Foundation's CartItem, Product and MasterProductVariant classes
- Added the extendable `TaxEngine` (facade) that can resolve tax rates from taxables, billing/shipping addresses (a place for various country-specific taxation drivers)
- Added the `Merchant` interface
- Added the `CalculateTaxes` listener to cart update and shipping address change events
- BC: Added the `?CheckoutSubject` return type to the `getCart()` method of the `Checkout` interface
- BC: Changed `Checkout::getShippingAddress()` return type to be nullable
- BC: Added the void return type to `Checkout::setShippingAddress()`
Expand Down
73 changes: 73 additions & 0 deletions src/Foundation/Listeners/CalculateTaxes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

/**
* Contains the CalculateTaxes class.
*
* @copyright Copyright (c) 2024 Vanilo UG
* @author Attila Fulop
* @license MIT
* @since 2024-01-30
*
*/

namespace Vanilo\Foundation\Listeners;

use Illuminate\Support\Arr;
use Vanilo\Adjustments\Contracts\Adjustable;
use Vanilo\Adjustments\Contracts\Adjustment;
use Vanilo\Adjustments\Models\AdjustmentTypeProxy;
use Vanilo\Cart\Contracts\CartEvent;
use Vanilo\Checkout\Contracts\CheckoutEvent;
use Vanilo\Checkout\Facades\Checkout;
use Vanilo\Foundation\Models\CartItem;
use Vanilo\Support\Dto\DetailedAmount;
use Vanilo\Taxes\Contracts\TaxRateResolver;

class CalculateTaxes
{
public function __construct(
protected ?TaxRateResolver $rateResolver,
) {
}

public function handle(CheckoutEvent|CartEvent $event): void
{
if ($event instanceof CheckoutEvent) {
$checkout = $event->getCheckout();
$cart = $checkout->getCart();
} else {
$cart = $event->getCart();
Checkout::setCart($cart);
$checkout = Checkout::getFacadeRoot();
}

if (!$cart instanceof Adjustable) {
return;
}

$cart->adjustments()->deleteByType(AdjustmentTypeProxy::TAX());

if (null !== $this->rateResolver) {
$taxes = [];
/** @var CartItem $item */
foreach ($cart->getItems() as $item) {
if ($rate = $this->rateResolver->findTaxRate($item)) {
$calculator = $rate->getCalculator();
if ($adjuster = $calculator->getAdjuster($rate->configuration())) {
/** @var Adjustment|null $adjustment */
if ($adjustment = $cart->adjustments()?->create($adjuster)) {
$taxes[$adjustment->getTitle()] = ($taxes[$adjustment->getTitle()] ?? 0) + $adjustment->getAmount();
}
}
}
}
$checkout->setTaxesAmount(
DetailedAmount::fromArray(
Arr::mapWithKeys($taxes, fn($amount, $title) => [['title' => $title, 'amount' => $amount]]),
),
);
}
}
}
3 changes: 3 additions & 0 deletions src/Foundation/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Vanilo\Checkout\Events\ShippingAddressChanged;
use Vanilo\Checkout\Events\ShippingMethodSelected;
use Vanilo\Foundation\Listeners\CalculateShippingFees;
use Vanilo\Foundation\Listeners\CalculateTaxes;
use Vanilo\Foundation\Listeners\DeleteCartAdjustments;
use Vanilo\Foundation\Listeners\UpdateSalesFigures;
use Vanilo\Order\Events\OrderWasCreated;
Expand All @@ -35,9 +36,11 @@ class EventServiceProvider extends ServiceProvider
],
ShippingAddressChanged::class => [
CalculateShippingFees::class,
CalculateTaxes::class,
],
CartUpdated::class => [
CalculateShippingFees::class,
CalculateTaxes::class,
],
CartDeleting::class => [
DeleteCartAdjustments::class,
Expand Down
55 changes: 55 additions & 0 deletions src/Foundation/Tests/Examples/ExampleTaxCalculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* Contains the ExampleTaxCalculator class.
*
* @copyright Copyright (c) 2024 Vanilo UG
* @author Attila Fulop
* @license MIT
* @since 2024-02-26
*
*/

namespace Vanilo\Foundation\Tests\Examples;

use Nette\Schema\Expect;
use Nette\Schema\Schema;
use Vanilo\Adjustments\Adjusters\SimpleTax;
use Vanilo\Contracts\DetailedAmount;
use Vanilo\Taxes\Contracts\TaxCalculator;

class ExampleTaxCalculator implements TaxCalculator
{
public static function getName(): string
{
return 'Example';
}

public function getAdjuster(?array $configuration = null): ?object
{
$rate = floatval($configuration['rate'] ?? 0);
$adjuster = new SimpleTax($rate, false);
$adjuster->setTitle("$rate%");

return $adjuster;
}

public function calculate(?object $subject = null, ?array $configuration = null): DetailedAmount
{
$rate = floatval($configuration['rate'] ?? 0);

return \Vanilo\Support\Dto\DetailedAmount::fromArray([['title' => "$rate%", 'amount' => $subject->itemsTotal() * $rate / 100]]);
}

public function getSchema(): Schema
{
return Expect::structure(['rate' => Expect::float(0)->required()]);
}

public function getSchemaSample(array $mergeWith = null): array
{
return ['rate' => 19];
}
}
50 changes: 50 additions & 0 deletions src/Foundation/Tests/Examples/ExampleTaxEngine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

/**
* Contains the ExampleTaxEngine class.
*
* @copyright Copyright (c) 2024 Vanilo UG
* @author Attila Fulop
* @license MIT
* @since 2024-02-26
*
*/

namespace Vanilo\Foundation\Tests\Examples;

use Vanilo\Contracts\Address;
use Vanilo\Taxes\Contracts\Taxable;
use Vanilo\Taxes\Contracts\TaxRate;
use Vanilo\Taxes\Contracts\TaxRateResolver;
use Vanilo\Taxes\Models\TaxCategoryType;

/**
* This is an example tax rate resolver
* dedicated for unit-test only with
* the following hard-coded logic
* - physical products: 19%
* - shipping: 7%
* - everything else: 15%
*/
class ExampleTaxEngine implements TaxRateResolver
{
public const ID = 'example';

public function findTaxRate(Taxable $taxable, ?Address $billingAddress = null, ?Address $shippingAddress = null): ?TaxRate
{
$rate = match ($taxable->getTaxCategory()->getType()->value()) {
TaxCategoryType::PHYSICAL_GOODS => 19,
TaxCategoryType::TRANSPORT_SERVICES => 7,
default => 15,
};

return \Vanilo\Taxes\Models\TaxRate::firstOrCreate([
'rate' => $rate,
'name' => "$rate%",
'calculator' => 'example',
'configuration' => ['rate' => $rate],
]);
}
}
90 changes: 90 additions & 0 deletions src/Foundation/Tests/TaxCalculationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

/**
* Contains the TaxCalculationTest class.
*
* @copyright Copyright (c) 2024 Vanilo UG
* @author Attila Fulop
* @license MIT
* @since 2024-02-26
*
*/

namespace Vanilo\Foundation\Tests;

use Vanilo\Adjustments\Contracts\AdjustmentCollection;
use Vanilo\Adjustments\Models\AdjustmentType;
use Vanilo\Cart\Facades\Cart;
use Vanilo\Checkout\Facades\Checkout;
use Vanilo\Foundation\Models\Product;
use Vanilo\Foundation\Tests\Examples\ExampleTaxCalculator;
use Vanilo\Foundation\Tests\Examples\ExampleTaxEngine;
use Vanilo\Taxes\Facades\TaxEngine;
use Vanilo\Taxes\Models\TaxCategory;
use Vanilo\Taxes\Models\TaxCategoryType;
use Vanilo\Taxes\Resolver\TaxEngineManager;
use Vanilo\Taxes\TaxCalculators;

class TaxCalculationTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

TaxEngine::extend(ExampleTaxEngine::ID, ExampleTaxEngine::class);
TaxCalculators::register('example', ExampleTaxCalculator::class);
}

/** @test */
public function no_tax_adjustment_gets_created_if_there_is_no_tax_engine_configured()
{
$product = factory(Product::class)->create();
config(['vanilo.taxes.engine.driver' => null]);

Cart::addItem($product);
Checkout::setCart(Cart::getFacadeRoot());

$this->assertCount(0, Cart::adjustments()->byType(AdjustmentType::TAX()));
$this->assertEquals(Cart::itemsTotal(), Cart::total());
}

/** @test */
public function no_tax_adjustment_gets_created_if_the_null_driver_is_set()
{
$product = factory(Product::class)->create();
config(['vanilo.taxes.engine.driver' => TaxEngineManager::NULL_DRIVER]);

Cart::addItem($product);
Checkout::setCart(Cart::getFacadeRoot());

$this->assertCount(0, Cart::adjustments()->byType(AdjustmentType::TAX()));
$this->assertEquals(Cart::itemsTotal(), Cart::total());
}

/** @test */
public function it_creates_a_tax_adjustment_when_setting_a_tax_engine()
{
$taxCategory = TaxCategory::create([
'name' => 'Physical products',
'type' => TaxCategoryType::PHYSICAL_GOODS,
'calculator' => 'example'
]);
$product = factory(Product::class)->create(['price' => 100, 'tax_category_id' => $taxCategory->id]);
config(['vanilo.taxes.engine.driver' => ExampleTaxEngine::ID]);

Cart::addItem($product);
Checkout::setCart(Cart::getFacadeRoot());

/** @var AdjustmentCollection $taxAdjustments */
$taxAdjustments = Cart::adjustments()->byType(AdjustmentType::TAX());
$this->assertCount(1, $taxAdjustments);
$taxAdjustment = $taxAdjustments->first();
$this->assertEquals(19, $taxAdjustment->getAmount());
$this->assertTrue($taxAdjustment->isCharge());
$this->assertFalse($taxAdjustment->isIncluded());
$this->assertEquals(100, Cart::itemsTotal());
$this->assertEquals(100 + 19, Cart::total());
}
}

0 comments on commit 20acf2f

Please sign in to comment.