135 lines
4.9 KiB
PHP
135 lines
4.9 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\EditInvoiceSeries;
|
|
use App\Livewire\Admin\Finance;
|
|
use App\Models\Invoice;
|
|
use App\Models\InvoiceSeries;
|
|
use App\Support\CompanyProfile;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* The details that end up on a legal document, and the parts of an invoice
|
|
* series that stop being editable once one has been issued.
|
|
*/
|
|
it('refuses to consider itself ready without the fields an invoice legally needs', function () {
|
|
// An invoice without a name, an address or a VAT number is not a valid
|
|
// invoice in Austria. Issuing one is worse than refusing to.
|
|
expect(CompanyProfile::missingForInvoicing())
|
|
->toContain('name')->toContain('address')->toContain('vat_id');
|
|
|
|
CompanyProfile::put([
|
|
'name' => 'CluPilot Cloud e.U.',
|
|
'address' => 'Musterstraße 1',
|
|
'postcode' => '1010',
|
|
'city' => 'Wien',
|
|
'vat_id' => 'ATU12345678',
|
|
]);
|
|
|
|
expect(CompanyProfile::missingForInvoicing())->toBe([]);
|
|
});
|
|
|
|
it('stores the company details from the finance tab', function () {
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Finance::class)
|
|
->set('company.name', 'CluPilot Cloud e.U.')
|
|
->set('company.address', 'Musterstraße 1')
|
|
->set('company.postcode', '1010')
|
|
->set('company.city', 'Wien')
|
|
->set('company.country', 'Österreich')
|
|
->set('company.vat_id', 'ATU12345678')
|
|
->set('company.payment_days', 14)
|
|
->set('taxRate', 20)
|
|
->call('saveCompany')
|
|
->assertHasNoErrors();
|
|
|
|
expect(CompanyProfile::get('vat_id'))->toBe('ATU12345678')
|
|
->and(CompanyProfile::taxRate())->toBe(20.0);
|
|
});
|
|
|
|
it('drops a field that is not part of the profile', function () {
|
|
// This is fed from a form. A forged key must not be able to write an
|
|
// arbitrary setting into the same table the site-visibility switch uses.
|
|
CompanyProfile::put(['site.public' => false, 'name' => 'Echt']);
|
|
|
|
expect(App\Support\Settings::get('site.public', true))->toBeTrue()
|
|
->and(CompanyProfile::get('name'))->toBe('Echt');
|
|
});
|
|
|
|
it('lets a series be renamed and its prefix corrected while nothing has been issued', function () {
|
|
$series = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(EditInvoiceSeries::class, ['uuid' => $series->uuid])
|
|
->set('name', 'Ausgangsrechnung')
|
|
->set('prefix', 'ar')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect($series->refresh()->prefix)->toBe('AR')
|
|
->and($series->name)->toBe('Ausgangsrechnung');
|
|
});
|
|
|
|
it('freezes the prefix once a document carries it', function () {
|
|
// The prefix is part of every number already issued. Changing it renames
|
|
// nothing and leaves a series whose past says RE and whose future says AR.
|
|
$series = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
|
|
issueOne($series);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(EditInvoiceSeries::class, ['uuid' => $series->uuid])
|
|
->set('prefix', 'AR')
|
|
->set('name', 'Ausgangsrechnung')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect($series->refresh()->prefix)->toBe('RE')
|
|
// The rest of the form still saves.
|
|
->and($series->name)->toBe('Ausgangsrechnung');
|
|
});
|
|
|
|
it('will not let the counter be wound back onto a number already issued', function () {
|
|
// Gapless AND ascending. A lower number is one that is already printed on
|
|
// a document somebody holds.
|
|
$series = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
|
|
$series->update(['next_number' => 25]);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(EditInvoiceSeries::class, ['uuid' => $series->uuid])
|
|
->set('nextNumber', 7)
|
|
->call('save')
|
|
->assertHasErrors(['nextNumber']);
|
|
|
|
expect($series->refresh()->next_number)->toBe(25);
|
|
});
|
|
|
|
it('does let the counter be moved forward', function () {
|
|
// Skipping numbers is lawful — reusing them is not. An operator migrating
|
|
// from another system needs exactly this.
|
|
$series = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(EditInvoiceSeries::class, ['uuid' => $series->uuid])
|
|
->set('nextNumber', 500)
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect($series->refresh()->next_number)->toBe(500);
|
|
});
|
|
|
|
/** A minimal issued document, enough to make a series non-empty. */
|
|
function issueOne(InvoiceSeries $series): Invoice
|
|
{
|
|
return Invoice::create([
|
|
'invoice_series_id' => $series->id,
|
|
'number' => $series->prefix.'-2026-0001',
|
|
'number_year' => 2026,
|
|
'number_sequence' => 1,
|
|
'issued_on' => now()->toDateString(),
|
|
'snapshot' => ['issuer' => [], 'lines' => []],
|
|
'net_cents' => 1900,
|
|
'tax_cents' => 380,
|
|
'gross_cents' => 2280,
|
|
'currency' => 'EUR',
|
|
]);
|
|
}
|