diff --git a/Changelog.md b/Changelog.md index a24306355..ce9b85e8d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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()` diff --git a/src/Foundation/Listeners/CalculateTaxes.php b/src/Foundation/Listeners/CalculateTaxes.php new file mode 100644 index 000000000..3cb7fd3ca --- /dev/null +++ b/src/Foundation/Listeners/CalculateTaxes.php @@ -0,0 +1,73 @@ +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]]), + ), + ); + } + } +} diff --git a/src/Foundation/Providers/EventServiceProvider.php b/src/Foundation/Providers/EventServiceProvider.php index c24731c94..7310467cf 100644 --- a/src/Foundation/Providers/EventServiceProvider.php +++ b/src/Foundation/Providers/EventServiceProvider.php @@ -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; @@ -35,9 +36,11 @@ class EventServiceProvider extends ServiceProvider ], ShippingAddressChanged::class => [ CalculateShippingFees::class, + CalculateTaxes::class, ], CartUpdated::class => [ CalculateShippingFees::class, + CalculateTaxes::class, ], CartDeleting::class => [ DeleteCartAdjustments::class, diff --git a/src/Foundation/Tests/Examples/ExampleTaxCalculator.php b/src/Foundation/Tests/Examples/ExampleTaxCalculator.php new file mode 100644 index 000000000..7a385128c --- /dev/null +++ b/src/Foundation/Tests/Examples/ExampleTaxCalculator.php @@ -0,0 +1,55 @@ +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]; + } +} diff --git a/src/Foundation/Tests/Examples/ExampleTaxEngine.php b/src/Foundation/Tests/Examples/ExampleTaxEngine.php new file mode 100644 index 000000000..b597583c2 --- /dev/null +++ b/src/Foundation/Tests/Examples/ExampleTaxEngine.php @@ -0,0 +1,50 @@ +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], + ]); + } +} diff --git a/src/Foundation/Tests/TaxCalculationTest.php b/src/Foundation/Tests/TaxCalculationTest.php new file mode 100644 index 000000000..57483793f --- /dev/null +++ b/src/Foundation/Tests/TaxCalculationTest.php @@ -0,0 +1,90 @@ +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()); + } +}