CluPilotCloud/tests/Feature/Billing/InvoiceMathTest.php

132 lines
5.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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 lines 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();
});