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

Draft
wants to merge 18 commits into
base: 5.3
Choose a base branch
from
Draft
10 changes: 10 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release Notes for Craft Commerce (WIP)

### Fixed

- Fixed a PHP error that could occur when calculating tax totals. ([#3822](https://github.com/craftcms/commerce/issues/3822))

### Store Management
- It is now possible to design card views for Products and Variants. ([#3809](https://github.com/craftcms/commerce/pull/3809))
- Order conditions can now have a “Coupon Code” rule. ([#3776](https://github.com/craftcms/commerce/discussions/3776))
Expand All @@ -16,15 +20,21 @@
- Added an `originalCart` value to `commerce/update-cart` action, for failed ajax responses. ([#430](https://github.com/craftcms/commerce/issues/430))

### Extensibility
- Added `craft\commerce\base\Purchasable::hasInventory()`.
- Added `craft\commerce\base\InventoryItemTrait`.
- Added `craft\commerce\base\InventoryLocationTrait`.
- Added `craft\commerce\elements\Purchasable::$allowOutOfStockPurchases`.
- Added `craft\commerce\elements\Purchasable::getIsOutOfStockPurchasingAllowed()`.
- Added `craft\commerce\elements\conditions\orders\CouponCodeConditionRule`.
- Added `craft\commerce\elements\conditions\variants\ProductConditionRule`.
- Added `craft\commerce\elements\db\OrderQuery::$couponCode`.
- Added `craft\commerce\elements\db\OrderQuery::couponCode()`.
- Added `craft\commerce\events\CartPurgeEvent`.
- Added `craft\commerce\events\PurchasableOutOfStockPurchasesAllowedEvent`.
- Added `craft\commerce\services\Inventory::updateInventoryLevel()`.
- Added `craft\commerce\services\Inventory::updatePurchasableInventoryLevel()`.
- Added `craft\commerce\services\Purchasables::EVENT_PURCHASABLE_OUT_OF_STOCK_PURCHASES_ALLOWED`.
- Added `craft\commerce\services\Purchasables::isPurchasableOutOfStockPurchasingAllowed()`.

### System
- Craft Commerce now requires Craft CMS 5.5 or later.
11 changes: 10 additions & 1 deletion example-templates/dist/shop/products/_includes/grid.twig
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,16 @@
checked: loop.first,
class: not variant.getIsAvailable() ? 'opacity-10' : '',
disabled: not variant.availableForPurchase,
}) }}<span class="pl-1">{{ variant.sku }} {% if variant.inventoryTracked %}({{ variant.stock ? variant.stock ~ ' available' : 'out of stock'}}){% endif %}</span><span class="ml-auto">{% if variant.onPromotion %} <del class="text-gray-400 text-xs">{{ variant.price|currency(cart.currency) }}</del>{% endif %} {{ variant.salePrice|currency(cart.currency) }}</span>
}) }}
<span class="pl-1">{{ variant.sku }}
{% if variant.hasInventory and variant.inventoryTracked %}
({{ variant.stock ? variant.stock ~ ' available' : 'out of stock'}})
{% if variant.allowOutOfStockPurchases %}
<span class="text-xs text-gray-400">{{ "Continue selling when out of stock."|t('commerce') }}</span>
{% endif %}
{% endif %}
</span>
<span class="ml-auto">{% if variant.onPromotion %} <del class="text-gray-400 text-xs">{{ variant.price|currency(cart.currency) }}</del>{% endif %} {{ variant.salePrice|currency(cart.currency) }}</span>
</label>
{% endfor %}
</div>
Expand Down
11 changes: 10 additions & 1 deletion example-templates/src/shop/products/_includes/grid.twig
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,16 @@
checked: loop.first,
class: not variant.getIsAvailable() ? 'opacity-10' : '',
disabled: not variant.availableForPurchase,
}) }}<span class="pl-1">{{ variant.sku }} {% if variant.inventoryTracked %}({{ variant.stock ? variant.stock ~ ' available' : 'out of stock'}}){% endif %}</span><span class="ml-auto">{% if variant.onPromotion %} <del class="text-gray-400 text-xs">{{ variant.price|currency(cart.currency) }}</del>{% endif %} {{ variant.salePrice|currency(cart.currency) }}</span>
}) }}
<span class="pl-1">{{ variant.sku }}
{% if variant.hasInventory and variant.inventoryTracked %}
({{ variant.stock ? variant.stock ~ ' available' : 'out of stock'}})
{% if variant.allowOutOfStockPurchases %}
<span class="text-xs text-gray-400">{{ "Continue selling when out of stock."|t('commerce') }}</span>
{% endif %}
{% endif %}
</span>
<span class="ml-auto">{% if variant.onPromotion %} <del class="text-gray-400 text-xs">{{ variant.price|currency(cart.currency) }}</del>{% endif %} {{ variant.salePrice|currency(cart.currency) }}</span>
</label>
{% endfor %}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ public static function editions(): array
/**
* @inheritDoc
*/
public string $schemaVersion = '5.2.9.1';
public string $schemaVersion = '5.3.0.2';

/**
* @inheritdoc
Expand Down
112 changes: 93 additions & 19 deletions src/adjusters/Tax.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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 @@ -78,12 +79,47 @@
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 order shipping
* 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 positive number');
}

$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 positive number');
}

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

Check failure on line 120 in src/adjusters/Tax.php

View workflow job for this annotation

GitHub Actions / ci / Code Quality / PHPStan / PHPStan

Property craft\commerce\adjusters\Tax::$_costRemovedForOrderTotalPrice (float) does not accept string.
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -125,6 +161,7 @@
{
$adjustments = [];
$hasValidEuVatId = false;
$teller = $this->_getTeller();

$zoneMatches = $taxRate->getIsEverywhere() || ($taxRate->getTaxZone() && $this->_matchAddress($taxRate->getTaxZone()));

Expand All @@ -136,7 +173,7 @@
$removeDueToVat = ($zoneMatches && $hasValidEuVatId && $taxRate->removeVatIncluded);
if ($removeIncluded || $removeDueToVat) {

// 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 @@ -146,41 +183,68 @@
$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)
)
);

// make amount negative
$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)
)
);

// make amount negative
$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');
$adjustment->amount = $amount;
$adjustment->amount = -$amount;
$adjustment->setLineItem($item);
$adjustment->type = 'discount';
$adjustment->included = false;
Expand All @@ -197,6 +261,7 @@
}
}
}

// Return the removed included taxes as discounts.
return $adjustments;
}
Expand Down Expand Up @@ -301,17 +366,16 @@
*/
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 @@ -433,4 +497,14 @@

return $address;
}

/**
* @return Teller
* @throws InvalidConfigException
* @since 5.3.0
*/
private function _getTeller(): Teller
{
return Plugin::getInstance()->getCurrencies()->getTeller($this->_order->currency);
}
}
Loading
Loading