Skip to content

Rezervace

Tato stránka slouží jako checklist pro testery pro přejmenovaný rezervační modul (větev feature/reservations-rework). Cíle: ověřit nové chování, zkontrolovat kompatibilitu s legacy stavem a odchytit regrese před merge na master.

Popis chování (referenční dokument) je v Rezervace. Zde najdete co se změnilo, jak to otestovat a jaké podmínky očekávat.


Tabulka / ModelStavÚčel
pub_tables / PubTableNováStoly v podniku jako first-class entita (nahrazuje JSON capacity)
alternate_reservations / AlternateReservationNováSamostatná tabulka pro náhradníky (dříve příznak is_alternate=1 na TeamReservation)
standing_reservations.pub_table_idNový sloupecPreferovaný stůl pro autorezervaci
team_reservations.pub_table_idNový sloupecKonkrétně přiřazený stůl
team_reservations.standing_reservation_idNový sloupecOdkaz na nadřazenou autorezervaci
team_reservations.confirmed_atNový sloupecČas potvrzení standing rezervace
team_reservations.notification_72h_sent_atNový sloupecByl odeslán request o potvrzení
team_reservations.notification_48h_sent_atNový sloupecDošlo k auto-zrušení nepotvrzené rezervace
standing_reservations.consecutive_unconfirmed_countNový sloupecPočítač nepotvrzených rezervací (3 = trvalé zrušení)
  1. Kapacita = počet stolů, ne seats/smallseats. Nový výpočet kapacity v Event::getCapacity() a resolveTableLayout() na základě PubTable.
  2. Automatické přiřazení stolu při rezervaci (pokud má podnik PubTables).
  3. Overbooking stolu (max_capacity > optimal_capacity) vyžaduje potvrzení hráče.
  4. Náhradníci (AlternateReservation) – samostatná tabulka, bez přiřazeného stolu, promuje se na TeamReservation při uvolnění místa.
  5. Standing reservations confirmation flow – 72 h před eventem se žádá o potvrzení, 48 h před eventem se nepotvrzené ruší, 3× nepotvrzeno = trvalé zrušení autorezervace.
  6. Soft deletes na všech rezervačních entitách – deaktivace stolu nemaže historická data.
  7. Migrace z legacy capacity JSON – při nasazení se vygenerují PubTable záznamy (velké 6/8, malé 2/4).
CommandFrekvenceCo dělá
reservations:backfillhodinově (:01)Pro každou aktivní StandingReservation zajistí právě jednu budoucí TeamReservation
reservations:reminddenně 10:00Pošle remindery na zítřejší eventy (beze změny, ale ověřit)
reservations:require-confirmationhodinově72 h před eventem pošle týmům s autorezervací žádost o potvrzení
reservations:cancel-unconfirmedhodinově48 h před eventem zruší nepotvrzené standing rezervace

Seeder ReservationSeeder vytvoří:

  • Klasické rezervace na dokončené eventy (podle výsledků)
  • 3–6 rezervací na aktivní eventy, z toho 5+ jako AlternateReservation
  • 2–3 standing reservations per aktivní blueprint + generované TeamReservation
  • Rezervace na speciály (4–6, z toho 5+ jako alternate)
Terminal window
docker-compose exec fpm php artisan migrate:fresh --seed
# Nebo jen rezervace:
docker-compose exec fpm php artisan db:seed --class=ReservationSeeder

  1. Admin → Podniky → detail → sekce Stoly.
  2. Klik na Přidat stoly, zadejte počet (např. 5).
  3. Očekávání:
    • Vzniklo 5 nových stolů s optimal_capacity = 4, max_capacity = 6, sort_order inkrementované.
    • Všechny mají is_active = true.
    • Stoly se zobrazí v tabulce seřazené podle sort_order.
  1. Klik na ikonu tužky u řádku stolu.
  2. Upravte poznámku, optimal/max kapacity, přepněte is_active.
  3. Očekávání: změna se okamžitě promítne do tabulky, neaktivní stůl se přeskočí při dalším přiřazení.
  1. Nastavte stůl, který má přiřazenou budoucí TeamReservation nebo aktivní StandingReservation.
  2. Klik na ikonu koše.
  3. Očekávání: notifikace „Nelze smazat stůl” a akce se zruší (action->halt()).
  1. Stůl bez budoucích rezervací/autorezervací.
  2. Klik na koš → potvrdit.
  3. Očekávání:
    • Soft delete (deleted_at není null).
    • Na historických TeamReservation zůstává pub_table_id – relation pubTable()->withTrashed() ji stále vrátí.
  • Ověřte, že po spuštění migrace 2026_03_23_182648_update_pub_tables_rename_name_to_note_and_seed_from_capacity mají podniky s capacity = [5,5,5] (tří-složkový JSON) vygenerovány odpovídající PubTable záznamy.
  • Velké (seats) → optimal=6, max=8, malé (smallseats) → optimal=2, max=4.

