From 5031d3bde0a639281644345553c8f452a31261cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Tue, 11 Aug 2026 14:54:33 +0200 Subject: [PATCH] Tax excpetion texts --- app/Enumerations/TaxExemptionReason.php | 12 ++- app/Views/Components/RichSelectBox.vue | 75 ++++++++++++----- ...06_170020_refine_tax_exemption_reasons.php | 81 +++++++++++++++++++ ...fault_tax_exemption_reason_jugendhilfe.php | 20 +++++ tests/Feature/TenantTaxInformationTest.php | 24 ++++-- 5 files changed, 179 insertions(+), 33 deletions(-) create mode 100644 database/migrations/2026_08_06_170020_refine_tax_exemption_reasons.php create mode 100644 database/migrations/2026_08_06_170030_default_tax_exemption_reason_jugendhilfe.php diff --git a/app/Enumerations/TaxExemptionReason.php b/app/Enumerations/TaxExemptionReason.php index 10da598..9493264 100644 --- a/app/Enumerations/TaxExemptionReason.php +++ b/app/Enumerations/TaxExemptionReason.php @@ -14,12 +14,14 @@ use App\Scopes\CommonModel; * @property string $name * @property string $invoice_text * @property bool $requires_note + * @property int $sort_order */ class TaxExemptionReason extends CommonModel { - public const string SMALL_BUSINESS = 'small_business'; - public const string EDUCATION = 'education'; - public const string YOUTH_WELFARE = 'youth_welfare'; + public const string USTG_19 = 'ustg_19'; + public const string USTG_4_24 = 'ustg_4_24'; + public const string USTG_4_25A = 'ustg_4_25a'; + public const string USTG_4_22A = 'ustg_4_22a'; public const string CUSTOM = 'custom'; protected $table = 'tax_exemption_reasons'; @@ -32,10 +34,12 @@ class TaxExemptionReason extends CommonModel 'name', 'invoice_text', 'requires_note', + 'sort_order', ]; protected $casts = [ 'requires_note' => 'boolean', + 'sort_order' => 'integer', ]; /** @@ -55,7 +59,7 @@ class TaxExemptionReason extends CommonModel */ public static function options(): array { - return self::all() + return self::orderBy('sort_order')->get() ->map(static fn (self $reason): array => [ 'value' => $reason->slug, 'label' => $reason->name, diff --git a/app/Views/Components/RichSelectBox.vue b/app/Views/Components/RichSelectBox.vue index 7513d8e..c168065 100644 --- a/app/Views/Components/RichSelectBox.vue +++ b/app/Views/Components/RichSelectBox.vue @@ -1,9 +1,11 @@ @@ -67,20 +92,28 @@ onBeforeUnmount(() => { - + @@ -126,11 +159,8 @@ onBeforeUnmount(() => { } .rich-select__list { - position: absolute; - z-index: 50; - top: calc(100% + 4px); - left: 0; - right: 0; + position: fixed; + z-index: 1000; margin: 0; padding: 4px; list-style: none; @@ -140,6 +170,7 @@ onBeforeUnmount(() => { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); max-height: 260px; overflow: auto; + box-sizing: border-box; } .rich-select__option { diff --git a/database/migrations/2026_08_06_170020_refine_tax_exemption_reasons.php b/database/migrations/2026_08_06_170020_refine_tax_exemption_reasons.php new file mode 100644 index 0000000..da9b672 --- /dev/null +++ b/database/migrations/2026_08_06_170020_refine_tax_exemption_reasons.php @@ -0,0 +1,81 @@ +unsignedSmallInteger('sort_order')->default(0); + }); + + // § 19 – nur Slug + Sortierung + DB::table('tax_exemption_reasons')->where('slug', 'small_business')->update([ + 'slug' => 'ustg_19', + 'sort_order' => 10, + ]); + + // § 4 Nr. 25 Buchst. a – präzises Zitat (Durchführung kultureller/sportlicher Veranstaltungen) + DB::table('tax_exemption_reasons')->where('slug', 'youth_welfare')->update([ + 'slug' => 'ustg_4_25a', + 'name' => 'Jugendhilfe – Veranstaltungen (§ 4 Nr. 25 Buchst. a UStG)', + 'invoice_text' => 'Die Leistung ist gemäß § 4 Nr. 25 Buchst. a UStG von der Umsatzsteuer befreit.', + 'sort_order' => 30, + ]); + + // § 4 Nr. 22 Buchst. a – korrekt benannt, ans Ende (vor Freitext) + DB::table('tax_exemption_reasons')->where('slug', 'education')->update([ + 'slug' => 'ustg_4_22a', + 'name' => 'Vorträge/Kurse (§ 4 Nr. 22 Buchst. a UStG)', + 'invoice_text' => 'Die Leistung ist gemäß § 4 Nr. 22 Buchst. a UStG von der Umsatzsteuer befreit.', + 'sort_order' => 40, + ]); + + // Freitext ans Ende + DB::table('tax_exemption_reasons')->where('slug', 'custom')->update([ + 'sort_order' => 50, + ]); + + // § 4 Nr. 24 (Jugendherbergen) – neu + $now = now(); + DB::table('tax_exemption_reasons')->insertOrIgnore([ + [ + 'slug' => 'ustg_4_24', + 'name' => 'Jugendherbergen (§ 4 Nr. 24 UStG)', + 'invoice_text' => 'Die Leistung ist gemäß § 4 Nr. 24 UStG von der Umsatzsteuer befreit.', + 'requires_note' => false, + 'sort_order' => 20, + 'created_at' => $now, + 'updated_at' => $now, + ], + ]); + } + + public function down(): void + { + DB::table('tax_exemption_reasons')->where('slug', 'ustg_4_24')->delete(); + DB::table('tax_exemption_reasons')->where('slug', 'ustg_19')->update(['slug' => 'small_business']); + DB::table('tax_exemption_reasons')->where('slug', 'ustg_4_25a')->update([ + 'slug' => 'youth_welfare', + 'name' => 'Kinder- und Jugendhilfe (§ 4 Nr. 25 UStG)', + 'invoice_text' => 'Die Leistung ist gemäß § 4 Nr. 25 UStG von der Umsatzsteuer befreit.', + ]); + DB::table('tax_exemption_reasons')->where('slug', 'ustg_4_22a')->update([ + 'slug' => 'education', + 'name' => 'Bildungs-/Kursleistung (§ 4 Nr. 22a UStG)', + 'invoice_text' => 'Die Leistung ist gemäß § 4 Nr. 22 Buchst. a UStG von der Umsatzsteuer befreit.', + ]); + + Schema::table('tax_exemption_reasons', function (Blueprint $table) { + $table->dropColumn('sort_order'); + }); + } +}; diff --git a/database/migrations/2026_08_06_170030_default_tax_exemption_reason_jugendhilfe.php b/database/migrations/2026_08_06_170030_default_tax_exemption_reason_jugendhilfe.php new file mode 100644 index 0000000..10cce5b --- /dev/null +++ b/database/migrations/2026_08_06_170030_default_tax_exemption_reason_jugendhilfe.php @@ -0,0 +1,20 @@ +tenant->update(['tax_exemption_reason' => 'small_business', 'tax_exemption_note' => 'alt']); + $this->tenant->update(['tax_exemption_reason' => 'ustg_19', 'tax_exemption_note' => 'alt']); $response = $this->runAction(taxLiable: true, vatRate: 7, reason: null); @@ -67,14 +67,14 @@ class TenantTaxInformationTest extends TestCase public function test_exemption_reason_persists_and_clears_rate(): void { - $response = $this->runAction(taxLiable: false, vatRate: 19, reason: 'youth_welfare'); + $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('youth_welfare', $fresh->tax_exemption_reason); + $this->assertSame('ustg_4_25a', $fresh->tax_exemption_reason); $this->assertNull($fresh->tax_exemption_note); } @@ -111,7 +111,7 @@ class TenantTaxInformationTest extends TestCase public function test_pricing_mode_resets_to_inclusive_when_exempt(): void { - $this->runAction(taxLiable: false, vatRate: 0, reason: 'small_business', mode: 'add_on'); + $this->runAction(taxLiable: false, vatRate: 0, reason: 'ustg_19', mode: 'add_on'); $this->assertSame('inclusive', $this->tenant->fresh()->vat_pricing_mode); } @@ -119,13 +119,23 @@ class TenantTaxInformationTest extends TestCase { $this->assertSame( 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.', - TaxExemptionReason::where('slug', 'small_business')->first()->invoiceText(), + TaxExemptionReason::where('slug', 'ustg_19')->first()->invoiceText(), ); $this->assertSame( - 'Die Leistung ist gemäß § 4 Nr. 25 UStG von der Umsatzsteuer befreit.', - TaxExemptionReason::where('slug', 'youth_welfare')->first()->invoiceText(), + '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); + } }