99 lines
4.0 KiB
PHP
99 lines
4.0 KiB
PHP
<?php
|
|
|
|
use App\Models\InvoiceSeries;
|
|
use App\Services\Billing\InvoiceNumbers;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Gapless and ascending, which is a legal requirement rather than a preference.
|
|
*
|
|
* All of this exists because the obvious implementation — "the highest number
|
|
* that exists, plus one" — hands the same number to two sales that land
|
|
* together, and hands a used number back out after a row is removed. A number
|
|
* that has already been printed on a document somebody holds.
|
|
*/
|
|
it('hands out the seeded series in order', function () {
|
|
$s = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
|
|
|
|
$numbers = [];
|
|
|
|
foreach (range(1, 3) as $ignored) {
|
|
DB::transaction(function () use ($s, &$numbers) {
|
|
[$number] = app(InvoiceNumbers::class)->next($s, 2026);
|
|
$numbers[] = $number;
|
|
});
|
|
}
|
|
|
|
expect($numbers)->toBe(['RE-2026-0001', 'RE-2026-0002', 'RE-2026-0003']);
|
|
});
|
|
|
|
it('restarts a yearly series in a new year, and only then', function () {
|
|
// Checked against the stored year, not against "is it January": an
|
|
// installation that issued nothing for fourteen months must still restart,
|
|
// and one issuing at 23:59 on New Year's Eve must not restart the invoice
|
|
// that follows a minute later under the old year.
|
|
$s = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
|
|
|
|
DB::transaction(fn () => app(InvoiceNumbers::class)->next($s, 2026));
|
|
DB::transaction(fn () => app(InvoiceNumbers::class)->next($s, 2026));
|
|
|
|
[$next] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($s->refresh(), 2027));
|
|
|
|
expect($next)->toBe('RE-2027-0001');
|
|
|
|
[$again] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($s->refresh(), 2027));
|
|
|
|
expect($again)->toBe('RE-2027-0002');
|
|
});
|
|
|
|
it('never restarts a series that does not reset yearly', function () {
|
|
$s = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
|
|
$s->update(['yearly_reset' => false, 'next_number' => 41]);
|
|
|
|
[$number] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($s->refresh(), 2026));
|
|
expect($number)->toBe('RE-0041');
|
|
|
|
[$across] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($s->refresh(), 2027));
|
|
expect($across)->toBe('RE-0042');
|
|
});
|
|
|
|
it('keeps every Rechnungskreis on its own counter', function () {
|
|
// A credit note is not an invoice with a minus sign in front of it, and
|
|
// sharing a counter would put gaps in both.
|
|
$invoice = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
|
|
$credit = InvoiceSeries::query()->where('kind', 'credit_note')->firstOrFail();
|
|
|
|
[$a] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($invoice, 2026));
|
|
[$b] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($credit, 2026));
|
|
[$c] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($invoice->refresh(), 2026));
|
|
|
|
expect($a)->toBe('RE-2026-0001')
|
|
->and($b)->toBe('GS-2026-0001')
|
|
->and($c)->toBe('RE-2026-0002');
|
|
});
|
|
|
|
/*
|
|
* There is deliberately no test for the "must be inside a transaction" guard.
|
|
*
|
|
* RefreshDatabase wraps every test in one, so DB::transactionLevel() is already
|
|
* above zero before the first line of any test here runs — the guard cannot
|
|
* fire, and a test written to prove it does would be green for a reason that
|
|
* has nothing to do with the guard. A test that passes for the wrong reason is
|
|
* worse than no test, so this note stands in its place.
|
|
*/
|
|
|
|
it('starts every seeded Rechnungskreis at one', function () {
|
|
// An installation with no series cannot issue an invoice at all, and
|
|
// finding that out at the moment of the first sale is the worst possible
|
|
// time.
|
|
//
|
|
// toEqual, not toBe: pluck returns them in whatever order the database
|
|
// hands them back, and the order of four seeded rows is not the assertion.
|
|
expect(InvoiceSeries::query()->pluck('prefix', 'kind')->all())->toEqual([
|
|
'invoice' => 'RE',
|
|
'credit_note' => 'GS',
|
|
'cancellation' => 'ST',
|
|
'deposit' => 'AZ',
|
|
]);
|
|
});
|