Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Teller helper to do more currency calculations #3834

Merged
merged 17 commits into from
Feb 3, 2025
150 changes: 122 additions & 28 deletions src/adjusters/Tax.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
use craft\commerce\models\TaxRate;
use craft\commerce\Plugin;
use craft\commerce\records\TaxRate as TaxRateRecord;
use craft\commerce\services\Taxes;
use craft\commerce\taxidvalidators\EuVatIdValidator;
use craft\elements\Address;
use DvK\Vat\Validator;
use Exception;
use Illuminate\Support\Collection;
use Money\Teller;
use yii\base\InvalidConfigException;
use function in_array;

Expand Down Expand Up @@ -76,12 +76,47 @@ class Tax extends Component implements AdjusterInterface
private float $_costRemovedForOrderShipping = 0;

/**
* Track the additional discounts created inside the tax adjuster for order total price
* Track the additional discounts created inside the tax adjuster for total price
*
* @internal This should not be modified directly, use _addAmountRemovedForOrderShipping() instead
* @var float
* @see _addAmountRemovedForOrderTotalPrice()
*/
private float $_costRemovedForOrderTotalPrice = 0;

/**
* The way to internally interact with the _costRemovedForOrderShipping property
*
* @param float $amount
* @return void
* @throws Exception
*/
private function _addAmountRemovedForOrderShipping(float $amount): void
{
if ($amount > 0) {
throw new Exception('Amount added to the total removed shipping must be a negative number');
}

$this->_costRemovedForOrderShipping = (float)$this->_getTeller()->add($this->_costRemovedForOrderShipping, $amount);
}


