Skip to content

Commit cee467a

Browse files
committed
fix tax calculation with discount
1 parent 4945687 commit cee467a

File tree

10 files changed

+116
-49
lines changed

10 files changed

+116
-49
lines changed

resources/lang/br/invoice.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'tax' => 'Imposto',
1919
'tax_label' => 'Imposto',
2020
'subtotal_amount' => 'Subtotal',
21+
'subtotal_discounted_amount' => 'Subtotal após o desconto',
2122
'amount' => 'Valor',
2223
'unit_price' => 'Preço unitário',
2324
'quantity' => 'Qtd.',

resources/lang/de/invoice.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'tax' => 'Steuer',
1919
'tax_label' => 'Steuer',
2020
'subtotal_amount' => 'Zwischensumme',
21+
'subtotal_discounted_amount' => 'Zwischensumme nach Rabatt',
2122
'amount' => 'Betrag',
2223
'unit_price' => 'Einzelpreis',
2324
'quantity' => 'Menge',

resources/lang/en/invoice.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'tax' => 'Tax',
1919
'tax_label' => 'Tax',
2020
'subtotal_amount' => 'Subtotal',
21+
'subtotal_discounted_amount' => 'Subtotal After Discount',
2122
'amount' => 'Amount',
2223
'unit_price' => 'Unit price',
2324
'quantity' => 'Qty',

resources/lang/fr/invoice.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'tax' => 'Tax',
1919
'tax_label' => 'Tax',
2020
'subtotal_amount' => 'Sous-total',
21+
'subtotal_discounted_amount' => 'Sous-total après remise',
2122
'amount' => 'Montant',
2223
'unit_price' => 'Prix unitaire',
2324
'quantity' => 'Qté',

resources/lang/pt/invoice.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'tax' => 'Imposto',
1919
'tax_label' => 'Imposto',
2020
'subtotal_amount' => 'Subtotal',
21+
'subtotal_discounted_amount' => 'Subtotal após o desconto',
2122
'amount' => 'Montante',
2223
'unit_price' => 'Preço unitário',
2324
'quantity' => 'Qtd.',

resources/views/default/invoice.blade.php

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,27 +184,43 @@
184184
{{-- empty space --}}
185185
<td class="py-2 pr-2"></td>
186186
<td class="border-b p-2 text-xs" colspan="3">
187-
{{ __('invoices::invoice.subtotal_amount') }}</td>
187+
{{ __('invoices::invoice.subtotal_amount') }}
188+
</td>
188189
<td class="whitespace-nowrap border-b py-2 pl-2 text-right text-xs">
189190
{{ $invoice->formatMoney($invoice->subTotalAmount()) }}
190191
</td>
191192
</tr>
192193

193-
@foreach ($invoice->discounts as $discount)
194+
@if ($invoice->discounts)
195+
@foreach ($invoice->discounts as $discount)
196+
<tr>
197+
{{-- empty space --}}
198+
<td class="py-2 pr-2"></td>
199+
<td class="border-b p-2 text-xs" colspan="3">
200+
{{ __($discount->name) ?? __('invoices::invoice.discount_name') }}
201+
@if ($discount->percent_off)
202+
({{ $discount->formatPercentage($discount->percent_off) }})
203+
@endif
204+
</td>
205+
<td class="whitespace-nowrap border-b py-2 pl-2 text-right text-xs">
206+
{{ $invoice->formatMoney($discount->computeDiscountAmountOn($invoice->subTotalAmount())?->multipliedBy(-1)) }}
207+
</td>
208+
</tr>
209+
@endforeach
210+
194211
<tr>
195212
{{-- empty space --}}
196213
<td class="py-2 pr-2"></td>
197214
<td class="border-b p-2 text-xs" colspan="3">
198-
{{ __($discount->name) ?? __('invoices::invoice.discount_name') }}
199-
@if ($discount->percent_off)
200-
({{ $discount->formatPercentage($discount->percent_off) }})
201-
@endif
215+
{{ __('invoices::invoice.subtotal_discounted_amount') }}
202216
</td>
203217
<td class="whitespace-nowrap border-b py-2 pl-2 text-right text-xs">
204-
{{ $invoice->formatMoney($discount->computeDiscountAmountOn($invoice->subTotalAmount())?->multipliedBy(-1)) }}
218+
{{ $invoice->formatMoney($invoice->subTotalDiscountedAmount()) }}
205219
</td>
206220
</tr>
207-
@endforeach
221+
@endif
222+
223+
208224