2.1 Happy path – ideální stůl existuje

Section titled “2.1 Happy path – ideální stůl existuje”
  1. Podnik s PubTable optimal=4, max=6 (volný).
  2. Web: vytvořit rezervaci se 4 hráči.
  3. Očekávání:
    • Rezervace vytvořena, pub_table_id vyplněno.
    • E-mail Potvrzení rezervace v Mailhogu.
    • Kapacita eventu klesla o 1 stůl.
  1. Všechny volné stoly mají optimal_capacity < počet hráčů, ale existuje stůl s max_capacity >= počet hráčů.
  2. Submit rezervace.
  3. Očekávání:
    • Stránka se vrátí s dialogem / zprávou confirm_overflow (obsahuje optimal/max/players).
    • Po submitu s confirmed=1 se stůl přiřadí (nejbližší podle |optimal − players|).
  1. Všechny volné stoly mají max_capacity < počet hráčů.
  2. Submit rezervace.
  3. Očekávání: flash danger zpráva „K dispozici jsou jen stoly s maximální kapacitou X hráčů…“.
  1. Všechny stoly obsazené (capacity = 0).
  2. Submit rezervace.
  3. Očekávání:
    • Vzniká záznam v alternate_reservations (ne v team_reservations).
    • Flash: „Váš tým jsme zaregistrovali jako náhradní…”.
    • E-mail ReservationAlternateConfirmation (ne Confirmation!).
    • pub_table_id je NULL.
  1. Tým vytvoří rezervaci, zruší ji (soft delete), a poté znovu vytvoří se stejným team_id a event_id.
  2. Očekávání:
    • Nevznikne duplicitní záznam, původní se obnoví (restore()).
    • Pokud má původní pub_table_id, který je obsazený jiným týmem, systém vybere nejbližší volný stůl.
    • Odejde nová e-mailová notifikace z restored() hooku.
  1. Anonymní: zkusit rezervaci se stejným team_name + stejný event.
  2. Očekávání: flash danger „Tento tým už má aktivní rezervaci”.
  3. Zkusit team_name existující v jiném týmu stejného podniku.
  4. Očekávání: flash danger s nabídkou přihlášení (HTML se strongem a ul).
  1. Podnik, kde nebyla spuštěna migrace stolů (nemá PubTable).
  2. Očekávání:
    • Kapacita se počítá ze sumy capacity minus existující rezervace.
    • Rezervace se vytvoří s pub_table_id = NULL.
    • Systém nevyžaduje potvrzení overflow (tento flow je jen pro PubTable).
  1. Podnik s online_reservations = false.
  2. GET /rezervace/{event} na blueprint event.
  3. Očekávání: abort(401) „Online rezervace pro tento podnik nejsou povoleny”. Speciály tímto nejsou blokovány.

3.1 Potvrzení pozvánky – dostupná kapacita

Section titled “3.1 Potvrzení pozvánky – dostupná kapacita”
  1. Event plný, tým je AlternateReservation, někdo zruší rezervaci.
  2. Očekávání:
    • Job InviteAlternateReservations odešle e-mail ReservationInvitation všem náhradníkům na daném eventu.
    • invited_at na alternate rezervaci se vyplní.
  3. Náhradník kliká na confirmUrl z e-mailu.
  4. Na stránce /rezervace/confirm/{token} potvrdí.
  5. Očekávání:
    • Vzniká TeamReservation (s pub_table_id pokud je PubTable dostupný).
    • AlternateReservation je soft-deleted.
    • Zpráva „Budeme se na váš těšit v…” + link na kvíz.

3.2 Potvrzení pozvánky – kapacita mezitím zmizla

Section titled “3.2 Potvrzení pozvánky – kapacita mezitím zmizla”
  1. Dva náhradníci, event s kapacitou = 1.
  2. První potvrdí → alokuje místo. Druhý kliká na svůj link.
  3. Očekávání:
    • Druhý vidí zprávu „Ajajaj… Někdo byl rychlejší…”.
    • AlternateReservation druhého zůstává beze změny (nepromuje se).
  1. Tým klikne na cancelUrl ze svého confirmation e-mailu (token od AlternateReservation).
  2. Očekávání:
    • AlternateReservation soft-delete.
    • Pokud je event ACTIVE: e-mail ReservationCancellation (observer deleted).
    • Jinak žádný e-mail.
  • Ověřte, že migrace 2026_03_23_150010_migrate_alternate_reservations_data přesunula všechny TeamReservation s legacy příznakem is_alternate=1 do alternate_reservations.
  • Po migraci by v team_reservations neměl být žádný záznam s is_alternate=1.

  1. Přihlášený uživatel zakládá rezervaci, zaškrtne autorezervace.
  2. Očekávání:
    • Vzniká TeamReservation i StandingReservation (active=true).
    • Notifikace StandingReservationCreatedNotification manažerovi týmu.
    • Token se vygeneruje v boot::creating.
  1. Máte aktivní StandingReservation pro blueprint bez existující budoucí TeamReservation.
  2. Vytvořte budoucí event pro tento blueprint (admin).
  3. Spusťte php artisan reservations:backfill (nebo počkejte na hodinovou sked).
  4. Očekávání:
    • Job EnsureExactlyOneFutureTeamReservationJob se dispatche na queue pouze jednou per blueprint (ShouldBeUnique, uniqueFor = 60).
    • Vzniká TeamReservation s standing_reservation_id vyplněným.
    • Stůl se přiřadí v pořadí:
      1. standing_reservation.pub_table_id (pokud je)
      2. Nejbližší volný stůl podle |optimal − players|

