From 308cd8a611baa0a35744f53df50356c72322740f Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode <123783602+HolgerHatGarKeineNode@users.noreply.github.com> Date: Sun, 17 May 2026 18:13:37 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20**Automate=20Meetup=20Activity?= =?UTF-8?q?=20Recalculation**=20-=20Introduced=20`recalculateActivity`=20m?= =?UTF-8?q?ethod=20in=20`Meetup`=20model=20to=20centralize=20activity=20an?= =?UTF-8?q?d=20event=20timestamp=20updates.=20-=20Added=20`MeetupEventObse?= =?UTF-8?q?rver`=20to=20trigger=20activity=20recalculation=20on=20event=20?= =?UTF-8?q?save/delete.=20-=20Updated=20`/meetups:update-activity`=20comma?= =?UTF-8?q?nd=20to=20leverage=20the=20new=20model=20method=20for=20cleanup?= =?UTF-8?q?.=20-=20Enhanced=20tests=20to=20cover=20various=20`MeetupEvent`?= =?UTF-8?q?=20scenarios=20affecting=20activity=20states.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/UpdateMeetupActivity.php | 42 +--------------- app/Models/Meetup.php | 34 +++++++++++++ app/Models/MeetupEvent.php | 3 ++ app/Observers/MeetupEventObserver.php | 18 +++++++ .../Database/UpdateMeetupActivityTest.php | 49 ++++++++++++++++++- 5 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 app/Observers/MeetupEventObserver.php diff --git a/app/Console/Commands/Database/UpdateMeetupActivity.php b/app/Console/Commands/Database/UpdateMeetupActivity.php index 5723e57..6ee405d 100644 --- a/app/Console/Commands/Database/UpdateMeetupActivity.php +++ b/app/Console/Commands/Database/UpdateMeetupActivity.php @@ -3,12 +3,9 @@ namespace App\Console\Commands\Database; use App\Models\Meetup; -use App\Models\MeetupEvent; -use Carbon\CarbonInterface; use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Date; #[Signature('meetups:update-activity')] #[Description('Recalculate is_active and last_event_at for every meetup based on its events.')] @@ -16,11 +13,9 @@ class UpdateMeetupActivity extends Command { public function handle(): int { - $threshold = now()->subYear(); - - Meetup::query()->chunkById(200, function ($meetups) use ($threshold) { + Meetup::query()->chunkById(200, function ($meetups) { foreach ($meetups as $meetup) { - $this->updateMeetup($meetup, $threshold); + $meetup->recalculateActivity(); } }); @@ -28,37 +23,4 @@ class UpdateMeetupActivity extends Command return Command::SUCCESS; } - - private function updateMeetup(Meetup $meetup, CarbonInterface $threshold): void - { - $lastEventAt = MeetupEvent::query() - ->where('meetup_id', $meetup->id) - ->where('start', '<=', now()) - ->max('start'); - - $lastEventAt = $lastEventAt ? Date::parse($lastEventAt) : null; - - $hasFutureEvent = MeetupEvent::query() - ->where('meetup_id', $meetup->id) - ->where('start', '>', now()) - ->exists(); - - $hasActiveRecurrence = MeetupEvent::query() - ->where('meetup_id', $meetup->id) - ->whereNotNull('recurrence_type') - ->where(function ($query) { - $query->whereNull('recurrence_end_date') - ->orWhere('recurrence_end_date', '>=', now()); - }) - ->exists(); - - $isActive = ($lastEventAt && $lastEventAt->greaterThanOrEqualTo($threshold)) - || $hasFutureEvent - || $hasActiveRecurrence; - - $meetup->forceFill([ - 'is_active' => $isActive, - 'last_event_at' => $lastEventAt, - ])->saveQuietly(); - } } diff --git a/app/Models/Meetup.php b/app/Models/Meetup.php index 00dbbd2..85d6272 100644 --- a/app/Models/Meetup.php +++ b/app/Models/Meetup.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Facades\Cookie; +use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; use Spatie\Image\Enums\Fit; use Spatie\MediaLibrary\HasMedia; @@ -166,4 +167,37 @@ class Meetup extends Model implements HasMedia { return $this->hasMany(MeetupEvent::class); } + + public function recalculateActivity(): void + { + $threshold = now()->subYear(); + + $lastEventAt = MeetupEvent::query() + ->where('meetup_id', $this->id) + ->where('start', '<=', now()) + ->max('start'); + + $lastEventAt = $lastEventAt ? Date::parse($lastEventAt) : null; + + $hasFutureEvent = MeetupEvent::query() + ->where('meetup_id', $this->id) + ->where('start', '>', now()) + ->exists(); + + $hasActiveRecurrence = MeetupEvent::query() + ->where('meetup_id', $this->id) + ->whereNotNull('recurrence_type') + ->whereNotNull('recurrence_end_date') + ->where('recurrence_end_date', '>=', now()) + ->exists(); + + $isActive = ($lastEventAt && $lastEventAt->greaterThanOrEqualTo($threshold)) + || $hasFutureEvent + || $hasActiveRecurrence; + + $this->forceFill([ + 'is_active' => $isActive, + 'last_event_at' => $lastEventAt, + ])->saveQuietly(); + } } diff --git a/app/Models/MeetupEvent.php b/app/Models/MeetupEvent.php index 6b42bfd..ca1ee61 100644 --- a/app/Models/MeetupEvent.php +++ b/app/Models/MeetupEvent.php @@ -3,10 +3,13 @@ namespace App\Models; use App\Enums\RecurrenceType; +use App\Observers\MeetupEventObserver; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +#[ObservedBy([MeetupEventObserver::class])] class MeetupEvent extends Model { use HasFactory; diff --git a/app/Observers/MeetupEventObserver.php b/app/Observers/MeetupEventObserver.php new file mode 100644 index 0000000..7635980 --- /dev/null +++ b/app/Observers/MeetupEventObserver.php @@ -0,0 +1,18 @@ +meetup?->recalculateActivity(); + } + + public function deleted(MeetupEvent $meetupEvent): void + { + $meetupEvent->meetup?->recalculateActivity(); + } +} diff --git a/tests/Feature/Database/UpdateMeetupActivityTest.php b/tests/Feature/Database/UpdateMeetupActivityTest.php index 752bfa4..85b1863 100644 --- a/tests/Feature/Database/UpdateMeetupActivityTest.php +++ b/tests/Feature/Database/UpdateMeetupActivityTest.php @@ -49,8 +49,8 @@ it('keeps a meetup active when it only has a future event', function () { ->and($meetup->last_event_at)->toBeNull(); }); -it('keeps a meetup active when a recurring event has no end date', function () { - $meetup = Meetup::factory()->create(['is_active' => false, 'last_event_at' => null]); +it('marks a meetup as inactive when a recurring event has no end date but is older than a year', function () { + $meetup = Meetup::factory()->create(['is_active' => true, 'last_event_at' => now()]); MeetupEvent::factory()->create([ 'meetup_id' => $meetup->id, 'start' => now()->subYears(3), @@ -60,6 +60,21 @@ it('keeps a meetup active when a recurring event has no end date', function () { $this->artisan('meetups:update-activity')->assertSuccessful(); + $meetup->refresh(); + expect($meetup->is_active)->toBeFalse(); +}); + +it('keeps a meetup active when a recurring event has an end date in the future', function () { + $meetup = Meetup::factory()->create(['is_active' => false, 'last_event_at' => null]); + MeetupEvent::factory()->create([ + 'meetup_id' => $meetup->id, + 'start' => now()->subYears(3), + 'recurrence_type' => RecurrenceType::Monthly, + 'recurrence_end_date' => now()->addMonths(6), + ]); + + $this->artisan('meetups:update-activity')->assertSuccessful(); + $meetup->refresh(); expect($meetup->is_active)->toBeTrue(); }); @@ -74,6 +89,36 @@ it('marks a meetup as inactive when no events exist at all', function () { ->and($meetup->last_event_at)->toBeNull(); }); +it('flips a meetup from inactive to active immediately when a future event is created', function () { + $meetup = Meetup::factory()->create(['is_active' => false, 'last_event_at' => null]); + + MeetupEvent::factory()->create([ + 'meetup_id' => $meetup->id, + 'start' => now()->addDays(7), + 'recurrence_type' => null, + ]); + + $meetup->refresh(); + expect($meetup->is_active)->toBeTrue(); +}); + +it('flips a meetup back to inactive when its only future event is deleted', function () { + $meetup = Meetup::factory()->create(['is_active' => false, 'last_event_at' => null]); + $event = MeetupEvent::factory()->create([ + 'meetup_id' => $meetup->id, + 'start' => now()->addDays(7), + 'recurrence_type' => null, + ]); + + $meetup->refresh(); + expect($meetup->is_active)->toBeTrue(); + + $event->delete(); + + $meetup->refresh(); + expect($meetup->is_active)->toBeFalse(); +}); + it('marks a meetup as inactive when a recurring event has ended more than a year ago', function () { $meetup = Meetup::factory()->create(['is_active' => true, 'last_event_at' => now()]); MeetupEvent::factory()->create([