From 0724052313ffca5bca5a2d2515a25e96699886d1 Mon Sep 17 00:00:00 2001 From: philipp lang Date: Sun, 15 Mar 2026 04:42:26 +0100 Subject: [PATCH] Add skipping for yearly prevention --- app/Lib/ScoutFilter.php | 17 +++++++++++ app/Member/FilterScope.php | 15 ++++++---- app/Member/Member.php | 4 +++ .../Actions/YearlyRememberAction.php | 2 +- app/Prevention/PreventionSettings.php | 7 +++++ config/scout.php | 4 +-- database/factories/Member/MemberFactory.php | 2 ++ ...e_members_prevention_exceptions_column.php | 30 +++++++++++++++++++ tests/EndToEnd/Member/PreventionTest.php | 14 ++++++++- 9 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 database/migrations/2026_03_15_032430_create_members_prevention_exceptions_column.php diff --git a/app/Lib/ScoutFilter.php b/app/Lib/ScoutFilter.php index b512f407..2faace56 100644 --- a/app/Lib/ScoutFilter.php +++ b/app/Lib/ScoutFilter.php @@ -3,6 +3,7 @@ namespace App\Lib; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; use Laravel\Scout\Builder; use Spatie\LaravelData\Data; @@ -40,4 +41,20 @@ abstract class ScoutFilter extends Data { return static::factory()->withoutMagicalCreation()->from($post ?: []); } + + /** + * @param Collection $filter + * @param array $conditions + * @return Collection + */ + public function switches(Collection $filter, array $conditions): Collection + { + foreach ($conditions as $field => $value) { + if ($value !== null) { + $filter->push($field . ' = ' . ($value ? 'true' : 'false')); + } + } + + return $filter; + } } diff --git a/app/Member/FilterScope.php b/app/Member/FilterScope.php index 627ec9de..90eb6e50 100644 --- a/app/Member/FilterScope.php +++ b/app/Member/FilterScope.php @@ -48,6 +48,8 @@ class FilterScope extends ScoutFilter public ?bool $hasBirthday = null, public ?bool $hasSvk = null, public ?bool $hasVk = null, + public ?bool $skipYearlyPrevention = null, + public ?bool $skipEventPrevention = null, ) {} /** @@ -72,6 +74,7 @@ class FilterScope extends ScoutFilter $this->search = $this->search ?: ''; return Member::search($this->search, function ($engine, string $query, array $options) { + /** @var Collection */ $filter = collect([]); if ($this->hasFullAddress === true) { @@ -86,12 +89,12 @@ class FilterScope extends ScoutFilter if ($this->hasBirthday === true) { $filter->push('birthday IS NOT NULL'); } - if ($this->hasSvk !== null) { - $filter->push('has_svk = ' . ($this->hasSvk ? 'true' : 'false')); - } - if ($this->hasVk !== null) { - $filter->push('has_vk = ' . ($this->hasVk ? 'true' : 'false')); - } + $filter = $this->switches($filter, [ + 'skip_yearly_prevention' => $this->skipYearlyPrevention, + 'skip_event_prevention' => $this->skipEventPrevention, + 'has_vk' => $this->hasVk, + 'has_svk' => $this->hasSvk, + ]); if ($this->ausstand === true) { $filter->push('ausstand > 0'); } diff --git a/app/Member/Member.php b/app/Member/Member.php index 67973eac..5797f990 100644 --- a/app/Member/Member.php +++ b/app/Member/Member.php @@ -79,6 +79,8 @@ class Member extends Model implements Geolocatable, Preventable 'nami_id' => 'integer', 'has_svk' => 'boolean', 'has_vk' => 'boolean', + 'skip_yearly_prevention' => 'boolean', + 'skip_event_prevention' => 'boolean', 'multiply_pv' => 'boolean', 'multiply_more_pv' => 'boolean', 'is_leader' => 'boolean', @@ -597,6 +599,8 @@ class Member extends Model implements Geolocatable, Preventable 'group_name' => $this->group->inner_name ?: $this->group->name, 'has_vk' => $this->has_vk, 'has_svk' => $this->has_svk, + 'skip_yearly_prevention' => $this->skip_yearly_prevention, + 'skip_event_prevention' => $this->skip_event_prevention, 'links' => [ 'show' => route('member.show', ['member' => $this], false), 'edit' => route('member.edit', ['member' => $this], false), diff --git a/app/Prevention/Actions/YearlyRememberAction.php b/app/Prevention/Actions/YearlyRememberAction.php index 4f94fc80..fb25dacc 100644 --- a/app/Prevention/Actions/YearlyRememberAction.php +++ b/app/Prevention/Actions/YearlyRememberAction.php @@ -26,7 +26,7 @@ class YearlyRememberAction return; } - foreach ($settings->yearlyMemberFilter->getQuery()->get() as $member) { + foreach ($settings->getYearlyMemberFilter()->getQuery()->get() as $member) { // @todo add this check to FilterScope if ($member->getMailRecipient() === null) { continue; diff --git a/app/Prevention/PreventionSettings.php b/app/Prevention/PreventionSettings.php index 59eb5875..287b7c76 100644 --- a/app/Prevention/PreventionSettings.php +++ b/app/Prevention/PreventionSettings.php @@ -53,4 +53,11 @@ class PreventionSettings extends LocalSettings 'replyToMail' => $this->replyToMail, ]; } + + public function getYearlyMemberFilter(): FilterScope + { + $this->yearlyMemberFilter->skipYearlyPrevention = false; + + return $this->yearlyMemberFilter; + } } diff --git a/config/scout.php b/config/scout.php index 85d135f3..232f803f 100644 --- a/config/scout.php +++ b/config/scout.php @@ -138,10 +138,10 @@ return [ 'key' => env('MEILI_MASTER_KEY', null), 'index-settings' => [ Member::class => [ - 'filterableAttributes' => ['address', 'birthday', 'ausstand', 'bill_kind', 'group_id', 'memberships', 'has_vk', 'has_svk', 'id'], + 'filterableAttributes' => ['address', 'birthday', 'ausstand', 'bill_kind', 'group_id', 'memberships', 'has_vk', 'has_svk', 'id', 'skip_yearly_prevention', 'skip_event_prevention'], 'searchableAttributes' => ['fullname', 'address'], 'sortableAttributes' => ['lastname', 'firstname'], - 'displayedAttributes' => ['age_group_icon', 'group_name', 'links', 'is_leader', 'lastname', 'firstname', 'fullname', 'address', 'ausstand', 'birthday', 'id', 'memberships', 'bill_kind', 'group_id'], + 'displayedAttributes' => ['age_group_icon', 'group_name', 'links', 'is_leader', 'lastname', 'firstname', 'fullname', 'address', 'ausstand', 'birthday', 'id', 'memberships', 'bill_kind', 'group_id', 'skip_yearly_prevention', 'skip_event_prevention', 'has_vk', 'has_svk'], 'pagination' => [ 'maxTotalHits' => 1000000, ] diff --git a/database/factories/Member/MemberFactory.php b/database/factories/Member/MemberFactory.php index 7478eed4..b4b7e7eb 100644 --- a/database/factories/Member/MemberFactory.php +++ b/database/factories/Member/MemberFactory.php @@ -39,6 +39,8 @@ class MemberFactory extends Factory 'keepdata' => false, 'has_svk' => $this->faker->boolean(), 'has_vk' => $this->faker->boolean(), + 'skip_yearly_prevention' => false, + 'skip_event_prevention' => false, ]; } diff --git a/database/migrations/2026_03_15_032430_create_members_prevention_exceptions_column.php b/database/migrations/2026_03_15_032430_create_members_prevention_exceptions_column.php new file mode 100644 index 00000000..c71bf792 --- /dev/null +++ b/database/migrations/2026_03_15_032430_create_members_prevention_exceptions_column.php @@ -0,0 +1,30 @@ +boolean('skip_yearly_prevention')->default(false); + $table->boolean('skip_event_prevention')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('members', function (Blueprint $table) { + $table->dropColumn('skip_yearly_prevention'); + $table->dropColumn('skip_event_prevention'); + }); + } +}; diff --git a/tests/EndToEnd/Member/PreventionTest.php b/tests/EndToEnd/Member/PreventionTest.php index 8a1696f4..f1300432 100644 --- a/tests/EndToEnd/Member/PreventionTest.php +++ b/tests/EndToEnd/Member/PreventionTest.php @@ -248,6 +248,18 @@ it('notices a few weeks before', function ($date, bool $shouldSend) { [fn() => now()->subYears(5)->addWeeks(2)->subDay(), false], ]); +it('skips members that are marked as skipped for yearly mail', function (bool $skip) { + Mail::fake(); + createMember(['efz' => null, 'skip_yearly_prevention' => $skip]); + + sleep(2); + YearlyRememberAction::run(); + + $skip + ? Mail::assertNotSent(YearlyMail::class) + : Mail::assertSent(YearlyMail::class); +})->with([true, false]); + it('sets reply to mail', function () { Mail::fake(); app(PreventionSettings::class)->fill(['replyToMail' => 'admin@example.com'])->save(); @@ -256,7 +268,7 @@ it('sets reply to mail', function () { sleep(2); YearlyRememberAction::run(); - Mail::assertSent(YearlyMail::class, fn ($message) => $message->hasReplyTo('admin@example.com')); + Mail::assertSent(YearlyMail::class, fn($message) => $message->hasReplyTo('admin@example.com')); }); it('remembers members yearly', function ($date, $shouldSend) {