mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-05-18 20:34:52 +00:00
🎉 **Introduce meetup activity management**
- Added `is_active` and `last_event_at` fields to meetups with migration. - Enhanced UI: Display `Aktiv`/`Inaktiv` badges and last event dates across dashboard, tables, and maps. - Introduced `/meetups:update-activity` command to manage activity flags and timestamps. - Validated latitude/longitude to prevent `0,0` inputs in city creation and updates. - Updated factories and tests to include meetup activity states (`active`, `inactive`).
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
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.')]
|
||||
class UpdateMeetupActivity extends Command
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
$threshold = now()->subYear();
|
||||
|
||||
Meetup::query()->chunkById(200, function ($meetups) use ($threshold) {
|
||||
foreach ($meetups as $meetup) {
|
||||
$this->updateMeetup($meetup, $threshold);
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Meetup activity flags updated.');
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ class Meetup extends Model implements HasMedia
|
||||
'community',
|
||||
'github_data',
|
||||
'visible_on_map',
|
||||
'is_active',
|
||||
'last_event_at',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -53,6 +55,8 @@ class Meetup extends Model implements HasMedia
|
||||
'city_id' => 'integer',
|
||||
'github_data' => 'json',
|
||||
'simplified_geojson' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'last_event_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
||||
@@ -38,6 +38,24 @@ class MeetupFactory extends Factory
|
||||
'nostr' => NostrHelper::randomNpub(),
|
||||
'nostr_status' => NostrHelper::fakeNostrEventStatus(),
|
||||
'created_by' => User::factory(),
|
||||
'is_active' => false,
|
||||
'last_event_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function active(): static
|
||||
{
|
||||
return $this->state(fn (array $attrs) => [
|
||||
'is_active' => true,
|
||||
'last_event_at' => now()->subDays(30),
|
||||
]);
|
||||
}
|
||||
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attrs) => [
|
||||
'is_active' => false,
|
||||
'last_event_at' => now()->subYears(2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('meetups', function (Blueprint $table) {
|
||||
$table->boolean('is_active')->default(false)->after('visible_on_map');
|
||||
$table->timestamp('last_event_at')->nullable()->after('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('meetups', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_active', 'last_event_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,20 @@
|
||||
@props(['meetup', 'url', 'eventUrl' => null])
|
||||
|
||||
<div class="w-72">
|
||||
<flux:heading size="lg" class="mb-3">{{ $meetup->name }}</flux:heading>
|
||||
<div class="flex items-center justify-between mb-3 gap-2">
|
||||
<flux:heading size="lg">{{ $meetup->name }}</flux:heading>
|
||||
@if($meetup->is_active)
|
||||
<flux:badge color="green" size="sm">{{ __('Aktiv') }}</flux:badge>
|
||||
@else
|
||||
<flux:badge color="zinc" size="sm">{{ __('Inaktiv') }}</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($meetup->last_event_at)
|
||||
<flux:text class="text-xs text-zinc-500 mb-2">
|
||||
{{ __('Letztes Event') }}: {{ $meetup->last_event_at->asDate() }}
|
||||
</flux:text>
|
||||
@endif
|
||||
|
||||
@if($meetup->intro)
|
||||
<flux:text class="text-sm text-zinc-600 dark:text-zinc-400 mb-3">
|
||||
|
||||
@@ -36,8 +36,17 @@ class extends Component {
|
||||
'longitude' => ['required', 'numeric', 'between:-180,180'],
|
||||
'population' => ['nullable', 'integer', 'min:0'],
|
||||
'population_date' => ['nullable', 'string', 'max:255'],
|
||||
], [], [
|
||||
'latitude' => __('Breitengrad'),
|
||||
'longitude' => __('Längengrad'),
|
||||
]);
|
||||
|
||||
if ((float) $validated['latitude'] === 0.0 && (float) $validated['longitude'] === 0.0) {
|
||||
$this->addError('latitude', __('Breiten- und Längengrad dürfen nicht beide 0 sein.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$validated['slug'] = str($validated['name'])->slug();
|
||||
$validated['created_by'] = auth()->id();
|
||||
|
||||
|
||||
@@ -39,8 +39,17 @@ class extends Component {
|
||||
'longitude' => ['required', 'numeric', 'between:-180,180'],
|
||||
'population' => ['nullable', 'integer', 'min:0'],
|
||||
'population_date' => ['nullable', 'string', 'max:255'],
|
||||
], [], [
|
||||
'latitude' => __('Breitengrad'),
|
||||
'longitude' => __('Längengrad'),
|
||||
]);
|
||||
|
||||
if ((float) $validated['latitude'] === 0.0 && (float) $validated['longitude'] === 0.0) {
|
||||
$this->addError('latitude', __('Breiten- und Längengrad dürfen nicht beide 0 sein.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$validated['slug'] = str($validated['name'])->slug();
|
||||
|
||||
$this->city->update($validated);
|
||||
|
||||
@@ -152,11 +152,12 @@ class extends Component {
|
||||
<flux:separator class="my-4"/>
|
||||
<div class="space-y-3">
|
||||
@foreach($myMeetups as $meetup)
|
||||
<div class="flex flex-col sm:flex-row items-start justify-between gap-3">
|
||||
<div class="flex flex-col sm:flex-row items-start justify-between gap-3 {{ $meetup->is_active ? '' : 'opacity-60' }}">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<flux:avatar
|
||||
:href="route('meetups.landingpage', ['meetup' => $meetup, 'country' => $country])"
|
||||
size="sm"
|
||||
class="{{ $meetup->is_active ? '' : 'grayscale' }}"
|
||||
src="{{ $meetup->getFirstMedia('logo') ? $meetup->getFirstMediaUrl('logo', 'thumb') : asset('android-chrome-512x512.png') }}"/>
|
||||
<a href="{{ route('meetups.landingpage', ['meetup' => $meetup, 'country' => $country]) }}">
|
||||
<div>
|
||||
@@ -167,6 +168,9 @@ class extends Component {
|
||||
src="{{ asset('vendor/blade-flags/country-'.strtolower($meetup->city->country->code).'.svg') }}"
|
||||
width="24" height="12"
|
||||
/>
|
||||
@unless($meetup->is_active)
|
||||
<flux:badge color="zinc" size="sm">{{ __('Inaktiv') }}</flux:badge>
|
||||
@endunless
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
{{ $meetup->city->name }}
|
||||
|
||||
@@ -64,14 +64,18 @@ class extends Component {
|
||||
@if($activity['type'] === 'meetup')
|
||||
@php $meetup = $activity['data']; @endphp
|
||||
<a href="{{ route('meetups.landingpage', ['meetup' => $meetup, 'country' => $meetup->city->country->code]) }}"
|
||||
class="block p-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg transition-colors">
|
||||
class="block p-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg transition-colors {{ $meetup->is_active ? '' : 'opacity-60' }}">
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:avatar
|
||||
size="sm"
|
||||
class="{{ $meetup->is_active ? '' : 'grayscale' }}"
|
||||
src="{{ $meetup->getFirstMedia('logo') ? $meetup->getFirstMediaUrl('logo', 'thumb') : asset('android-chrome-512x512.png') }}"/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge color="green" size="sm">{{ __('Neues Meetup') }}</flux:badge>
|
||||
@unless($meetup->is_active)
|
||||
<flux:badge color="zinc" size="sm">{{ __('Inaktiv') }}</flux:badge>
|
||||
@endunless
|
||||
<img
|
||||
alt="{{ strtolower($meetup->city->country->code) }}"
|
||||
src="{{ asset('vendor/blade-flags/country-'.strtolower($meetup->city->country->code).'.svg') }}"
|
||||
|
||||
@@ -87,13 +87,19 @@ class extends Component {
|
||||
<div class="space-y-3">
|
||||
@foreach($topMeetups as $meetup)
|
||||
<a href="{{ route('meetups.landingpage', ['meetup' => $meetup, 'country' => $meetup->city->country->code]) }}"
|
||||
class="flex items-center justify-between gap-3 p-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg transition-colors block">
|
||||
class="flex items-center justify-between gap-3 p-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg transition-colors block {{ $meetup->is_active ? '' : 'opacity-60' }}">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<flux:avatar
|
||||
size="sm"
|
||||
class="{{ $meetup->is_active ? '' : 'grayscale' }}"
|
||||
src="{{ $meetup->getFirstMedia('logo') ? $meetup->getFirstMediaUrl('logo', 'thumb') : asset('android-chrome-512x512.png') }}"/>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">{{ $meetup->name }}</div>
|
||||
<div class="font-medium flex items-center gap-2">
|
||||
<span>{{ $meetup->name }}</span>
|
||||
@unless($meetup->is_active)
|
||||
<flux:badge color="zinc" size="sm">{{ __('Inaktiv') }}</flux:badge>
|
||||
@endunless
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-xs text-zinc-500">{{ $meetup->users_count }} {{ __('User') }}</div>
|
||||
<img
|
||||
|
||||
@@ -47,10 +47,19 @@ class extends Component {
|
||||
$validated = $this->validate([
|
||||
'newCityName' => ['required', 'string', 'max:255', 'unique:cities,name'],
|
||||
'newCityCountryId' => ['required', 'exists:countries,id'],
|
||||
'newCityLatitude' => ['required', 'numeric'],
|
||||
'newCityLongitude' => ['required', 'numeric'],
|
||||
'newCityLatitude' => ['required', 'numeric', 'between:-90,90'],
|
||||
'newCityLongitude' => ['required', 'numeric', 'between:-180,180'],
|
||||
], [], [
|
||||
'newCityLatitude' => __('Breitengrad'),
|
||||
'newCityLongitude' => __('Längengrad'),
|
||||
]);
|
||||
|
||||
if ((float) $validated['newCityLatitude'] === 0.0 && (float) $validated['newCityLongitude'] === 0.0) {
|
||||
$this->addError('newCityLatitude', __('Breiten- und Längengrad dürfen nicht beide 0 sein.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$city = City::create([
|
||||
'name' => $validated['newCityName'],
|
||||
'country_id' => $validated['newCityCountryId'],
|
||||
|
||||
@@ -73,6 +73,7 @@ class extends Component {
|
||||
<flux:table.columns>
|
||||
<flux:table.column>{{ __('Name') }}
|
||||
</flux:table.column>
|
||||
<flux:table.column>{{ __('Aktivität') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Nächster Termin') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Links') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||||
@@ -80,10 +81,10 @@ class extends Component {
|
||||
|
||||
<flux:table.rows>
|
||||
@foreach ($meetups as $meetup)
|
||||
<flux:table.row :key="$meetup->id">
|
||||
<flux:table.row :key="$meetup->id" :class="$meetup->is_active ? '' : 'opacity-60'">
|
||||
<flux:table.cell variant="strong" class="flex items-center gap-3">
|
||||
<flux:avatar
|
||||
class="[:where(&)]:size-24 [:where(&)]:text-base" size="xl"
|
||||
class="[:where(&)]:size-24 [:where(&)]:text-base {{ $meetup->is_active ? '' : 'grayscale' }}" size="xl"
|
||||
:href="route('meetups.landingpage', ['meetup' => $meetup, 'country' => $country])"
|
||||
src="{{ $meetup->getFirstMedia('logo') ? $meetup->getFirstMediaUrl('logo', 'thumb') : asset('android-chrome-512x512.png') }}"/>
|
||||
<div>
|
||||
@@ -102,6 +103,23 @@ class extends Component {
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<div class="flex flex-col gap-1">
|
||||
@if($meetup->is_active)
|
||||
<flux:badge color="green" size="sm">{{ __('Aktiv') }}</flux:badge>
|
||||
@else
|
||||
<flux:badge color="zinc" size="sm">{{ __('Inaktiv') }}</flux:badge>
|
||||
@endif
|
||||
@if($meetup->last_event_at)
|
||||
<span class="text-xs text-zinc-500">
|
||||
{{ __('Letztes Event') }}: {{ $meetup->last_event_at->asDate() }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-xs text-zinc-500">{{ __('Noch kein Event') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
@if($meetup->nextEvent && $meetup->nextEvent['start']->isFuture())
|
||||
<a href="{{ route('meetups.landingpage-event', ['meetup' => $meetup, 'event' => $meetup->nextEvent['id'], 'country' => $country]) }}">
|
||||
|
||||
@@ -49,6 +49,8 @@ class extends Component {
|
||||
'meetups.nostr',
|
||||
'meetups.simplex',
|
||||
'meetups.signal',
|
||||
'meetups.is_active',
|
||||
'meetups.last_event_at',
|
||||
])
|
||||
->with(['city:id,country_id,longitude,latitude', 'city.country'])
|
||||
->when(
|
||||
@@ -81,6 +83,7 @@ class extends Component {
|
||||
'name' => $meetup->name,
|
||||
'slug' => $meetup->slug,
|
||||
'city' => $meetup->city,
|
||||
'is_active' => (bool) $meetup->is_active,
|
||||
'popupHtml' => view('components.meetup-popup', [
|
||||
'meetup' => $meetup,
|
||||
'url' => route('meetups.landingpage', [
|
||||
@@ -105,6 +108,18 @@ class extends Component {
|
||||
#map:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.meetup-marker-inactive {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.meetup-marker-inactive img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.55;
|
||||
}
|
||||
</style>
|
||||
@php
|
||||
$attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
@@ -127,7 +142,7 @@ class extends Component {
|
||||
attribution: '{{ $attribution }}'
|
||||
}).addTo(map);
|
||||
|
||||
// Custom BTC icon
|
||||
// Custom BTC icon (active meetups)
|
||||
const btcIcon = L.icon({
|
||||
iconUrl: '/img/btc_marker.png',
|
||||
iconSize: [32, 32], // Full size of the image
|
||||
@@ -136,9 +151,18 @@ class extends Component {
|
||||
shadowUrl: null // No shadow for simplicity
|
||||
});
|
||||
|
||||
// Inactive meetups: smaller, grayscaled, semi-transparent
|
||||
const btcIconInactive = L.divIcon({
|
||||
className: 'meetup-marker-inactive',
|
||||
html: `<img src='/img/btc_marker.png' alt='' />`,
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 20],
|
||||
popupAnchor: [0, -20],
|
||||
});
|
||||
|
||||
this.markers.forEach(marker => {
|
||||
L.marker([marker.city.latitude, marker.city.longitude], {
|
||||
icon: btcIcon
|
||||
icon: marker.is_active ? btcIcon : btcIconInactive
|
||||
})
|
||||
.bindPopup(marker.popupHtml)
|
||||
.addTo(map);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Console\Commands\Database\CleanupLoginKeys;
|
||||
use App\Console\Commands\Database\UpdateMeetupActivity;
|
||||
use App\Console\Commands\Nostr\PublishUnpublishedItems;
|
||||
|
||||
Schedule::command(CleanupLoginKeys::class)->everyFifteenMinutes();
|
||||
@@ -12,3 +13,5 @@ Schedule::command(PublishUnpublishedItems::class, [
|
||||
Schedule::command(PublishUnpublishedItems::class, [
|
||||
'--model' => 'Meetup',
|
||||
])->dailyAt('18:00');
|
||||
|
||||
Schedule::command(UpdateMeetupActivity::class)->dailyAt('03:30');
|
||||
|
||||
@@ -86,6 +86,40 @@ it('rejects city creation with non-existent country', function () {
|
||||
->assertHasErrors(['country_id' => 'exists']);
|
||||
});
|
||||
|
||||
it('rejects city creation when latitude and longitude are both zero', function () {
|
||||
actingAsUser();
|
||||
|
||||
Livewire::test('cities.create')
|
||||
->set('name', 'Null Island')
|
||||
->set('country_id', $this->country->id)
|
||||
->set('latitude', 0)
|
||||
->set('longitude', 0)
|
||||
->call('createCity')
|
||||
->assertHasErrors(['latitude']);
|
||||
|
||||
expect(City::query()->where('name', 'Null Island')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('rejects city update when latitude and longitude are both zero', function () {
|
||||
$city = City::factory()->create([
|
||||
'name' => 'Berlin Test',
|
||||
'country_id' => $this->country->id,
|
||||
'latitude' => 52.52,
|
||||
'longitude' => 13.405,
|
||||
]);
|
||||
actingAsUser();
|
||||
|
||||
Livewire::test('cities.edit', ['city' => $city])
|
||||
->set('name', 'Berlin Test')
|
||||
->set('country_id', $this->country->id)
|
||||
->set('latitude', 0)
|
||||
->set('longitude', 0)
|
||||
->call('updateCity')
|
||||
->assertHasErrors(['latitude']);
|
||||
|
||||
expect($city->refresh()->latitude)->toEqual(52.52);
|
||||
});
|
||||
|
||||
it('updates an existing city', function () {
|
||||
$city = City::factory()->create(['name' => 'Old Name', 'country_id' => $this->country->id]);
|
||||
actingAsUser();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\RecurrenceType;
|
||||
use App\Models\Meetup;
|
||||
use App\Models\MeetupEvent;
|
||||
|
||||
it('marks a meetup as active when it has a past event within the last year', function () {
|
||||
$meetup = Meetup::factory()->create(['is_active' => false, 'last_event_at' => null]);
|
||||
MeetupEvent::factory()->create([
|
||||
'meetup_id' => $meetup->id,
|
||||
'start' => now()->subMonths(3),
|
||||
'recurrence_type' => null,
|
||||
]);
|
||||
|
||||
$this->artisan('meetups:update-activity')->assertSuccessful();
|
||||
|
||||
$meetup->refresh();
|
||||
expect($meetup->is_active)->toBeTrue()
|
||||
->and($meetup->last_event_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('marks a meetup as inactive when the last event is older than a year and no future event exists', function () {
|
||||
$meetup = Meetup::factory()->create(['is_active' => true, 'last_event_at' => now()]);
|
||||
MeetupEvent::factory()->create([
|
||||
'meetup_id' => $meetup->id,
|
||||
'start' => now()->subYears(2),
|
||||
'recurrence_type' => null,
|
||||
]);
|
||||
|
||||
$this->artisan('meetups:update-activity')->assertSuccessful();
|
||||
|
||||
$meetup->refresh();
|
||||
expect($meetup->is_active)->toBeFalse()
|
||||
->and($meetup->last_event_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('keeps a meetup active when it only has a future event', function () {
|
||||
$meetup = Meetup::factory()->create(['is_active' => false, 'last_event_at' => null]);
|
||||
MeetupEvent::factory()->create([
|
||||
'meetup_id' => $meetup->id,
|
||||
'start' => now()->addDays(14),
|
||||
'recurrence_type' => null,
|
||||
]);
|
||||
|
||||
$this->artisan('meetups:update-activity')->assertSuccessful();
|
||||
|
||||
$meetup->refresh();
|
||||
expect($meetup->is_active)->toBeTrue()
|
||||
->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]);
|
||||
MeetupEvent::factory()->create([
|
||||
'meetup_id' => $meetup->id,
|
||||
'start' => now()->subYears(3),
|
||||
'recurrence_type' => RecurrenceType::Monthly,
|
||||
'recurrence_end_date' => null,
|
||||
]);
|
||||
|
||||
$this->artisan('meetups:update-activity')->assertSuccessful();
|
||||
|
||||
$meetup->refresh();
|
||||
expect($meetup->is_active)->toBeTrue();
|
||||
});
|
||||
|
||||
it('marks a meetup as inactive when no events exist at all', function () {
|
||||
$meetup = Meetup::factory()->create(['is_active' => true, 'last_event_at' => now()]);
|
||||
|
||||
$this->artisan('meetups:update-activity')->assertSuccessful();
|
||||
|
||||
$meetup->refresh();
|
||||
expect($meetup->is_active)->toBeFalse()
|
||||
->and($meetup->last_event_at)->toBeNull();
|
||||
});
|
||||
|
||||
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([
|
||||
'meetup_id' => $meetup->id,
|
||||
'start' => now()->subYears(3),
|
||||
'recurrence_type' => RecurrenceType::Monthly,
|
||||
'recurrence_end_date' => now()->subYears(2),
|
||||
]);
|
||||
|
||||
$this->artisan('meetups:update-activity')->assertSuccessful();
|
||||
|
||||
$meetup->refresh();
|
||||
expect($meetup->is_active)->toBeFalse();
|
||||
});
|
||||
Reference in New Issue
Block a user