/**
* The way to interact with the _costRemovedForOrderTotalPrice property
*
* @param float $amount
* @return void
* @throws Exception
*/
private function _addAmountRemovedForOrderTotalPrice(float $amount): void
{
if ($amount > 0) {
throw new Exception('Amount added to the total removed price must be a negative number');
}

$this->_costRemovedForOrderTotalPrice = (float)$this->_getTeller()->add($this->_costRemovedForOrderTotalPrice, $amount);
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -122,6 +157,7 @@ private function _adjustInternal(): array
private function _getAdjustments(TaxRate $taxRate): array
{
$adjustments = [];
$teller = $this->_getTeller();
$hasValidTaxId = false;

$zoneMatches = $taxRate->getIsEverywhere() || ($taxRate->getTaxZone() && $this->_matchAddress($taxRate->getTaxZone()));
Expand All @@ -134,7 +170,7 @@ private function _getAdjustments(TaxRate $taxRate): array
$removeDueToVatId = ($zoneMatches && $hasValidTaxId && $taxRate->removeVatIncluded);
if ($removeIncluded || $removeDueToVatId) {

// Is this an order level tax rate?
// Remove included tax for order level taxable.
if (in_array($taxRate->taxable, TaxRateRecord::ORDER_TAXABALES, false)) {
$orderTaxableAmount = 0;

Expand All @@ -144,37 +180,62 @@ private function _getAdjustments(TaxRate $taxRate): array
$orderTaxableAmount = $this->_order->getTotalShippingCost();
}

$amount = -$this->_getTaxAmount($orderTaxableAmount, $taxRate->rate, $taxRate->include);
$orderLevelAmountToBeRemovedByDiscount = -$this->_getTaxAmount($orderTaxableAmount, $taxRate->rate, $taxRate->include);

if ($taxRate->taxable === TaxRateRecord::TAXABLE_ORDER_TOTAL_PRICE) {
$this->_costRemovedForOrderTotalPrice += $amount;
$this->_addAmountRemovedForOrderTotalPrice($orderLevelAmountToBeRemovedByDiscount);
} elseif ($taxRate->taxable === TaxRateRecord::TAXABLE_ORDER_TOTAL_SHIPPING) {
$this->_costRemovedForOrderShipping += $amount;
$this->_addAmountRemovedForOrderShipping($orderLevelAmountToBeRemovedByDiscount);
}

$adjustment = $this->_createAdjustment($taxRate);
// We need to display the adjustment that removed the included tax
$adjustment->name = Craft::t('site', $taxRate->name) . ' ' . Craft::t('commerce', 'Removed');
$adjustment->amount = $amount;
$adjustment->amount = $orderLevelAmountToBeRemovedByDiscount;
$adjustment->type = 'discount'; // TODO Not use a discount adjustment, but modify the price of the item instead. #COM-26
$adjustment->included = false;

$adjustments[] = $adjustment;
}

// Not an order level taxable, add tax adjustments to the line items.
if (!in_array($taxRate->taxable, TaxRateRecord::ORDER_TAXABALES, false)) {
// Not an order level taxable, add tax adjustments to the line items.
foreach ($this->_order->getLineItems() as $item) {
if ($item->taxCategoryId == $taxRate->taxCategoryId) {
if ($taxRate->taxable == TaxRateRecord::TAXABLE_PURCHASABLE) {
$taxableAmount = $item->salePrice - Currency::round($item->getDiscount() / $item->qty);
$amount = -($taxableAmount - ($taxableAmount / (1 + $taxRate->rate)));
$amount = $amount * $item->qty;
// taxableAmount = salePrice - (discount / qty)
$taxableAmount = $teller->subtract(
$item->salePrice,
$teller->divide(
$item->getDiscount(), // float amount of discount
$item->qty
)
);

// amount = taxableAmount - (taxableAmount / (1 + taxRate))
$amount = $teller->subtract(
$taxableAmount,
$teller->divide(
$taxableAmount,
(1 + $taxRate->rate)
)
);

$amount = -(float)$teller->multiply($amount, $item->qty);
} else {
$taxableAmount = $item->getTaxableSubtotal($taxRate->taxable);
$amount = -($taxableAmount - ($taxableAmount / (1 + $taxRate->rate)));
// amount = taxableAmount - (taxableAmount / (1 + taxRate))
$amount = $teller->subtract(
$taxableAmount,
$teller->divide(
$taxableAmount,
(1 + $taxRate->rate)
)
);

$amount = -(float)$amount;
}
$amount = Currency::round($amount);
$adjustment = $this->_createAdjustment($taxRate);
// We need to display the adjustment that removed the included tax
$adjustment->name = Craft::t('site', $taxRate->name) . ' ' . Craft::t('commerce', 'Removed');
Expand All @@ -186,7 +247,7 @@ private function _getAdjustments(TaxRate $taxRate): array
$objectId = spl_object_hash($item); // We use this ID since some line items are not saved in the DB yet and have no ID.

if (isset($this->_costRemovedByLineItem[$objectId])) {
$this->_costRemovedByLineItem[$objectId] += $amount;
$this->_costRemovedByLineItem[$objectId] = (float)$this->_getTeller()->add($this->_costRemovedByLineItem[$objectId], $amount);
} else {
$this->_costRemovedByLineItem[$objectId] = $amount;
}
Expand All @@ -195,6 +256,7 @@ private function _getAdjustments(TaxRate $taxRate): array
}
}
}

// Return the removed included taxes as discounts.
return $adjustments;
}
Expand Down Expand Up @@ -223,12 +285,12 @@ private function _getAdjustments(TaxRate $taxRate): array

if ($taxRate->taxable === TaxRateRecord::TAXABLE_ORDER_TOTAL_PRICE) {
$orderTaxableAmount = $this->_getOrderTotalTaxablePrice($this->_order);
$orderTaxableAmount += $this->_costRemovedForOrderTotalPrice;
$orderTaxableAmount = (float)$this->_getTeller()->add($orderTaxableAmount, $this->_costRemovedForOrderTotalPrice);
}

if ($taxRate->taxable === TaxRateRecord::TAXABLE_ORDER_TOTAL_SHIPPING) {
$orderTaxableAmount = $this->_order->getTotalShippingCost();
$orderTaxableAmount += $this->_costRemovedForOrderShipping;
$orderTaxableAmount = (float)$this->_getTeller()->add($orderTaxableAmount, $this->_costRemovedForOrderShipping);
}

$orderTax = $this->_getTaxAmount($orderTaxableAmount, $taxRate->rate, $taxRate->include);
Expand All @@ -254,13 +316,30 @@ private function _getAdjustments(TaxRate $taxRate): array
* since the discount adjustments we just added won't be picked up in getTaxableSubtotal()
*/
if ($taxRate->taxable == TaxRateRecord::TAXABLE_PURCHASABLE) {
$purchasableAmount = $item->salePrice - Currency::round($item->getDiscount() / $item->qty);
$purchasableAmount += Currency::round(($this->_costRemovedByLineItem[$objectId] ?? 0) / $item->qty);
$purchasableTax = $this->_getTaxAmount($purchasableAmount, $taxRate->rate, $taxRate->include);
$itemTax = $purchasableTax * $item->qty; //already rounded
// $item->salePrice - Currency::round($item->getDiscount() / $item->qty);
$purchasableAmount = $this->_getTeller()->subtract(
$item->salePrice,
$this->_getTeller()->divide(
$item->getDiscount(),
$item->qty
)
);

$purchasableAmount = $this->_getTeller()->add(
$purchasableAmount,
$this->_getTeller()->divide(
($this->_costRemovedByLineItem[$objectId] ?? 0),
$item->qty
)
);
$purchasableTax = $this->_getTaxAmount((float)$purchasableAmount, $taxRate->rate, $taxRate->include);
$itemTax = $this->_getTeller()->multiply($purchasableTax, $item->qty); //already rounded
} else {
$taxableAmount = $item->getTaxableSubtotal($taxRate->taxable);
$taxableAmount += $this->_costRemovedByLineItem[$objectId] ?? 0;
$taxableAmount = (float)$this->_getTeller()->add(
$taxableAmount,
$this->_costRemovedByLineItem[$objectId] ?? 0
);
$itemTax = $this->_getTaxAmount($taxableAmount, $taxRate->rate, $taxRate->include);
}

Expand Down Expand Up @@ -299,17 +378,16 @@ protected function getTaxRates(?int $storeId = null): Collection
*/
private function _getTaxAmount($taxableAmount, $rate, $included): float
{
$teller = $this->_getTeller();
if (!$included) {
$incTax = $taxableAmount * (1 + $rate);
$incTax = Currency::round($incTax);
$tax = $incTax - $taxableAmount;
$incTax = $teller->multiply($taxableAmount, (1 + $rate));
$tax = $teller->subtract($incTax, $taxableAmount);
} else {
$exTax = $taxableAmount / (1 + $rate);
$exTax = Currency::round($exTax);
$tax = $taxableAmount - $exTax;
$exTax = $teller->divide($taxableAmount, (1 + $rate));
$tax = $teller->subtract($taxableAmount, $exTax);
}

return $tax;
return (float)$tax;
}

/**
Expand Down Expand Up @@ -415,7 +493,13 @@ private function _getOrderTotalTaxablePrice(Order $order): float
$taxAdjustments = $order->getTotalTax();
$includedTaxAdjustments = $order->getTotalTaxIncluded();

return $itemTotal + $allNonIncludedAdjustmentsTotal - ($taxAdjustments + $includedTaxAdjustments);
$totals = (float)$this->_getTeller()->add($itemTotal, $allNonIncludedAdjustmentsTotal);
$adjustments = (float)$this->_getTeller()->add($taxAdjustments, $includedTaxAdjustments);

return (float)$this->_getTeller()->subtract(
$totals,
$adjustments
);
}

/**
Expand All @@ -440,4 +524,14 @@ private function _getTaxAddress(): ?Address

return $address;
}

/**
* @return Teller
* @throws InvalidConfigException
* @since 5.3.0
*/
private function _getTeller(): Teller
{
return Plugin::getInstance()->getCurrencies()->getTeller($this->_order->currency);
}
}
4 changes: 3 additions & 1 deletion src/services/ShippingCategories.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ public function getAllShippingCategoriesAsList(?int $storeId = null): array
*/
public function getShippingCategoryById(int $shippingCategoryId, ?int $storeId = null): ?ShippingCategory
{
return $this->getAllShippingCategories($storeId)->firstWhere('id', $shippingCategoryId);
$shippingCategories = $this->getAllShippingCategories($storeId);
$first = $shippingCategories->firstWhere('id', $shippingCategoryId);
return $first;
}

/**
Expand Down
Loading