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(() => {
▾
-
+
+
+ {{ option.label }}
+
+
+
@@ -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);
+ }
}