4.3 Auto-backfill – už má budoucí rezervaci

Section titled “4.3 Auto-backfill – už má budoucí rezervaci”
  1. Tým už má TeamReservation na některý budoucí event blueprintu.
  2. Spusťte reservations:backfill --blueprint_id=X.
  3. Očekávání: žádná nová rezervace se nevytvoří (log: „Team already has future reservation”).

4.4 Confirmation request – 72 h před eventem

Section titled “4.4 Confirmation request – 72 h před eventem”
  1. TeamReservation se standing_reservation_id, event začíná za 72 h (rozmezí 70–74 h), confirmed_at = NULL, notification_72h_sent_at = NULL.
  2. Spusťte php artisan reservations:require-confirmation.
  3. Očekávání:
    • E-mail StandingReservationConfirmationRequest v Mailhogu.
    • notification_72h_sent_at vyplněn.
    • Command nepošle podruhé (WHERE notification_72h_sent_at IS NULL).
  1. Klik na confirm link v e-mailu → /rezervace/confirm-standing/{token}.
  2. Potvrdit.
  3. Očekávání:
    • TeamReservation::confirmStanding()confirmed_at vyplněn.
    • standing_reservation.consecutive_unconfirmed_count resetován na 0.
    • Redirect na web.reservation.index.
  1. TeamReservation se standing_reservation_id, event začíná za 48 h (rozmezí 46–50 h), confirmed_at = NULL, notification_72h_sent_at IS NOT NULL.
  2. Spusťte php artisan reservations:cancel-unconfirmed.
  3. Očekávání:
    • TeamReservation je soft-deleted (cancelUnconfirmed()).
    • E-mail StandingReservationAutoCancel.
    • Observer deleted pošle ReservationCancellation + dispatche InviteAlternateReservations.
    • standing_reservation.consecutive_unconfirmed_count inkrementován o 1.

4.7 Trvalé zrušení po 3 × nepotvrzení

Section titled “4.7 Trvalé zrušení po 3 × nepotvrzení”
  1. StandingReservation.consecutive_unconfirmed_count = 2, proběhne cancelUnconfirmed (scénář 4.6).
  2. Očekávání:
    • consecutive_unconfirmed_count dosáhne 3.
    • StandingReservation soft-deleted (trvalé zrušení).
    • E-mail StandingReservationCancelledPermanent.
  1. StandingReservation.consecutive_unconfirmed_count = 2.
  2. Tým potvrdí další standing rezervaci.
  3. Očekávání: counter se resetuje na 0 (cyklus nepotvrzování se přeruší).
  1. Hráč klikne na revoke link autorezervace (StandingReservationController::revoke).
  2. Očekávání:
    • StandingReservation soft-delete.
    • Všechny TeamReservation s tímto team_id pro budoucí eventy téhož blueprintu se smažou.
    • Notifikace StandingReservationRevokedNotification manažerovi týmu.
    • Observer deleted odešle Cancellation e-maily za budoucí eventy (status=ACTIVE).
  1. Moderátor v panelu → Stálé rezervace → edit → přiřaď Stůl.
  2. Spusťte reservations:backfill.
  3. Očekávání: nová TeamReservation vygenerovaná z autorezervace má tento pub_table_id (pokud není obsazený; jinak fallback na nejbližší volný).

  1. Dispatch EnsureExactlyOneFutureTeamReservationJob::dispatch($blueprintId) 3× za sebou.
  2. Očekávání: díky ShouldBeUnique + uniqueFor = 60 se vykoná jen 1× během 60 s. Žádné duplicitní TeamReservation.

5.2 Souběžný přístup (race condition)

Section titled “5.2 Souběžný přístup (race condition)”
  1. Dva souběžné requesty na rezervaci stejného týmu na stejný event.
  2. Očekávání: díky lockForUpdate() v job + existing-check v kontroléru nevznikne duplicita. Unique index team_id + event_id je poslední pojistkou.
  1. Event zítra, rezervace s reminder_sent_at = NULL.
  2. php artisan reservations:remind.
  3. Očekávání: e-mail ReservationReminder, reminder_sent_at vyplněn, podruhé se neodešle.