209225
@if ($hasTaxes)
210226
<tr>

src/InvoiceDiscount.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ public function computeDiscountAmountOn(Money $amout): Money
3434
return $this->amount_off;
3535
}
3636

37-
if (! is_null($this->percent_off)) {
38-
return $amout->multipliedBy($this->percent_off / 100, RoundingMode::HALF_CEILING);
37+
if ($this->percent_off !== null) {
38+
return $amout->multipliedBy($this->percent_off / 100.0, RoundingMode::HALF_CEILING);
3939
}
4040

4141
return Money::of(0, $amout->getCurrency());

src/Pdf/PdfInvoice.php

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Elegantly\Invoices\Pdf;
66

7+
use Brick\Math\RoundingMode;
78
use Brick\Money\Money;
89
use Carbon\Carbon;
910
use Dompdf\Dompdf;
@@ -101,40 +102,75 @@ public function subTotalAmount(): Money
101102
);
102103
}
103104

104-
public function totalTaxAmount(): Money
105-
{
106-
return array_reduce(
107-
$this->items,
108-
fn ($total, $item) => $total->plus($item->totalTaxAmount()),
109-
Money::of(0, $this->getCurrency())
110-
);
111-
}
112-
113105
public function totalDiscountAmount(): Money
114106
{
115107
if (! $this->discounts) {
116108
return Money::of(0, $this->getCurrency());
117109
}
118110

119-
$subtotal = $this->subTotalAmount();
111+
$amount = $this->subTotalAmount();
120112

121113
return array_reduce(
122114
$this->discounts,
123-
function ($total, $discount) use ($subtotal) {
124-
return $total->plus($discount->computeDiscountAmountOn($subtotal));
115+
function ($total, $discount) use ($amount) {
116+
return $total->plus($discount->computeDiscountAmountOn($amount));
125117
},
126-
Money::of(0, $subtotal->getCurrency()));
118+
Money::of(0, $amount->getCurrency())
119+
);
127120
}
128121

129-
public function totalAmount(): Money
122+
public function subTotalDiscountedAmount(): Money
130123
{
131-
$total = array_reduce(
132-
$this->items,
133-
fn ($total, $item) => $total->plus($item->totalAmount()),
134-
Money::of(0, $this->getCurrency())
135-
);
124+
return $this->subTotalAmount()->minus($this->totalDiscountAmount());
125+
}
136126

137-
return $total->minus($this->totalDiscountAmount());
127+
/**
128+
* After discount and taxes
129+
*/
130+
public function totalTaxAmount(): Money
131+
{
132+
$totalDiscount = $this->totalDiscountAmount();
133+
134+
/**
135+
* Taxes must be calculated on the discounted subtotal.
136+
* Since discounts apply at the invoice level and taxes at the item level,
137+
* we allocate the discount across items before computing taxes.
138+
*/
139+
$allocatedDiscounts = $totalDiscount->allocate(...array_map(
140+
fn ($item) => $item->subTotalAmount()->getMinorAmount()->toInt(),
141+
$this->items
142+
));
143+
144+
$totalTaxAmount = Money::of(0, $this->getCurrency());
145+
146+
foreach ($this->items as $index => $item) {
147+
148+
if ($item->unit_tax) {
149+
/**
150+
* When unit_tax is defined, the amount is considered right
151+
* and the discount is not apply
152+
*/
153+
$itemTaxAmount = $item->unit_tax->multipliedBy($item->quantity);
154+
} elseif ($item->tax_percentage) {
155+
$itemDiscount = $allocatedDiscounts[$index];
156+
157+
$itemTaxAmount = $item->subTotalAmount()
158+
->minus($itemDiscount)
159+
->multipliedBy($item->tax_percentage / 100.0, roundingMode: RoundingMode::HALF_EVEN);
160+
} else {
161+
$itemTaxAmount = Money::of(0, $totalTaxAmount->getCurrency());
162+
}
163+
164+
$totalTaxAmount = $totalTaxAmount->plus($itemTaxAmount);
165+
166+
}
167+
168+
return $totalTaxAmount;
169+
}
170+
171+
public function totalAmount(): Money
172+
{
173+
return $this->subTotalDiscountedAmount()->plus($this->totalTaxAmount());
138174
}
139175

140176
/**

src/Pdf/PdfInvoiceItem.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function totalTaxAmount(): Money
5959
}
6060

6161
if ($this->tax_percentage) {
62-
return $this->subTotalAmount()->multipliedBy($this->tax_percentage / 100, roundingMode: RoundingMode::HALF_EVEN);
62+
return $this->subTotalAmount()->multipliedBy($this->tax_percentage / 100.0, roundingMode: RoundingMode::HALF_EVEN);
6363
}
6464

6565
return Money::ofMinor(0, $this->currency);

tests/Feature/PdfInvoiceTest.php

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,42 @@
55
use Brick\Money\Money;
66
use Elegantly\Invoices\Enums\InvoiceState;
77
use Elegantly\Invoices\Enums\InvoiceType;
8+
use Elegantly\Invoices\InvoiceDiscount;
89
use Elegantly\Invoices\Pdf\PdfInvoice;
910
use Elegantly\Invoices\Pdf\PdfInvoiceItem;
1011

11-
it('computes the right subTotalAmount, totalTaxAmount and totalAmount', function () {
12+
it('computes the right amounts', function ($items, $taxPercentage, $discounPercentage, $subtotalAmount, $totalDiscountAmount, $totalTaxAmount, $totalAmount) {
1213
$pdfInvoice = new PdfInvoice(
1314
type: InvoiceType::Invoice,
1415
state: InvoiceState::Paid,
1516
serial_number: 'FAKE-INVOICE-01',
1617
due_at: now(),
1718
created_at: now(),
18-
);
19-
20-
$pdfInvoice->items = [
21-
new PdfInvoiceItem(
22-
label: 'Item 1',
23-
unit_price: Money::of(110, 'USD'),
24-
unit_tax: Money::of(10, 'USD')
19+
items: array_map(
20+
fn ($item) => new PdfInvoiceItem(
21+
label: 'Item 1',
22+
unit_price: Money::of($item, 'USD'),
23+
tax_percentage: $taxPercentage
24+
),
25+
$items
2526
),
26-
new PdfInvoiceItem(
27-
label: 'Item 1',
28-
unit_price: Money::of(234, 'USD'),
29-
unit_tax: Money::of(12, 'USD')
30-
),
31-
];
27+
discounts: [
28+
new InvoiceDiscount(
29+
percent_off: $discounPercentage
30+
),
31+
]
32+
);
3233

33-
expect($pdfInvoice->subTotalAmount()->getAmount()->toFloat())->toEqual(344);
34-
expect($pdfInvoice->totalTaxAmount()->getAmount()->toFloat())->toEqual(22);
35-
expect($pdfInvoice->totalAmount()->getAmount()->toFloat())->toEqual(366);
36-
});
34+
expect($pdfInvoice->subTotalAmount()->getAmount()->toFloat())->toEqual($subtotalAmount);
35+
expect($pdfInvoice->totalDiscountAmount()->getAmount()->toFloat())->toEqual($totalDiscountAmount);
36+
expect($pdfInvoice->totalTaxAmount()->getAmount()->toFloat())->toEqual($totalTaxAmount);
37+
expect($pdfInvoice->totalAmount()->getAmount()->toFloat())->toEqual($totalAmount);
38+
})->with([
39+
[[100.0], 0.0, 0.0, 100.0, 0.0, 0.0, 100.0],
40+
[[100.0], 20.0, 0.0, 100.0, 0.0, 20.0, 120.0],
41+
[[100.0], 0.0, 10.0, 100.0, 10.0, 0.0, 90.0],
42+
[[100.0], 20.0, 10.0, 100.0, 10.0, 18.0, 108.0],
43+
[[100.0, 50.0], 0.0, 0.0, 150.0, 0.0, 0.0, 150.0],
44+
[[100.0, 50.0], 20.0, 0.0, 150.0, 0.0, 30.0, 180.0],
45+
[[100.0, 50.0], 20.0, 10.0, 150.0, 15.0, 27.0, 162.0],
46+
]);

0 commit comments

Comments
 (0)