Fehler in Abrechnungsende behoben

This commit is contained in:
2026-09-11 11:26:35 +02:00
parent 328825293a
commit ef6284d5cc
11 changed files with 284 additions and 19 deletions
@@ -0,0 +1,184 @@
<?php
namespace Tests\Feature;
use App\Domains\CostUnit\Actions\ChangeCostUnitDetails\ChangeCostUnitDetailsCommand;
use App\Domains\CostUnit\Actions\ChangeCostUnitDetails\ChangeCostUnitDetailsRequest;
use App\Enumerations\CostUnitType;
use App\Enumerations\UserRole;
use App\Models\CostUnit;
use App\Models\Tenant;
use App\Models\User;
use App\Tasks\CloseCostUnit;
use App\ValueObjects\Amount;
use DateTime;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* Der Abrechnungsschluss einer Kostenstelle.
*
* Er ist ein Tag, keine Uhrzeit: Abrechnungen dürfen bis einschließlich 23:59 des Stichtags eingereicht werden. Das
* Modal „Details anpassen" muss ihn anzeigen -- sonst löscht jedes Speichern den Wert unbemerkt.
*/
class CostUnitBillingDeadlineTest extends TestCase
{
use RefreshDatabase;
private Tenant $tenant;
protected function setUp(): void
{
parent::setUp();
$this->tenant = Tenant::create([
'slug' => 'wm',
'name' => 'Wilde Möhre',
'address_1' => 'Musterweg 1',
'email' => 't@example.com',
'email_finance' => 'finance@example.com',
'url' => parse_url(config('app.url'), PHP_URL_HOST),
'account_name' => 'Test e.V.',
'account_iban' => 'DE00',
'account_bic' => 'XY',
'city' => 'Stadt',
'postcode' => '00000',
'invoice_prefix' => 'WM',
'is_active_local_group' => true,
'has_active_instance' => true,
]);
app()->instance('tenant', $this->tenant);
DB::table('cost_unit_types')->insert(['slug' => CostUnitType::COST_UNIT_TYPE_EVENT, 'name' => 'Veranstaltung']);
foreach ([UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_USER] as $role) {
UserRole::create(['slug' => $role, 'name' => $role]);
}
}
public function test_details_liefern_den_abrechnungsschluss_im_format_des_datumsfelds(): void
{
$costUnit = $this->makeCostUnit('2026-09-11');
$this->actingAs($this->makeAdmin());
$response = $this->getJson('/api/v1/cost-unit/' . $costUnit->id . '/details');
$response->assertOk();
$this->assertSame('2026-09-11', $response->json('costUnit.billingDeadline'));
}
public function test_details_ohne_abrechnungsschluss_liefern_null(): void
{
$costUnit = $this->makeCostUnit(null);
$this->actingAs($this->makeAdmin());
$response = $this->getJson('/api/v1/cost-unit/' . $costUnit->id . '/details');
$this->assertNull($response->json('costUnit.billingDeadline'));
}
public function test_speichern_uebernimmt_das_datum_aus_dem_formular(): void
{
$costUnit = $this->makeCostUnit('2026-09-11');
$this->actingAs($this->makeAdmin());
$this->postJson('/api/v1/cost-unit/' . $costUnit->id . '/details', ['formData' => [
'billingDeadline' => '2026-09-30',
'mailOnNew' => true,
'distanceAllowance' => '0,30',
]])->assertOk()->assertJson(['status' => 'success']);
$this->assertSame('2026-09-30', $costUnit->fresh()->billing_deadline);
}
public function test_ein_geleertes_datumsfeld_entfernt_den_abrechnungsschluss_ohne_fehler(): void
{
$costUnit = $this->makeCostUnit('2026-09-11');
$this->actingAs($this->makeAdmin());
$this->postJson('/api/v1/cost-unit/' . $costUnit->id . '/details', ['formData' => [
'billingDeadline' => '',
'mailOnNew' => false,
'distanceAllowance' => '0,25',
]])->assertOk()->assertJson(['status' => 'success']);
$this->assertNull($costUnit->fresh()->billing_deadline);
}
public function test_eine_uhrzeit_im_request_wird_nicht_gespeichert(): void
{
$costUnit = $this->makeCostUnit(null);
new ChangeCostUnitDetailsCommand(new ChangeCostUnitDetailsRequest(
$costUnit,
Amount::fromString('0,25'),
false,
new DateTime('2026-09-11 14:23:45'),
))->execute();
$this->assertSame('2026-09-11', $costUnit->fresh()->billing_deadline);
}
public function test_am_stichtag_um_2359_ist_die_kostenstelle_noch_offen(): void
{
$costUnit = $this->makeCostUnit('2026-09-11');
$this->travelTo(new DateTime('2026-09-11 23:59:00'));
new CloseCostUnit()->handle();
$this->assertTrue((bool) $costUnit->fresh()->allow_new);
}
public function test_am_folgetag_schliesst_der_cron_die_kostenstelle(): void
{
$costUnit = $this->makeCostUnit('2026-09-11');
$this->travelTo(new DateTime('2026-09-12 00:05:00'));
new CloseCostUnit()->handle();
$this->assertFalse((bool) $costUnit->fresh()->allow_new);
}
public function test_ohne_abrechnungsschluss_setzt_der_cron_sechs_wochen_und_laesst_offen(): void
{
$costUnit = $this->makeCostUnit(null);
$this->travelTo(new DateTime('2026-09-12 00:05:00'));
new CloseCostUnit()->handle();
$costUnit = $costUnit->fresh();
$this->assertSame('2026-10-24', $costUnit->billing_deadline);
$this->assertTrue((bool) $costUnit->allow_new);
}
private function makeCostUnit(?string $billingDeadline): CostUnit
{
return CostUnit::create([
'tenant' => $this->tenant->slug,
'name' => 'Sommerlager',
'type' => CostUnitType::COST_UNIT_TYPE_EVENT,
'billing_deadline' => $billingDeadline,
'distance_allowance' => 0.25,
'mail_on_new' => false,
'allow_new' => true,
'archived' => false,
]);
}
private function makeAdmin(): User
{
return User::create([
'username' => 'admin-' . uniqid() . '@example.com',
'email' => 'admin-' . uniqid() . '@example.com',
'firstname' => 'Test',
'lastname' => 'Person',
'password' => bcrypt('secret'),
'local_group' => $this->tenant->slug,
'user_role_main' => UserRole::USER_ROLE_USER,
'user_role_local_group' => UserRole::USER_ROLE_ADMIN,
'active' => true,
]);
}
}
+36 -1
View File
@@ -183,12 +183,47 @@ class InvoicePurposeTest extends TestCase
public function test_travel_costs_without_travellers_record_only_the_reason(): void
{
// Ohne Login steht im Formular niemand drin -- das darf den Beleg nicht aufhalten.
// Der Command selbst verlangt keine Personen: Abrechnungskorrekturen laufen ohne sie durch. Die
// Pflicht beim Einreichen prüft der SaveInvoiceController -- siehe die Tests unten.
$invoice = $this->submit(InvoiceType::INVOICE_TYPE_TRAVELLING, travelReason: 'Landeslager');
$this->assertSame('Landeslager', $invoice->purpose);
}
/**
* Eine Fahrtkostenabrechnung über das Formular, als Kilometerpauschale ohne Beleg.
*/
private function postTravelExpense(CostUnit $costUnit, ?string $travellers): \Illuminate\Testing\TestResponse
{
return $this->postJson('/api/v1/invoice/new/' . $costUnit->id . '/' . InvoiceType::INVOICE_TYPE_TRAVELLING, [
'name' => 'Max Mustermann',
'amount' => '100',
'otherText' => 'Halle – Leipzig',
'decision' => 'payout',
'travelReason' => 'Landeslager',
'travellers' => $travellers,
]);
}
public function test_submitting_travel_costs_requires_who_travelled(): void
{
$costUnit = $this->makeCostUnit();
foreach ([null, '', ' '] as $travellers) {
$this->postTravelExpense($costUnit, $travellers)
->assertJson(['status' => 'error', 'message' => 'Bitte gib an, welche Personen gereist sind.']);
}
$this->assertSame(0, Invoice::count());
}
public function test_submitting_travel_costs_with_travellers_is_accepted(): void
{
$this->postTravelExpense($this->makeCostUnit(), 'Mika und Kim')->assertJson(['status' => 'success']);
$this->assertSame('Landeslager — Mika und Kim', Invoice::sole()->purpose);
}
public function test_a_refund_records_no_purpose(): void
{
// Beitragserstattungen entstehen ohne Freitext. `null` statt Leerstring: So greift für sie