Add RSVP functionality for Meetup Events

- 🏷️ Introduce `RsvpStatus` enum for managing attendance states (`attending`, `maybe`, `none`).
- ✏️ Add `MeetupEventController` methods for RSVP actions (`rsvpStatus`, `rsvp`) and payload handling.
-  Implement RSVP helpers in `MeetupEvent` model for user-specific attendance management.
- 🌐 Register RSVP routes for showing and updating attendance in the API.
- 🧪 Add feature tests for RSVP actions, covering validation, idempotency, and correct list handling.
This commit is contained in:
HolgerHatGarKeineNode
2026-06-15 22:10:10 +02:00
parent e55967e9ac
commit 0a1d177fc4
6 changed files with 249 additions and 0 deletions
+74
View File
@@ -3,11 +3,13 @@
namespace App\Models;
use App\Enums\RecurrenceType;
use App\Enums\RsvpStatus;
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;
use Illuminate\Support\Collection;
#[ObservedBy([MeetupEventObserver::class])]
class MeetupEvent extends Model
@@ -62,4 +64,76 @@ class MeetupEvent extends Model
{
return $this->belongsTo(Meetup::class);
}
/**
* Eindeutige Kennung eines angemeldeten Nutzers in den Teilnehmer-Listen.
* Einträge werden als `id_<userId>|<name>` abgelegt; der angehängte Pipe
* grenzt z. B. `id_5` sauber von `id_50` ab.
*/
public static function rsvpIdentifierFor(User $user): string
{
return 'id_'.$user->id;
}
/**
* Prefix, mit dem ein Eintrag des Nutzers in den Listen beginnt inklusive
* Pipe, damit `id_5` nicht auf `id_50` matcht.
*/
private static function rsvpPrefixFor(User $user): string
{
return self::rsvpIdentifierFor($user).'|';
}
/**
* Aktueller RSVP-Status des Nutzers für diesen Termin.
*/
public function rsvpStatusFor(User $user): RsvpStatus
{
$prefix = self::rsvpPrefixFor($user);
if (collect($this->attendees ?? [])->contains(fn ($entry): bool => str($entry)->startsWith($prefix))) {
return RsvpStatus::Attending;
}
if (collect($this->might_attendees ?? [])->contains(fn ($entry): bool => str($entry)->startsWith($prefix))) {
return RsvpStatus::Maybe;
}
return RsvpStatus::None;
}
/**
* Setzt den RSVP-Status des Nutzers: entfernt ihn zunächst aus beiden Listen
* und trägt ihn anschließend in die gewählte Liste ein. `None` sagt nur ab
* (kein erneutes Eintragen). Persistiert die Änderung.
*/
public function setRsvpFor(User $user, RsvpStatus $status, string $name): void
{
$prefix = self::rsvpPrefixFor($user);
$attendees = $this->withoutEntry($this->attendees, $prefix);
$mightAttendees = $this->withoutEntry($this->might_attendees, $prefix);
$entry = $prefix.$name;
match ($status) {
RsvpStatus::Attending => $attendees->push($entry),
RsvpStatus::Maybe => $mightAttendees->push($entry),
RsvpStatus::None => null,
};
$this->update([
'attendees' => $attendees->values()->all(),
'might_attendees' => $mightAttendees->values()->all(),
]);
}
/**
* @param array<int, string>|null $list
* @return Collection<int, string>
*/
private function withoutEntry(?array $list, string $prefix): Collection
{
return collect($list ?? [])->reject(fn ($entry): bool => str($entry)->startsWith($prefix));
}
}