CoreShop
CoreShop copied to clipboard
Rounding of prices (specifically for CHF) - AKA rappenrundung
Q | A |
---|---|
Bug report? | no |
Feature request? | yes |
BC Break report? | no |
RFC? | yes |
When using CHF, there's a common usecase to round prices to the nearest 5 rappen (0.05 CHF), AKA "rappenrundung".
This means:
- you put in prices already rounded, like 6.75 CHF
- apply VAT of 7.7% to it, total comes out to 7.27 CHF (7,26975 CHF)
- because of rappenrundung, it should actually become 7.30 CHF
- VAT is calculated to be 0,55 CHF instead of 0,53 CHF
This is enough to round everything in the cart down to the nearest 0.05 CHF, since all the components are either pre-round or rounded by this procedure. See the equivalent here: https://github.com/brick/money#cash-rounding
The implementation is done by decorating the tax calculator factory which creates a decorated tax calculator which does the rounding. Rest of the places (cart, quote, order) already store the calculated values so they just use whatever the calculator said.
Should we add that to the Core? Does your implementation work?
It works, but I don't know if we could make it generic enough, a few things in how various totals are calculated would need to be revised first.
@dkarlovi Is there any code of that decorated tax calculator that you could share here? I actually need exactly the same for one of my projects (Rappenrundung to nearest 0.05 CHF). Would be very helpful!
Futhermore, I think it would be a good idea to have a setting in the core for each currency, where you could choose the rounding, e.g. for CHF 0.05 / for EUR 0.01, etc.
@aarongerig not really, it would need to be done on CS directly IMO.
@dkarlovi, @aarongerig:
I'm doing this in almost any project with adjustments
<?php
namespace AppBundle\CoreShop\Processor;
use CoreShop\Component\Order\Factory\AdjustmentFactoryInterface;
use CoreShop\Component\Order\Model\CartInterface;
use CoreShop\Component\Order\Processor\CartProcessorInterface;
final class RoundingProcessor implements CartProcessorInterface
{
public const ADJUSTMENT_NAME = '5-cent-rounding';
private int $decimalFactor;
private int $decimalPrecision;
private AdjustmentFactoryInterface $adjustmentFactory;
public function __construct(
int $decimalFactor,
int $decimalPrecision,
AdjustmentFactoryInterface $adjustmentFactory
) {
$this->decimalFactor = $decimalFactor;
$this->decimalPrecision = $decimalPrecision;
$this->adjustmentFactory = $adjustmentFactory;
}
public function process(CartInterface $cart)
{
$diffInclTax = $this->calculateRoundingDiff($cart->getTotal(true));
if ($diffInclTax === 0) {
return;
}
$cart->addAdjustment($this->adjustmentFactory->createWithData(self::ADJUSTMENT_NAME, '', $diffInclTax, 0));
}
private function calculateRoundingDiff(int $cartTotal)
{
// $this->decimalFactor = 2
// $this->decimalPrecision = 100
// define float total
$cartTotalFloat = abs($cartTotal / $this->decimalFactor);
// define rounded float total based on 5-cent-rule and use decimalPrecision in round()
$cartTotalFloatRounded = round($cartTotalFloat / 5, $this->decimalPrecision) * 5;
// transform total back to int
// use round() to avoid result of 160399999
$newCartTotal = (int) round(abs($cartTotalFloatRounded * $this->decimalFactor));
return $newCartTotal - $cartTotal;
}
}
Don't forget to clear your ajustments!
<?php
namespace AppBundle\CoreShop\Processor;
use CoreShop\Component\Order\Model\CartInterface;
use CoreShop\Component\Order\Processor\CartProcessorInterface;
final class AdjustmentClearer implements CartProcessorInterface
{
/**
* {@inheritdoc}
*/
public function process(CartInterface $cart)
{
$cart->removeAdjustmentsRecursively(RoundingProcessor::ADJUSTMENT_NAME);
}
}
AppBundle\CoreShop\Processor\RoundingProcessor:
arguments:
- '%coreshop.currency.decimal_factor%'
- '%coreshop.currency.decimal_precision%'
- '@coreshop.factory.adjustment'
tags:
- { name: coreshop.cart_processor, priority: 320 }
AppBundle\CoreShop\Processor\AdjustmentClearer:
tags:
- { name: coreshop.cart_processor, priority: 640 }
This will add or remove the required delta to the next 5 cent step.
Note: This will only affect the cart / pay total!
Better use paymentTotal, that already is rounded to 2 decimals and can then be used for Rappenrundung
That's not enough, you also need to present the "adjusted" total to the customer before he's going to confirm the order.
payment total is processed before the order is commited, you just need to display it in the cart.
Not in CS2, but anyway, it feels terrible wrong to completely ignore the cart total and also to store invalidated values which will fall back to you when it comes to other dependencies like custom reports and so on.
But of course that's up to you.
All the best!
Why ignore? The rule is that the customer only pays in 5 cent steps, it doesn't say that the total must be in 5 cent steps.
Ok. In our cases, it had to be that way.
Btw. I'm not sure if this is core related. Maybe that's something for a "coreshop cookbook" or something like that.
It will not be a Core thing. Even though CoreShop is used a lot in Switzerland, we cannot make exceptions and small adjustments for every country in the Core. Maybe someone can create and maintain a Switzerland Bundle and that takes care of that.