diff --git a/app/Models/RejectionCategory.php b/app/Models/RejectionCategory.php new file mode 100644 index 00000000..e958a41e --- /dev/null +++ b/app/Models/RejectionCategory.php @@ -0,0 +1,59 @@ + */ + use HasFactory; + + protected $fillable = [ + 'code', + 'public_label', + 'public_desc', + 'examples', + 'severity', + 'rubric_ref', + 'active', + 'sort_order', + ]; + + protected function casts(): array + { + return [ + 'active' => 'boolean', + 'sort_order' => 'integer', + ]; + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeActive(Builder $query): Builder + { + return $query->where('active', true); + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeOrdered(Builder $query): Builder + { + return $query->orderBy('sort_order')->orderBy('code'); + } +} diff --git a/database/factories/RejectionCategoryFactory.php b/database/factories/RejectionCategoryFactory.php new file mode 100644 index 00000000..f338fd27 --- /dev/null +++ b/database/factories/RejectionCategoryFactory.php @@ -0,0 +1,36 @@ + + */ +class RejectionCategoryFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'code' => 'K'.fake()->unique()->numberBetween(1, 999), + 'public_label' => fake()->sentence(3), + 'public_desc' => fake()->sentence(12), + 'examples' => null, + 'severity' => 'rot', + 'rubric_ref' => null, + 'active' => true, + 'sort_order' => fake()->numberBetween(0, 100), + ]; + } + + public function inactive(): static + { + return $this->state(['active' => false]); + } +} diff --git a/database/migrations/2026_07_06_112202_create_rejection_categories_table.php b/database/migrations/2026_07_06_112202_create_rejection_categories_table.php new file mode 100644 index 00000000..2144b7da --- /dev/null +++ b/database/migrations/2026_07_06_112202_create_rejection_categories_table.php @@ -0,0 +1,39 @@ +id(); + + $table->string('code', 20)->unique(); // z. B. "K1", stabil + $table->string('public_label'); + $table->text('public_desc'); + $table->text('examples')->nullable(); // optional, öffentlich + $table->string('severity', 20)->default('rot'); // rot | gelb_hinweis + $table->string('rubric_ref', 80)->nullable(); // interne Rubrik, nie öffentlich + $table->boolean('active')->default(true); + $table->unsignedInteger('sort_order')->default(0); + $table->timestamps(); + + $table->index(['active', 'sort_order']); + }); + } + + public function down(): void + { + Schema::dropIfExists('rejection_categories'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index f19346db..3bb83175 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -22,6 +22,7 @@ class DatabaseSeeder extends Seeder PlanSeeder::class, PaymentOptionSeeder::class, LegalDocumentSeeder::class, + RejectionCategorySeeder::class, // CategorySeeder::class, ]); diff --git a/database/seeders/RejectionCategorySeeder.php b/database/seeders/RejectionCategorySeeder.php new file mode 100644 index 00000000..106dd591 --- /dev/null +++ b/database/seeders/RejectionCategorySeeder.php @@ -0,0 +1,39 @@ + [$code, $label, $desc]) { + RejectionCategory::query()->updateOrCreate( + ['code' => $code], + [ + 'public_label' => $label, + 'public_desc' => $desc, + 'severity' => 'rot', + 'active' => true, + 'sort_order' => ($index + 1) * 10, + ], + ); + } + } +} diff --git a/docs/weiteres/Live-Deployment-Checkliste (WS-7).md b/docs/weiteres/Live-Deployment-Checkliste (WS-7).md index 0ac8fa1f..efdad51b 100644 --- a/docs/weiteres/Live-Deployment-Checkliste (WS-7).md +++ b/docs/weiteres/Live-Deployment-Checkliste (WS-7).md @@ -125,6 +125,11 @@ php artisan legacy:grandfather-subscriptions # Favicons für alle Domains generieren php artisan domains:generate-favicons + +# Rechtstext-Versionen (E3) + Ablehnungskategorien K1–K6 anlegen — ohne sie +# fehlen AGB-Versionsangabe/Protokoll-Referenz und die /richtlinien-Kategorien. +php artisan db:seed --class=LegalDocumentSeeder --force +php artisan db:seed --class=RejectionCategorySeeder --force ``` --- diff --git a/resources/views/web/richtlinien.blade.php b/resources/views/web/richtlinien.blade.php index 30249536..d03de3b4 100644 --- a/resources/views/web/richtlinien.blade.php +++ b/resources/views/web/richtlinien.blade.php @@ -4,22 +4,20 @@ @section('meta_description', 'Veröffentlichungsrichtlinien für Pressemitteilungen auf presseecho.de und businessportal24.com: Prüfverfahren, Anforderungen und Ablehnungskategorien.') @php - // Rechtstexte-Architektur E3: Rahmentext v0.1 (03.07.2026). Die - // Ablehnungskategorien sind hier zum Launch statisch (K1–K6); das - // rejection_categories-System (live aus der DB) ist R3.1-Backlog. + // Rechtstexte-Architektur E3: statischer Rahmentext + dynamische + // Kategorienliste aus rejection_categories — neue/angepasste Kategorien + // erscheinen ohne Textänderung. Version/Stand aus legal_documents. $host = request()->getHost(); $isHub = str_contains($host, 'pressekonto'); $brandKey = $isHub ? 'pressekonto' : (str_contains($host, 'presseecho') ? 'presseecho' : 'businessportal24'); $contactEmail = (string) config('legal.contact_email'); - $rejectionCategories = [ - ['code' => 'K1', 'label' => 'Rechtswidrige Inhalte', 'desc' => 'Inhalte, die gegen geltendes Recht verstoßen — etwa strafbare Inhalte, Verstöße gegen das Wettbewerbs- oder Heilmittelwerberecht, Angebote verbotener Produkte oder Dienstleistungen.'], - ['code' => 'K2', 'label' => 'Explizite oder jugendgefährdende Inhalte', 'desc' => 'Pornografische, gewaltverherrlichende oder anderweitig jugendgefährdende Darstellungen in Text oder Bild.'], - ['code' => 'K3', 'label' => 'Hass und Diskriminierung', 'desc' => 'Inhalte, die Menschen wegen ihrer Herkunft, Religion, Geschlechtsidentität, sexuellen Orientierung, Behinderung oder vergleichbarer Merkmale herabwürdigen oder zu Hass aufrufen.'], - ['code' => 'K4', 'label' => 'Irreführung und Falschbehauptung', 'desc' => 'Nachweislich falsche Tatsachenbehauptungen, erfundene Studien oder Zitate, Täuschung über den Absender sowie gesundheits- oder finanzbezogene Heilsversprechen ohne Grundlage.'], - ['code' => 'K5', 'label' => 'Rechte Dritter', 'desc' => 'Verletzung von Urheber-, Marken- oder Persönlichkeitsrechten — etwa fremde Texte oder Bilder ohne Lizenz, unbefugte Namensnennung, herabsetzende Aussagen über Dritte.'], - ['code' => 'K6', 'label' => 'Kein Pressecharakter / Spam', 'desc' => 'Reine Werbe- oder Verkaufstexte ohne Nachrichtenwert, Keyword-Spam, automatisch generierte Masseninhalte ohne inhaltliche Substanz, Duplikate bereits veröffentlichter Mitteilungen.'], - ]; + $rejectionCategories = \App\Models\RejectionCategory::query()->active()->ordered()->get(); + + $guidelinesDocument = \App\Models\LegalDocument::current(\App\Models\LegalDocument::TYPE_RICHTLINIEN); + $guidelinesStand = $guidelinesDocument + ? 'Stand: '.$guidelinesDocument->effectiveFromFormatted().' · Version '.$guidelinesDocument->version + : 'Stand: 3. Juli 2026 · Version 0.1'; @endphp @section('content') @@ -27,7 +25,7 @@ eyebrow="Rechtliches" title="Veröffentlichungsrichtlinien" breadcrumb="Richtlinien" - updated="Stand: 3. Juli 2026 · Version 0.1" + updated="{{ $guidelinesStand }}" >

