Files
mareike/tests/Feature/InvoiceNumberingTest.php

216 lines
8.3 KiB
PHP

<?php
namespace Tests\Feature;
use App\Domains\Event\Actions\CreateEvent\CreateEventCommand;
use App\Domains\Event\Actions\CreateEvent\CreateEventRequest;
use App\Domains\Event\Actions\SignUp\SignUpCommand;
use App\Domains\Event\Actions\SignUp\SignUpRequest;
use App\Enumerations\EatingHabit;
use App\Enumerations\EfzStatus;
use App\Enumerations\FirstAidPermission;
use App\Enumerations\ParticipationFeeType;
use App\Enumerations\ParticipationType;
use App\Enumerations\SwimmingPermission;
use App\Models\Event;
use App\Models\PaymentMethod;
use App\Models\Tenant;
use App\ValueObjects\Amount;
use DateTime;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* Die Rechnungsnummer wird nirgends gespeichert, sondern aus `events.invoice_key` und
* `event_participants.invoice_sequence` zusammengesetzt. Beide Bausteine müssen deshalb stabil sein.
*/
class InvoiceNumberingTest extends TestCase
{
use RefreshDatabase;
private Tenant $wildeMoehre;
protected function setUp(): void
{
parent::setUp();
$this->wildeMoehre = $this->makeTenant('wm', 'Wilde Möhre');
DB::table('participation_fee_types')->insert(['slug' => 'fixed', 'name' => 'Fix']);
EatingHabit::create(['slug' => EatingHabit::EATING_HABIT_VEGAN, 'name' => 'Vegan']);
EatingHabit::create(['slug' => EatingHabit::EATING_HABIT_VEGETARIAN, 'name' => 'Vegetarisch']);
ParticipationType::create(['slug' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT, 'name' => 'Teilnehmende']);
PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]);
EfzStatus::create(['slug' => EfzStatus::EFZ_STATUS_NOT_CHECKED, 'name' => 'Nicht geprüft']);
EfzStatus::create(['slug' => EfzStatus::EFZ_STATUS_NOT_REQUIRED, 'name' => 'Nicht erforderlich']);
SwimmingPermission::create(['slug' => SwimmingPermission::SWIMMING_PERMISSION_ALLOWED, 'name' => 'Darf baden', 'short' => 'Erteilt']);
FirstAidPermission::create(['slug' => FirstAidPermission::FIRST_AID_PERMISSION_ALLOWED, 'name' => 'Zugestimmt', 'description' => '']);
app()->instance('tenant', $this->wildeMoehre);
}
/**
* `create()` liefert nur die übergebenen Attribute zurück; die DB-Defaults (u.a. `tax_liable`) stehen
* erst nach dem Neuladen bereit -- so, wie der Tenant im Betrieb aus der IdentifyTenant-Middleware kommt.
*/
private function makeTenant(string $slug, string $name): Tenant
{
return Tenant::create([
'slug' => $slug,
'name' => $name,
'address_1' => 'Musterweg 1',
'email' => $slug . '@example.com',
'email_finance' => $slug . '-f@example.com',
'url' => $slug . '.test.local',
'account_name' => $name,
'account_iban' => 'DE00',
'account_bic' => 'XY',
'city' => 'Stadt',
'postcode' => '00000',
'invoice_prefix' => strtoupper($slug),
'is_active_local_group' => true,
'has_active_instance' => true,
])->fresh();
}
private function createEvent(string $begin, string $name = 'Lager'): Event
{
$response = new CreateEventCommand(new CreateEventRequest(
name: $name,
location: 'Ort',
postalCode: '00000',
email: 'e@example.com',
begin: new DateTime($begin),
end: new DateTime($begin),
earlyBirdEnd: new DateTime('2026-01-01'),
registrationFinalEnd: new DateTime('2026-01-02'),
earlyBirdEndAmountIncrease: 0,
participationFeeType: ParticipationFeeType::where('slug', 'fixed')->first(),
accountOwner: 'Owner',
accountIban: 'DE00',
payPerDay: false,
))->execute();
$this->assertTrue($response->success);
return $response->event->fresh();
}
/*
|--------------------------------------------------------------------------
| Event-Teil der Nummer
|--------------------------------------------------------------------------
*/
public function test_events_of_the_same_month_are_numbered_consecutively(): void
{
$this->assertSame('WM-V-20260701', $this->createEvent('2026-07-16')->invoice_key);
$this->assertSame('WM-V-20260702', $this->createEvent('2026-07-24')->invoice_key);
// Anderer Monat, eigene Zählung.
$this->assertSame('WM-V-20260801', $this->createEvent('2026-08-03')->invoice_key);
}
public function test_each_tenant_counts_for_itself(): void
{
$this->createEvent('2026-07-16');
app()->instance('tenant', $this->makeTenant('sn', 'Sachsen'));
$this->assertSame('SN-V-20260701', $this->createEvent('2026-07-18')->invoice_key);
}
public function test_invoice_key_survives_moving_the_event_to_another_month(): void
{
$event = $this->createEvent('2026-07-16');
$event->update(['start_date' => '2026-11-05', 'end_date' => '2026-11-07']);
// Eingefroren: eine bereits herausgegebene Rechnung gehört sonst plötzlich zu einer anderen Nummer.
$this->assertSame('WM-V-20260701', $event->fresh()->invoice_key);
}
public function test_new_tenants_default_to_their_uppercased_slug(): void
{
$tenant = Tenant::create([
'slug' => 'fen',
'name' => 'Fen',
'email' => 'fen@example.com',
'email_finance' => 'fen-f@example.com',
'url' => 'fen.test.local',
'account_name' => 'Fen',
'account_iban' => 'DE00',
'account_bic' => 'XY',
'city' => 'Stadt',
'postcode' => '00000',
'is_active_local_group' => true,
'has_active_instance' => true,
])->fresh();
// Ohne gesetztes Präfix greift der Migrations-Default: der großgeschriebene Slug.
$this->assertSame('FEN', $tenant->invoice_prefix);
app()->instance('tenant', $tenant);
$this->assertSame('FEN-V-20260701', $this->createEvent('2026-07-16')->invoice_key);
}
/*
|--------------------------------------------------------------------------
| Teilnehmer-Teil der Nummer
|--------------------------------------------------------------------------
*/
private function signUp(Event $event, string $firstname): void
{
$response = new SignUpCommand(new SignUpRequest(
$event, null, $firstname, 'Muster', null, ParticipationType::PARTICIPATION_TYPE_PARTICIPANT,
$this->wildeMoehre, new DateTime('2000-01-01'), 'Weg 1', null, '00000', 'Stadt',
strtolower($firstname) . '@example.com', '123', null, null, null, null, null, null, null,
'vegan', null, null, false, false, false, false, false,
new DateTime('2026-07-16'), new DateTime('2026-07-16'), 0, 0, null,
new Amount(50.0, 'Euro'), PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION,
))->execute();
$this->assertTrue($response->success, $response->message ?? '');
}
public function test_sequences_are_handed_out_in_order(): void
{
$event = $this->createEvent('2026-07-16');
$this->signUp($event, 'Ada');
$this->signUp($event, 'Bo');
$this->signUp($event, 'Cem');
$this->assertSame([1, 2, 3], $event->participants()->orderBy('id')->pluck('invoice_sequence')->all());
}
public function test_signing_off_does_not_shift_later_sequences(): void
{
$event = $this->createEvent('2026-07-16');
$this->signUp($event, 'Ada');
$this->signUp($event, 'Bo');
// Abmeldung: der Datensatz bleibt bestehen, die Nummer damit vergeben.
$event->participants()->orderBy('id')->first()->update(['unregistered_at' => '2026-07-01']);
$this->signUp($event, 'Cem');
$this->assertSame(3, $event->participants()->orderBy('id')->get()->last()->invoice_sequence);
}
public function test_sequences_are_per_event(): void
{
$first = $this->createEvent('2026-07-16', 'Erstes');
$second = $this->createEvent('2026-07-24', 'Zweites');
$this->signUp($first, 'Ada');
$this->signUp($second, 'Bo');
$this->assertSame(1, $first->participants()->first()->invoice_sequence);
$this->assertSame(1, $second->participants()->first()->invoice_sequence);
}
}