Handling of event addons

This commit is contained in:
2026-08-12 17:18:45 +02:00
parent 085299336e
commit 6e4e6b645c
74 changed files with 3072 additions and 95 deletions
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace Tests\Feature;
use App\Domains\Event\Actions\CreateEvent\CreateEventCommand;
use App\Domains\Event\Actions\CreateEvent\CreateEventRequest;
use App\Enumerations\EatingHabit;
use App\Enumerations\ParticipationFeeType;
use App\Models\Event;
use App\Models\Tenant;
use App\RelationModels\EventParticipationFee;
use App\Resources\EventResource;
use DateTime;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class EventVatPricingTest extends TestCase
{
use RefreshDatabase;
private Tenant $tenant;
protected function setUp(): void
{
parent::setUp();
$this->tenant = Tenant::create([
'slug' => 'tv',
'name' => 'Test',
'email' => 't@example.com',
'email_finance' => 'finance@example.com',
'url' => 'test.local',
'account_name' => 'Test e.V.',
'account_iban' => 'DE00',
'account_bic' => 'XY',
'city' => 'Stadt',
'postcode' => '00000',
'is_active_local_group' => true,
'has_active_instance' => true,
]);
app()->instance('tenant', $this->tenant);
DB::table('participation_types')->insert(['slug' => 'participant', 'name' => 'Teilnehmer']);
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']);
}
private function makeFee(float $amountStandard): EventParticipationFee
{
return EventParticipationFee::create([
'tenant' => $this->tenant->slug,
'type' => 'participant',
'name' => 'Standard',
'description' => null,
'amount_standard' => $amountStandard,
'amount_reduced' => null,
'amount_solidarity' => null,
]);
}
private function makeEvent(bool $taxLiable, int $vatRate, string $mode, int $feeId): Event
{
return Event::create([
'tenant' => $this->tenant->slug,
'name' => 'Event',
'identifier' => 'evt-' . uniqid(),
'location' => 'Ort',
'postal_code' => '00000',
'email' => 'e@example.com',
'start_date' => '2026-09-01',
'end_date' => '2026-09-05',
'early_bird_end' => '2026-08-20',
'registration_final_end' => '2026-08-25',
'early_bird_end_amount_increase' => 0,
'account_owner' => 'Owner',
'account_iban' => 'DE00',
'participation_fee_type' => 'fixed',
'participation_fee_1' => $feeId,
'pay_per_day' => false,
'pay_direct' => false,
'tax_liable' => $taxLiable,
'vat_rate' => $vatRate,
'vat_pricing_mode' => $mode,
]);
}
private function calculate(Event $event): float
{
return new EventResource($event)->calculateAmount(
'participant',
'standard',
new DateTime('2026-09-01'),
new DateTime('2026-09-01'),
false,
)->getAmount();
}
public function test_add_on_mode_adds_vat_on_top(): void
{
$event = $this->makeEvent(taxLiable: true, vatRate: 19, mode: 'add_on', feeId: $this->makeFee(100.0)->id);
$this->assertEqualsWithDelta(119.0, $this->calculate($event), 0.001);
}
public function test_inclusive_mode_keeps_amount(): void
{
$event = $this->makeEvent(taxLiable: true, vatRate: 19, mode: 'inclusive', feeId: $this->makeFee(100.0)->id);
$this->assertEqualsWithDelta(100.0, $this->calculate($event), 0.001);
}
public function test_not_liable_ignores_add_on_mode(): void
{
$event = $this->makeEvent(taxLiable: false, vatRate: 19, mode: 'add_on', feeId: $this->makeFee(100.0)->id);
$this->assertEqualsWithDelta(100.0, $this->calculate($event), 0.001);
}
public function test_create_event_snapshots_tenant_tax_config(): void
{
$this->tenant->update([
'tax_liable' => true,
'vat_rate' => 7,
'vat_pricing_mode' => 'add_on',
'tax_exemption_reason' => null,
'tax_exemption_note' => null,
]);
$response = new CreateEventCommand(new CreateEventRequest(
name: 'Sommerlager',
location: 'Ort',
postalCode: '00000',
email: 'e@example.com',
begin: new DateTime('2026-09-01'),
end: new DateTime('2026-09-05'),
earlyBirdEnd: new DateTime('2026-08-20'),
registrationFinalEnd: new DateTime('2026-08-25'),
earlyBirdEndAmountIncrease: 0,
participationFeeType: ParticipationFeeType::where('slug', 'fixed')->first(),
accountOwner: 'Owner',
accountIban: 'DE00',
payPerDay: false,
))->execute();
$this->assertTrue($response->success);
$event = $response->event->fresh();
$this->assertTrue($event->tax_liable);
$this->assertSame(7, $event->vat_rate);
$this->assertSame('add_on', $event->vat_pricing_mode);
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace Tests\Feature;
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxAction;
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxRequest;
use App\Enumerations\TaxExemptionReason;
use App\Enumerations\VatPricingMode;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TenantTaxInformationTest extends TestCase
{
use RefreshDatabase;
private Tenant $tenant;
protected function setUp(): void
{
parent::setUp();
$this->tenant = Tenant::create([
'slug' => 'tv',
'name' => 'Test',
'email' => 't@example.com',
'email_finance' => 'finance@example.com',
'url' => 'test.local',
'account_name' => 'Test e.V.',
'account_iban' => 'DE00',
'account_bic' => 'XY',
'city' => 'Stadt',
'postcode' => '00000',
'is_active_local_group' => true,
'has_active_instance' => true,
]);
app()->instance('tenant', $this->tenant);
}
private function runAction(bool $taxLiable, int $vatRate, ?string $reason, ?string $note = null, string $mode = 'inclusive'): object
{
return new UpdateTenantTaxAction(new UpdateTenantTaxRequest(
tenant: $this->tenant,
taxLiable: $taxLiable,
vatRate: $vatRate,
exemptionReason: $reason ? TaxExemptionReason::where('slug', $reason)->first() : null,
exemptionNote: $note,
vatPricingMode: VatPricingMode::from($mode),
))->execute();
}
public function test_taxable_sets_rate_and_clears_exemption(): void
{
$this->tenant->update(['tax_exemption_reason' => 'ustg_19', 'tax_exemption_note' => 'alt']);
$response = $this->runAction(taxLiable: true, vatRate: 7, reason: null);
$this->assertTrue($response->success);
$fresh = $this->tenant->fresh();
$this->assertTrue($fresh->tax_liable);
$this->assertSame(7, $fresh->vat_rate);
$this->assertNull($fresh->tax_exemption_reason);
$this->assertNull($fresh->tax_exemption_note);
}
public function test_exemption_reason_persists_and_clears_rate(): void
{
$response = $this->runAction(taxLiable: false, vatRate: 19, reason: 'ustg_4_25a');
$this->assertTrue($response->success);
$fresh = $this->tenant->fresh();
$this->assertFalse($fresh->tax_liable);
$this->assertSame(0, $fresh->vat_rate);
$this->assertSame('ustg_4_25a', $fresh->tax_exemption_reason);
$this->assertNull($fresh->tax_exemption_note);
}
public function test_invalid_exemption_reason_fails(): void
{
$response = $this->runAction(taxLiable: false, vatRate: 0, reason: 'not_a_reason');
$this->assertFalse($response->success);
// Unbekannter Slug wird nicht aufgelöst (null) → kein Speichern, Wert bleibt unverändert.
$this->assertNull($this->tenant->fresh()->tax_exemption_reason);
}
public function test_custom_reason_requires_note(): void
{
$missing = $this->runAction(taxLiable: false, vatRate: 0, reason: 'custom', note: ' ');
$this->assertFalse($missing->success);
$ok = $this->runAction(taxLiable: false, vatRate: 0, reason: 'custom', note: 'Individueller Grund');
$this->assertTrue($ok->success);
$this->assertSame('Individueller Grund', $this->tenant->fresh()->tax_exemption_note);
}
public function test_vat_rate_is_clamped(): void
{
$this->runAction(taxLiable: true, vatRate: 250, reason: null);
$this->assertSame(100, $this->tenant->fresh()->vat_rate);
}
public function test_pricing_mode_persists_when_liable(): void
{
$this->runAction(taxLiable: true, vatRate: 19, reason: null, mode: 'add_on');
$this->assertSame('add_on', $this->tenant->fresh()->vat_pricing_mode);
}
public function test_pricing_mode_resets_to_inclusive_when_exempt(): void
{
$this->runAction(taxLiable: false, vatRate: 0, reason: 'ustg_19', mode: 'add_on');
$this->assertSame('inclusive', $this->tenant->fresh()->vat_pricing_mode);
}
public function test_invoice_text_per_reason(): void
{
$this->assertSame(
'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
TaxExemptionReason::where('slug', 'ustg_19')->first()->invoiceText(),
);
$this->assertSame(
'Die Leistung ist gemäß § 4 Nr. 25 Buchst. a UStG von der Umsatzsteuer befreit.',
TaxExemptionReason::where('slug', 'ustg_4_25a')->first()->invoiceText(),
);
$this->assertSame(
'Die Leistung ist gemäß § 4 Nr. 24 UStG von der Umsatzsteuer befreit.',
TaxExemptionReason::where('slug', 'ustg_4_24')->first()->invoiceText(),
);
// Freitext-Grund liefert den mitgegebenen Text statt eines festen Rechnungshinweises.
$this->assertSame('Mein Text', TaxExemptionReason::where('slug', 'custom')->first()->invoiceText('Mein Text'));
}
public function test_options_are_ordered(): void
{
$slugs = array_column(TaxExemptionReason::options(), 'value');
$this->assertSame(['ustg_19', 'ustg_4_24', 'ustg_4_25a', 'ustg_4_22a', 'custom'], $slugs);
}
}