6.1 Admin – detail eventu → záložka Rezervace

Section titled “6.1 Admin – detail eventu → záložka Rezervace”
  • Ověřte, že:
    • Filament tabulka ReservationList zobrazuje aktivní TeamReservation.
    • Oddělená tabulka Náhradníci (AlternateReservationList) pro daný event.
    • Oddělená tabulka Autorezervace (StandingTable) pro daný blueprint.
    • Sloupec Přiřazený stůl zobrazuje Stůl {sort_order} + poznámku, nebo při NULL.

6.2 Moderátor – panel → záložka Rezervace

Section titled “6.2 Moderátor – panel → záložka Rezervace”
  • Ověřte zobrazení ReservationsTable + StandingReservationsTable pod sebou.
  • Edit standing rezervace → přiřazení stolu přes select (jen aktivní stoly podniku, formát Stůl {n} – {note} ({opt}/{max})).
  • Delete standing → confirm modal „Opravdu zrušit?” + danger color + e-mail týmu.
  • StandingTable (admin): filtr Podle kvízu – nabídka jen blueprintů s existujícími standing rezervacemi.
  • AlternateReservationList: řazení podle invited_at pro rychlé ověření, zda už byli pozváni.

7 · E-mailové šablony – kontrola obsahu

Section titled “7 · E-mailové šablony – kontrola obsahu”

Všechny šablony najdete v resources/views/emails/. Ověřte:

ŠablonaTriggerMusí obsahovat
reservation_confirmationnová TeamReservationtým, event, cancel URL
reservation_cancellationsoft delete TeamReservation / AlternateReservation na aktivní eventtým, event
reservation_invitationInviteAlternateReservations jobevent, confirm URL
reservation_alternate_confirmationnová AlternateReservationtým, event, cancel URL
reservation_reminderreservations:remindevent, link
standing_reservation_confirmation_requestreservations:require-confirmationevent, 72 h varování, confirm link
standing_reservation_auto_cancelreservations:cancel-unconfirmedtým, event, důvod (48 h nepotvrzeno)
standing_reservation_cancelled_permanent3× po sobě nepotvrzenodůvod, info o trvalém zrušení

Po změnách rezervací ověřit, že nic dalšího nepraskne:

  • Speciály – vytvoření rezervace, overbooking, náhradníci (Special\ReservationsTable).
  • Anonymní rezervace – cookie anon_reservation_{eventId} na 30 dní; po přihlášení se rezervace onboardne na hráčský profil.
  • Výpis rezervací v profilu hráče – neselže na smazaném eventu (fix z 2026-03-30).
  • Kapacita se počítá bez náhradníků (fix z 2026-03-11).
  • Po zrušení rezervace tlačítko Zpět → správný kvíz / speciál (fix).
  • Po přihlášení-odhlášení-přihlášení: obnovení předchozí soft-deleted rezervace (ne chyba 500, ne unique constraint violation).
  • Blueprint bez budoucího eventu → backfill se hlásí bez erroru, jen zaloguje a odejde.
  • Disable event → QuizzEvent cancel → notifikace EventCanceled všem rezervovaným týmům.

Terminal window
# Stoly v podniku
docker-compose exec fpm php artisan tinker --execute="App\Modules\Pub\Pub::find(1)->pubTables->toArray()"
# Počet aktivních standing rezervací s nenulovým counterem
docker-compose exec fpm php artisan tinker --execute="App\Models\StandingReservation::where('consecutive_unconfirmed_count', '>', 0)->count()"
# Rezervace čekající na 72h potvrzení
docker-compose exec fpm php artisan tinker --execute="App\TeamReservation::whereNotNull('standing_reservation_id')->whereNull('confirmed_at')->whereNull('notification_72h_sent_at')->count()"
# Náhradníci na konkrétním eventu
docker-compose exec fpm php artisan tinker --execute="App\Models\AlternateReservation::where('event_id', 123)->get(['team_name','email','invited_at'])->toArray()"

Před release pustit:

Terminal window
docker-compose exec fpm php artisan test tests/Feature/Reservations/ --compact
docker-compose exec fpm php artisan test tests/Feature/GeneratingReservationFromStandingTest.php --compact
docker-compose exec fpm php artisan test tests/Feature/ReservationControllerTest.php --compact

Pokryté scénáře:

  • AlternateReservationTest – overbooking, promote, duplicates
  • PubTableTest – seed migrace, kapacita z PubTable
  • StandingConfirmationTest – confirm / auto-cancel / permanent delete / commandy
  • TableAssignmentTest – výběr stolu při backfill jobu