Skip to content

Updated Price Model TaxIncl methods & Cart Item TaxIncl properties to actually provide Tax inclusive values.#2411

Open
MacTavish-69 wants to merge 6 commits intolunarphp:1.xfrom
MacTavish-69:patch/tax-incl
Open

Updated Price Model TaxIncl methods & Cart Item TaxIncl properties to actually provide Tax inclusive values.#2411
MacTavish-69 wants to merge 6 commits intolunarphp:1.xfrom
MacTavish-69:patch/tax-incl

Conversation

@MacTavish-69
Copy link
Copy Markdown
Contributor

@MacTavish-69 MacTavish-69 commented Feb 27, 2026

Lunar Current Behavior

  1. Taxes are only accounted for when shipping address is set.

    For most stores, this is at the time of checkout. Logically, if tax is applicable on end user, the prices should be shown tax inclusive. Often, this is a legal requirement.

  2. Lunar internally relies on Default Tax Class & Default Tax Zone.

    When Lunar Internally just uses Default Tax class & Zone, this system causes incorrect behavior when you've some taxed zone and some zones which are NOT taxed. In this case, you'll set the non taxed zone as Default and the incTax methods will result in same non-taxed amount.

  3. Because of 1 & 2, the taxIncl methods often don't actually provide the intended taxIncl prices.

Fixes in PR

The PR approaches this problem at cart level and Price Model level.

Cart Level

We add TaxZone property to Cart model. The typical flow is to find the user country based on IP in middleware, determine the tax zone and set it on the cart. With this, The items added to cart, will correctly give tax incl price when taxInc methods are called.

Price Model Level

When we have Product/Purchasable instead of Cart Line (Item is added to cart), we get the price from prices relation (prices attached to purchasable) and utilize priceIncTax, comparePriceIncTax methods. An optional TaxZone parameter is added so you can get the tax inclusive price as per the zone of your choice. This is useful when you need it for one time for any reason.
Note: You can totally ignore it if you need the prices to be as per the zone set on cart, the PR will fallback to the zone set on cart.

Previous PR: #2406

The previous PR focused strictly on Product/Purchasable display prices. It introduced a taxzone parameter to specific methods, allowing developers to:

  • Identify the user's taxable region manually.
  • Pass that taxzone into price methods to display the correct tax-inclusive amounts.
  • While effective for simple product displays, it did not address the underlying Cart logic.

This PR is a superset of #2406. It retains the functionality mentioned above but expands it by adding a taxZone property directly to the Cart.

IMPORTANT:
Since this PR includes all changes from #2406, please reject/close that PR if you choose to merge this one.

taxIncl methods internally work on default tax class basis. The moment you've more than 1 tax class, it stops working.

Moreover, taxation works on cart when address is set. However, it's a standard use case to display Tax inclusive prices to user when they're from a tax eligible jurisdiction. This use case is not achievable with current way of Lunar.

Hence, we need to have tax information on the cart which can take care of it.
For one time situations where you need tax Incl price for a given zone, you can pass the zone as parameter

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Lunar\Models\TaxZone;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be using the contract here.

* Call `->save()` afterwards to persist it to the database; the `booted()` retrieved-event listener
* will then restore the zone automatically on every subsequent page load.
*/
public function setTaxZone(?TaxZone $taxZone): Cart
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it would cause a breaking change, this should be added to the Cart contract.

public function setTaxZone(?TaxZone $taxZone): Cart
{
$this->taxZone = $taxZone;
$meta = $this->meta ?? new \ArrayObject;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meta column is cast with AsArrayObject::class, so $this->meta is already an ArrayObject instance when the model exists. Creating new ArrayObject as a fallback is only needed for a brand-new unsaved cart where meta was never set.

The issue is that AsArrayObject returns the same object instance on repeated access (it is not cloned), so assigning $this->meta = $meta re-assigns the same reference and may not trigger Eloquent's dirty tracking, meaning a subsequent ->save() call might not persist the meta change. The pattern should use $this->forceFill(['meta' => ...]) to ensure dirty state is marked.

use Lunar\Exceptions\FingerprintMismatchException;
use Lunar\Facades\DB;
use Lunar\Facades\ShippingManifest;
use Lunar\Models\TaxZone;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure we reference the contracts and not concrete classes.


// Cache the TaxZone so that price model taxInc methods accounts for it in computation.
if ($cart->taxZone) {
Blink::put('lunar_cart_tax_zone', $cart->taxZone);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If application code calculates two carts in the same request (e.g. comparing shipping options across accounts, or a queue job processing multiple carts in the same process), the second cart's CalculateLines pass will overwrite the Blink key set by the first, meaning Price::getPriceableTaxRate() will silently use the wrong zone for calls made after the key is overwritten.

Recommendation: Key the Blink entry by cart ID: 'lunar_cart_tax_zone_'.$cart->id, and have Price::getPriceableTaxRate() accept the cart ID as a lookup hint, or pass the zone directly rather than routing it through Blink at all.

@MacTavish-69
Copy link
Copy Markdown
Contributor Author

@alecritson I've seen your review. I'm quite caught up in these days. I'll try to get this on priority. I'll try to make amendments as per the review over the weekend

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants