132 lines
5.6 KiB
PHP
132 lines
5.6 KiB
PHP
<?php
|
||
|
||
use App\Services\Billing\InvoiceMath;
|
||
|
||
/**
|
||
* The arithmetic, which on an invoice is the whole product.
|
||
*
|
||
* A document that does not add up is not a rounding artefact, it is a document
|
||
* that does not add up — and the customer's calculator is what finds it.
|
||
*/
|
||
it('multiplies a fractional quantity without losing a cent', function () {
|
||
// 12,5 hours at 100,00 €
|
||
expect(InvoiceMath::lineNet(10000, 12500))->toBe(125000);
|
||
|
||
// 0,25 months at 19,00 € — 4,75, not 4,74 and not 4,76
|
||
expect(InvoiceMath::lineNet(1900, 250))->toBe(475);
|
||
});
|
||
|
||
it('applies a percentage to the sum, not line by line', function () {
|
||
// Three lines at 3,33 € with 10 % off. Rounded per line, each discount is
|
||
// 33 cents and the total is 99 — but 10 % of 9,99 € is 100 cents. One cent,
|
||
// on the line the customer checks first.
|
||
$lines = [
|
||
['unit_net_cents' => 333, 'quantity_milli' => 1000],
|
||
['unit_net_cents' => 333, 'quantity_milli' => 1000],
|
||
['unit_net_cents' => 333, 'quantity_milli' => 1000],
|
||
];
|
||
|
||
$totals = InvoiceMath::totals($lines, ['type' => InvoiceMath::PERCENT, 'value' => -1000], 2000);
|
||
|
||
expect($totals['lines_net'])->toBe(999)
|
||
->and($totals['adjustment'])->toBe(-100)
|
||
->and($totals['net'])->toBe(899);
|
||
});
|
||
|
||
it('treats a surcharge as the same operation with the other sign', function () {
|
||
// One rule, not two flags somebody has to remember the meaning of.
|
||
$lines = [['unit_net_cents' => 10000, 'quantity_milli' => 1000]];
|
||
|
||
$discounted = InvoiceMath::totals($lines, ['type' => InvoiceMath::PERCENT, 'value' => -2000], 2000);
|
||
$surcharged = InvoiceMath::totals($lines, ['type' => InvoiceMath::PERCENT, 'value' => 2000], 2000);
|
||
|
||
expect($discounted['net'])->toBe(8000)
|
||
->and($surcharged['net'])->toBe(12000);
|
||
});
|
||
|
||
it('takes a fixed amount off as readily as a percentage', function () {
|
||
$lines = [['unit_net_cents' => 10000, 'quantity_milli' => 1000]];
|
||
|
||
$totals = InvoiceMath::totals($lines, ['type' => InvoiceMath::AMOUNT, 'value' => -1500], 2000);
|
||
|
||
expect($totals['net'])->toBe(8500)
|
||
->and($totals['tax'])->toBe(1700)
|
||
->and($totals['gross'])->toBe(10200);
|
||
});
|
||
|
||
it('adds up: net plus tax is gross, on the figures actually printed', function () {
|
||
// The assertion that matters, because it is the one a reader performs.
|
||
$lines = [
|
||
['unit_net_cents' => 10000, 'quantity_milli' => 12000],
|
||
['unit_net_cents' => 31200, 'quantity_milli' => 20000],
|
||
];
|
||
|
||
$totals = InvoiceMath::totals($lines, ['type' => InvoiceMath::PERCENT, 'value' => -2000], 2000);
|
||
|
||
expect($totals['lines_net'])->toBe(744000)
|
||
->and($totals['adjustment'])->toBe(-148800)
|
||
->and($totals['net'])->toBe(595200)
|
||
->and($totals['tax'])->toBe(119040)
|
||
->and($totals['gross'])->toBe(714240)
|
||
->and($totals['net'] + $totals['tax'])->toBe($totals['gross']);
|
||
});
|
||
|
||
it('derives gross from net and never the other way round', function () {
|
||
// 19,99 € net at 20 % is 23,99 €. Taking VAT back out of 23,99 gives 19,99
|
||
// here and 19,992 elsewhere — which is why only one direction is offered.
|
||
$totals = InvoiceMath::totals([['unit_net_cents' => 1999, 'quantity_milli' => 1000]], null, 2000);
|
||
|
||
expect($totals['tax'])->toBe(400)
|
||
->and($totals['gross'])->toBe(2399);
|
||
});
|
||
|
||
it('formats money and quantities the way the document reads them', function () {
|
||
expect(InvoiceMath::money(714240))->toBe('7.142,40')
|
||
->and(InvoiceMath::money(-14880))->toBe('-148,80')
|
||
->and(InvoiceMath::money(0))->toBe('0,00');
|
||
|
||
// "12,000 Std." reads as a defect.
|
||
expect(InvoiceMath::quantity(12000))->toBe('12')
|
||
->and(InvoiceMath::quantity(1500))->toBe('1,5')
|
||
->and(InvoiceMath::quantity(250))->toBe('0,25');
|
||
});
|
||
|
||
it('puts a line’s discount inside that line, so the columns add up', function () {
|
||
// The reason it moved off the summary block. With one discount under the
|
||
// table, the gross column summed to more than the total — a document that
|
||
// does not agree with itself, and no way for the reader to know which
|
||
// figure is the real one.
|
||
$lines = [
|
||
['unit_net_cents' => 1900, 'quantity_milli' => 1000],
|
||
['unit_net_cents' => 9000, 'quantity_milli' => 2500, 'adjustment' => ['type' => InvoiceMath::PERCENT, 'value' => -1000]],
|
||
];
|
||
|
||
$columnNet = 0;
|
||
$columnGross = 0;
|
||
|
||
foreach ($lines as $line) {
|
||
$net = InvoiceMath::lineTotal($line['unit_net_cents'], $line['quantity_milli'], $line['adjustment'] ?? null);
|
||
$columnNet += $net;
|
||
$columnGross += $net + InvoiceMath::tax($net, 2000);
|
||
}
|
||
|
||
$totals = InvoiceMath::totals($lines, null, 2000);
|
||
|
||
expect($columnNet)->toBe($totals['net'])
|
||
->and($columnGross)->toBe($totals['gross'])
|
||
// 225,00 less 10 % is 202,50; plus 19,00 is 221,50.
|
||
->and($totals['net'])->toBe(22150);
|
||
});
|
||
|
||
it('labels an adjustment so the words cannot disagree with the arithmetic', function () {
|
||
expect(InvoiceMath::adjustmentLabel(['type' => InvoiceMath::PERCENT, 'value' => -1000]))
|
||
->toBe(__('invoice.discount').' 10 %')
|
||
->and(InvoiceMath::adjustmentLabel(['type' => InvoiceMath::PERCENT, 'value' => 1550]))
|
||
->toBe(__('invoice.surcharge').' 15,5 %')
|
||
->and(InvoiceMath::adjustmentLabel(['type' => InvoiceMath::AMOUNT, 'value' => -2500]))
|
||
->toBe(__('invoice.discount').' 25,00')
|
||
// Nothing at all rather than "Rabatt 0,00", which reads as a mistake.
|
||
->and(InvoiceMath::adjustmentLabel(['type' => InvoiceMath::AMOUNT, 'value' => 0]))->toBeNull()
|
||
->and(InvoiceMath::adjustmentLabel(null))->toBeNull();
|
||
});
|