Fehler in Abrechnungsende behoben
This commit is contained in:
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user