Incorrect VAT calculation

Prepare to have your brain melted.

Image

Let's dive into the code...

InvoiceItem:

    public function total($includeVat = false): int|float
    {
        $this->load(relations: 'vatType');

        if ($includeVat) {
            return $this->price + ($this->price * ($this->vatType->percentage / 100));
        }

        return $this->price;
    }

InvoiceItemResource:

            'price_excl_vat' => Money::EUR($this->total()),
            'price_incl_vat' => Money::EUR($this->total(includeVat: true)),

Invoice:

    public function total(bool $includeVat = false): int|float
    {
        return collect(value: $this->invoiceItems)
            ->sum(callback: function ($invoiceItem) use ($includeVat) {
                if ($includeVat) {
                    $vatType = VatType::find(id: $invoiceItem->vat_type_id);

                    return $invoiceItem->quantity * ($invoiceItem->price + ($invoiceItem->price * $vatType->percentage / 100));
                }

                return $invoiceItem->price * $invoiceItem->quantity;
            });
    }

InvoiceResource:

            'total_excl_vat' => Money::EUR($this->total()),
            'total_incl_vat' => Money::EUR($this->total(includeVat: true)),

Prices are stored in cents.

VAT percentages are stored like so: 0, 10, 14, 24, etc

    protected function price(): Attribute
    {
        return Attribute::make(
            set: fn($value) => $value * 100,
        );
    }

It's something stupid. Doing the exact same calculation on a Calculator with the same values works fine. I don't see what I am missing. I figured a fresh pair of eyes would help!

Using this package to display the formatted values.

https://github.com/cknow/laravel-money

PS: I don't love the return types on those methods, but I am not sure if casting to an int and returning would be better.

Yes! That due date is very suspicious...

Haz
Haz
Moderator
0
7
647
alex
alex
Moderator

I had the exact same thing happen after an update to the cknow/laravel-money package. Here's what I changed in the Plan model here.

From:

$price = money($price->multiply($this->couponDiscountPercentage())->divide(100), $this->currency);

To:

$price = money($price->multiply(100 - $this->couponDiscountPercentage())->divide(100), $this->currency);

To be honest I didn't really dig into what actually happened with the package. Hope this helps?

Haz
Haz
Moderator

How would that affect the calculation here though? There’s no get attribute. All of the calculations are done in cents. That package is only used in the resource.

Here are the values in cents. I think they are also incorrect. I'll double check them later though. Currently in the middle of something.

Image

alex
alex
Moderator

From your screenshot you posted, is it just the 'price' column that's wrong? Sorry I'm so tired, I can barely concentrate.

Haz
Haz
Moderator

The values in cents are correct. The item row isn't taking into account quantity but that's not an issue. I just need to multiply by it. It seems the issue is package related after all.

Gotta figure out why the package is causing such issues.

Haz
Haz
Moderator

Fixed!

Image

It now also takes into account the quantity.

Don't really understand it, but the issue seems to be related to floats.

Invoice:


    public function total(bool $includeVat = false): int
    {
        return collect($this->invoiceItems)
            ->sum(callback: function ($invoiceItem) use ($includeVat) {
                if ($includeVat) {
                    $vatType = VatType::find(id: $invoiceItem->vat_type_id);

                    return intval(value: $invoiceItem->quantity * ($invoiceItem->price + ($invoiceItem->price * $vatType->percentage / 100)));
                }

                return intval(value: $invoiceItem->price * $invoiceItem->quantity);
            });
    }

InvoiceItem:

    public function total($includeVat = false): int
    {
        $this->load(relations: 'vatType');

        if ($includeVat) {
            return intval(value: ($this->price + $this->price * ($this->vatType->percentage / 100)) * $this->quantity);
        }

        return intval(value: $this->price * $this->quantity);
    }

Resources remain unchanged.

whoami (Daryl)
whoami (Daryl)

Thanks, for sharing!

Haz
Haz
Moderator
Solution

Just an update.

There were so many mistakes in the code above that I'm seeing. I am in the process of writing tests for prices and now I am seeing them all.

Don't get me wrong. I think it's a great package, and I think all of the issues could most likely be resolved if I were to spend some time reading through the documentation again. It's strange because I don't recall having any of these issues when I was using it prior to this project, though, it's been some time.

Results (image)

Key points:

  • Don't use intval
  • Allow integers and floats to be returned

I think I'm going to swap it out with my own simple implementation of a Money class. We don't need all of the features that this package offers. Simplicity is definitely the way to go.

I have updated the best answer to reflect these results.