@@ -69,12 +67,15 @@

@foreach ($rejectionCategories as $category) -
+
- {{ $category['code'] }} + {{ $category->code }}
-

{{ $category['label'] }}

-

{{ $category['desc'] }}

+

{{ $category->public_label }}

+

{{ $category->public_desc }}

+ @if ($category->examples) +

Beispiele: {{ $category->examples }}

+ @endif
diff --git a/tests/Feature/Web/StaticPagesTest.php b/tests/Feature/Web/StaticPagesTest.php index f9415236..15282a6a 100644 --- a/tests/Feature/Web/StaticPagesTest.php +++ b/tests/Feature/Web/StaticPagesTest.php @@ -1,5 +1,7 @@ seed(RejectionCategorySeeder::class); + RejectionCategory::query()->where('code', 'K6')->update(['active' => false]); + RejectionCategory::factory()->create([ + 'code' => 'K7', + 'public_label' => 'Testkategorie aus der Datenbank', + 'sort_order' => 70, + ]); + $this->get('https://presseecho.test/de/richtlinien') ->assertSuccessful() ->assertSeeText('So funktioniert die Prüfung') - ->assertSeeText('Kein Pressecharakter / Spam') + ->assertSeeText('Rechtswidrige Inhalte') + ->assertSeeText('Testkategorie aus der Datenbank') ->assertSee('id="K1"', false) - ->assertSee('id="K6"', false); + ->assertSee('id="K7"', false) + // Deaktivierte Kategorien werden nicht mehr gerendert. + ->assertDontSeeText('Kein Pressecharakter / Spam'); }); test('the reporting page separates free legal paths from the paid takedown service', function () {