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', ]); } it('decides VAT in one place, so the invoice cannot disagree with the checkout', function () { // Two sources for one number is how a document ends up contradicting the // page that produced it. The Finance tab is the source; config is only the // fallback until a rate has been saved. config(['provisioning.tax.rate_percent' => 20]); expect(App\Support\CompanyProfile::taxRate())->toBe(20.0) ->and(App\Services\Billing\TaxTreatment::for(null)->rate)->toBe(0.2); App\Support\Settings::set('company.tax_rate', 10.0); expect(App\Support\CompanyProfile::taxRate())->toBe(10.0) ->and(App\Services\Billing\TaxTreatment::for(null)->rate)->toBe(0.1); });