From a43fd9cedf942a847da655c4c70be3b6036c5092 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20G=C3=BCnrher?=
Date: Tue, 29 Sep 2026 14:58:21 +0200
Subject: [PATCH] CoC Checks now optional
---
CLAUDE.md | 6 +++
.../CertificateOfConductionCheckCommand.php | 6 +++
.../Event/Actions/SignUp/SignUpCommand.php | 4 +-
.../UpdateEvent/UpdateEventCommand.php | 1 +
.../UpdateEvent/UpdateEventRequest.php | 4 +-
.../Event/Controllers/DetailsController.php | 1 +
.../Event/Views/Partials/CommonSettings.vue | 12 ++++++
app/Models/Event.php | 3 ++
app/Resources/EventResource.php | 1 +
...40020_add_coc_check_required_to_events.php | 24 +++++++++++
tests/Feature/ShortSignUpTest.php | 42 +++++++++++++++++++
version | 2 +-
12 files changed, 103 insertions(+), 3 deletions(-)
create mode 100644 database/migrations/2026_09_29_140020_add_coc_check_required_to_events.php
diff --git a/CLAUDE.md b/CLAUDE.md
index 5db148a..fcafba8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -36,6 +36,12 @@ UI-Bezeichnungen.
`/admin/settings`. Gelesen über `ParticipantGroupType::label()/pluralLabel()/labels()`. Texte ohne Artikel formulieren
(„Bitte bei „{label}" eine Auswahl treffen."), weil der Artikel vom Wort abhängt.
+## eFZ-Prüfung pro Veranstaltung
+
+`events.coc_check_required` (Default `true`, Checkbox in `CommonSettings.vue`, Resource-Feld `cocCheckRequired`). Ist
+sie aus, liefert `CertificateOfConductionCheckCommand` ohne API-Aufruf `not_required`, und `SignUpCommand` belegt
+`efz_status` mit `not_required` vor. Wirkt nur auf neue Anmeldungen, bestehende Status bleiben.
+
## Umsatzsteuer / Steuerinformationen
Rechtlich korrekter Begriff ist **Umsatzsteuer (USt)**, nicht MwSt. Ziel: tagesbasierte Anmelde-Rechnungen („Teilnahme
diff --git a/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckCommand.php b/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckCommand.php
index 1824432..aad8a37 100644
--- a/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckCommand.php
+++ b/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckCommand.php
@@ -12,6 +12,12 @@ class CertificateOfConductionCheckCommand {
public function execute() : CertificateOfConductionCheckResponse {
$response = new CertificateOfConductionCheckResponse();
+ // Verlangt die Veranstaltung kein eFZ, entfällt die Prüfung -- ohne Aufruf der Prüf-API.
+ if (!$this->request->participant->event()->first()?->coc_check_required) {
+ $response->status = EfzStatus::EFZ_STATUS_NOT_REQUIRED;
+ return $response;
+ }
+
// Der Stamm ist optional (die Kurzanmeldung erhebt ihn nicht, in der Verwaltung kann er entfernt werden).
$localGroup = str_replace('Stamm ', '', $this->request->participant->localGroup()->first()?->name ?? '');
diff --git a/app/Domains/Event/Actions/SignUp/SignUpCommand.php b/app/Domains/Event/Actions/SignUp/SignUpCommand.php
index f933bfb..e764bca 100644
--- a/app/Domains/Event/Actions/SignUp/SignUpCommand.php
+++ b/app/Domains/Event/Actions/SignUp/SignUpCommand.php
@@ -95,7 +95,9 @@ class SignUpCommand {
'payment_purpose' => $this->request->event->name . ' - Beitrag ' . $this->request->firstname . ' ' . $this->request->lastname,
'payment_method' => $this->request->paymentMethod,
'payment_options' => $paymentOptions,
- 'efz_status' => $participantAge->isfullAged() ? EfzStatus::EFZ_STATUS_NOT_CHECKED : EfzStatus::EFZ_STATUS_NOT_REQUIRED,
+ 'efz_status' => $participantAge->isfullAged() && $this->request->event->coc_check_required
+ ? EfzStatus::EFZ_STATUS_NOT_CHECKED
+ : EfzStatus::EFZ_STATUS_NOT_REQUIRED,
]
);
diff --git a/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php b/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php
index 1e216a3..b361ec4 100644
--- a/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php
+++ b/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php
@@ -28,6 +28,7 @@ class UpdateEventCommand {
$this->request->event->registration_allowed = $this->request->registrationAllowed;
$this->request->event->short_registration = $this->request->shortRegistration;
$this->request->event->swimming_permission_required = $this->request->swimmingPermissionRequired;
+ $this->request->event->coc_check_required = $this->request->cocCheckRequired;
// Umstellen zwischen Local und Parent Group nur ohne Anmeldungen -- sonst stünden in einer Veranstaltung
// beide Arten von Antworten nebeneinander.
diff --git a/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php b/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
index 5c9ad36..3e4ab74 100644
--- a/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
+++ b/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
@@ -22,6 +22,7 @@ class UpdateEventRequest {
public bool $registrationAllowed;
public bool $shortRegistration;
public bool $swimmingPermissionRequired;
+ public bool $cocCheckRequired;
public Amount $flatSupport;
public Amount $supportPerPerson;
public array $contributingLocalGroups;
@@ -29,9 +30,10 @@ class UpdateEventRequest {
/** Wonach bei der Anmeldung gefragt wird; null = unverändert. Greift nur, solange es keine Anmeldungen gibt. */
public ?ParticipantGroupType $participantGroupType;
- public function __construct(Event $event, string $eventName, string $eventLocation, string $postalCode, string $email, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $alcoholicsAge, bool $sendWeeklyReports, bool $registrationAllowed, Amount $flatSupport, Amount $supportPerPerson, array $contributingLocalGroups, array $eatingHabits, bool $shortRegistration = false, bool $swimmingPermissionRequired = true, ?string $street = null, ?string $houseNumber = null, ?ParticipantGroupType $participantGroupType = null)
+ public function __construct(Event $event, string $eventName, string $eventLocation, string $postalCode, string $email, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $alcoholicsAge, bool $sendWeeklyReports, bool $registrationAllowed, Amount $flatSupport, Amount $supportPerPerson, array $contributingLocalGroups, array $eatingHabits, bool $shortRegistration = false, bool $swimmingPermissionRequired = true, ?string $street = null, ?string $houseNumber = null, ?ParticipantGroupType $participantGroupType = null, bool $cocCheckRequired = true)
{
$this->participantGroupType = $participantGroupType;
+ $this->cocCheckRequired = $cocCheckRequired;
$this->event = $event;
$this->eventName = $eventName;
$this->eventLocation = $eventLocation;
diff --git a/app/Domains/Event/Controllers/DetailsController.php b/app/Domains/Event/Controllers/DetailsController.php
index 0db288a..97f3cde 100644
--- a/app/Domains/Event/Controllers/DetailsController.php
+++ b/app/Domains/Event/Controllers/DetailsController.php
@@ -70,6 +70,7 @@ class DetailsController extends CommonController {
$request->input('street'),
$request->input('houseNumber'),
ParticipantGroupType::tryFrom((string) $request->input('participantGroupType')),
+ (bool)$request->input('cocCheckRequired', true),
);
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
diff --git a/app/Domains/Event/Views/Partials/CommonSettings.vue b/app/Domains/Event/Views/Partials/CommonSettings.vue
index 568ec84..0ffcf86 100644
--- a/app/Domains/Event/Views/Partials/CommonSettings.vue
+++ b/app/Domains/Event/Views/Partials/CommonSettings.vue
@@ -44,6 +44,7 @@ const emit = defineEmits(['close'])
registrationAllowed: props.event.registrationAllowed,
shortRegistration: props.event.shortRegistration,
swimmingPermissionRequired: props.event.swimmingPermissionRequired,
+ cocCheckRequired: props.event.cocCheckRequired,
flatSupport: props.event.flatSupportEdit,
supportPerson: props.event.supportPersonIndex,
participantGroupType: props.event.participantGroupType ?? 'local_group',
@@ -78,6 +79,7 @@ const emit = defineEmits(['close'])
registrationAllowed: formData.registrationAllowed,
shortRegistration: formData.shortRegistration,
swimmingPermissionRequired: formData.swimmingPermissionRequired,
+ cocCheckRequired: formData.cocCheckRequired,
flatSupport: formData.flatSupport,
supportPerson: formData.supportPerson,
contributingLocalGroups: contributingLocalGroups.value,
@@ -234,6 +236,16 @@ const emit = defineEmits(['close'])
+
+
+
+
+
+
+ Ist die Prüfung deaktiviert, wird das eFZ bei neuen Anmeldungen als „Nicht erforderlich" hinterlegt.
+
+ |
+
diff --git a/app/Models/Event.php b/app/Models/Event.php
index 708048f..ef4f06a 100644
--- a/app/Models/Event.php
+++ b/app/Models/Event.php
@@ -51,6 +51,7 @@ use Illuminate\Support\Collection;
* @property boolean $archived
* @property boolean $short_registration
* @property boolean $swimming_permission_required
+ * @property boolean $coc_check_required
* @property string|null $invoice_key
* @property string $participant_group_type
*/
@@ -102,6 +103,7 @@ class Event extends InstancedModel
'short_registration',
'swimming_permission_required',
+ 'coc_check_required',
'invoice_key',
'participant_group_type',
@@ -137,6 +139,7 @@ class Event extends InstancedModel
'short_registration' => 'boolean',
'swimming_permission_required' => 'boolean',
+ 'coc_check_required' => 'boolean',
];
/**
diff --git a/app/Resources/EventResource.php b/app/Resources/EventResource.php
index ba565fd..d6e27db 100644
--- a/app/Resources/EventResource.php
+++ b/app/Resources/EventResource.php
@@ -85,6 +85,7 @@ class EventResource extends JsonResource{
// Steuert im Frontend, welcher Anmelde-Wizard gerendert wird, und ob die Badeerlaubnis abgefragt wird.
$returnArray['shortRegistration'] = (bool)$this->event->short_registration;
$returnArray['swimmingPermissionRequired'] = (bool)$this->event->swimming_permission_required;
+ $returnArray['cocCheckRequired'] = (bool)$this->event->coc_check_required;
$returnArray['siblingReduction'] = $this->event->sibling_reduction ?? true;
$returnArray['costUnit'] = new CostUnitResource($this->event->costUnit()->first())->toArray(true);
diff --git a/database/migrations/2026_09_29_140020_add_coc_check_required_to_events.php b/database/migrations/2026_09_29_140020_add_coc_check_required_to_events.php
new file mode 100644
index 0000000..7feb008
--- /dev/null
+++ b/database/migrations/2026_09_29_140020_add_coc_check_required_to_events.php
@@ -0,0 +1,24 @@
+boolean('coc_check_required')->default(true);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('events', function (Blueprint $table) {
+ $table->dropColumn('coc_check_required');
+ });
+ }
+};
diff --git a/tests/Feature/ShortSignUpTest.php b/tests/Feature/ShortSignUpTest.php
index b35eeeb..a90f344 100644
--- a/tests/Feature/ShortSignUpTest.php
+++ b/tests/Feature/ShortSignUpTest.php
@@ -2,6 +2,8 @@
namespace Tests\Feature;
+use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConductionCheckCommand;
+use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConductionCheckRequest;
use App\Domains\Event\Actions\ShortSignUp\ShortSignUpCommand;
use App\Domains\Event\Actions\ShortSignUp\ShortSignUpRequest;
use App\Enumerations\EatingHabit;
@@ -18,6 +20,7 @@ use App\Models\Tenant;
use App\RelationModels\EventParticipationFee;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
@@ -57,6 +60,7 @@ class ShortSignUpTest extends TestCase
bool $swimmingPermissionRequired = true,
?float $amountReduced = 30.0,
?float $amountSolidarity = 70.0,
+ bool $cocCheckRequired = true,
): Event
{
$event = Event::create([
@@ -66,6 +70,7 @@ class ShortSignUpTest extends TestCase
'account_owner' => 'Owner', 'account_iban' => 'DE00', 'participation_fee_type' => 'FIXED',
'pay_per_day' => false, 'pay_direct' => false,
'short_registration' => true, 'swimming_permission_required' => $swimmingPermissionRequired,
+ 'coc_check_required' => $cocCheckRequired,
]);
if ($withFee) {
@@ -217,6 +222,43 @@ class ShortSignUpTest extends TestCase
$this->assertSame('maria@example.com', $response->participant->email_2);
}
+ public function test_volljaehrige_ohne_efz_pruefung_brauchen_kein_efz(): void
+ {
+ Http::fake();
+ $event = $this->createEvent(cocCheckRequired: false);
+
+ $response = new ShortSignUpCommand($this->shortRequest($event))->execute();
+
+ $this->assertTrue($response->success, $response->message ?? '');
+ $this->assertSame(EfzStatus::EFZ_STATUS_NOT_REQUIRED, $response->participant->efz_status);
+
+ $cocResponse = new CertificateOfConductionCheckCommand(
+ new CertificateOfConductionCheckRequest($response->participant)
+ )->execute();
+
+ $this->assertSame(EfzStatus::EFZ_STATUS_NOT_REQUIRED, $cocResponse->status);
+ Http::assertNothingSent();
+ }
+
+ public function test_volljaehrige_mit_efz_pruefung_werden_geprueft(): void
+ {
+ putenv('COC_CHECK_URL=https://coc.test/check');
+ Http::fake(['*' => Http::response(['status' => '200/ok', 'efzStatus' => 'CHECKED_VALID'])]);
+ $event = $this->createEvent();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event))->execute();
+
+ $this->assertTrue($response->success, $response->message ?? '');
+ $this->assertSame(EfzStatus::EFZ_STATUS_NOT_CHECKED, $response->participant->efz_status);
+
+ $cocResponse = new CertificateOfConductionCheckCommand(
+ new CertificateOfConductionCheckRequest($response->participant)
+ )->execute();
+
+ $this->assertSame(EfzStatus::EFZ_STATUS_CHECKED_VALID, $cocResponse->status);
+ Http::assertSentCount(1);
+ }
+
public function test_u18_uebernimmt_gewaehlte_badeerlaubnis(): void
{
$event = $this->createEvent();
diff --git a/version b/version
index 2da4316..ad96464 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-4.10.0
+4.10.1