Zapis na zajęcia stałe dla powracających klientów
Klient, którego już znamy, nie musi przechodzić przez zajęcia próbne po raz drugi. W kalendarzu uczestnika ma przycisk, który otwiera ten sam wybór stałych terminów co konwersja po zajęciach próbnych — bez linku z SMS-a, bez weryfikacji telefonu i bez etapu próbnego.
Przebieg
👤 Instrukcja dla recepcji i biura
Gdzie klient znajduje przycisk
Ścieżka: Zajęcia ➔ (uczestnik) — baner nad kalendarzem uczestnika.
Widok Zajęcia otwiera się domyślnie na zakładce Grupowe — to jest oferta, po
którą przychodzi zdecydowana większość klientów, i to tam siedzi zapis na zajęcia stałe.
Zakładkę można zmienić ręcznie, a link z parametrem ?tab=individual albo
?tab=vacation nadal otwiera wskazaną (tak wchodzą linki rejestracyjne z kampanii).
Obok „Zapisz się na próbne zajęcia" pojawia się drugi przycisk „Zapisz się na zajęcia". Widać go tylko wtedy, gdy uczestnik ma już przypisany co najmniej jeden grupowy rodzaj zajęć. To przypisanie jest tym, z czego budowana jest lista terminów — bez niego nie ma czego zaproponować, więc przycisk się nie pokazuje.
Oba przyciski mają osobne warunki. Zaproszenie na próbne zależy od user_type
uczestnika, a ten zostaje pusty dla wszystkich, którzy przeszli publiczną rejestrację —
czyli dokładnie dla tych, którzy później dostają poziom od trenera. Dlatego baner
pokazuje się także wtedy, gdy jedynym dostępnym krokiem jest zapis na zajęcia stałe.
Przypisanie robi trener przy zajęciach próbnych (patrz Poziom uczestnika) albo recepcja ręcznie w profilu uczestnika.
Co widzi klient
To samo okno co przy konwersji z próbnych: cykliczne zajęcia w przypisanej grupie, z trenerem, kortem, dniem i godziną, liczbą wolnych miejsc i kwotą pierwszej płatności.
Różnice wobec konwersji z próbnych:
| Konwersja z próbnych | Powracający klient | |
|---|---|---|
| Wejście | link z SMS-a (/continue/{token}) | przycisk w kalendarzu |
| Logowanie | sesja mintowana z tokenu | klient jest już zalogowany |
| Zakres lokalizacji | adres, na którym odbyły się próbne | wszystkie adresy w mieście klienta |
| Faktura | tak, przed płatnością | tak, przed płatnością |
| Akceptacja regulaminu | wymagana przed płatnością | wymagana przed płatnością |
| Kod rabatowy | brak | tak, przed płatnością |
Zakres lokalizacji różni się celowo: przy próbnych wiadomo, gdzie klient był, a
powracający klient dopiero wybiera, gdzie chce grać. Miasto bierzemy z ustawienia
preferred_city na koncie klienta, a nie z żądania przeglądarki.
Zapis obejmuje cały cykl
Tak samo jak przy konwersji: klient nie wybiera pojedynczych zajęć, tylko wszystkie przyszłe wystąpienia wybranej serii, a płatność online obejmuje pierwszy pełny miesiąc. Miejsce musi być wolne we wszystkich zajęciach cyklu.
Reguła pełnego miesiąca (Minimum zajęć w pierwszej płatności) i blokada zapłaty za
pojedyncze zajęcia działają identycznie — opis w
Konwersji z zajęć próbnych.
Kod rabatowy i faktura
Przed przejściem do Przelewy24 klient może wpisać kod rabatowy i zaznaczyć, że chce fakturę (na osobę prywatną albo na firmę — z wyszukiwaniem danych po NIP).
Kod jest w oknie tylko podglądany. Wiążąca weryfikacja dzieje się w
/api/payments/initialize na realnych płatnościach, więc kod, który przestał być ważny
między otwarciem okna a kliknięciem „Zapłać", zostanie odrzucony tam, a nie po cichu
uznany.
Dane do faktury zapisują się na uczestniku (tabela invoice) przed zapisem na
zajęcia — to je czyta webhook płatności, decydując między fakturą a paragonem.
Okno mówi o tym wprost: pod polami faktury jest informacja, że dane obejmą
wszystkich uczestników z konta i kolejne płatności, a zmienić je można w profilu.
NIP firmy jest sprawdzany sumą kontrolną (odrzucane są też numery z jednej
powtórzonej cyfry, np. 9999999999) — w oknie i ponownie w
/api/regular-enrollment (błąd invoice_tax_id_invalid). Bez tego zły numer
przechodził do Fakturowni, która odmawiała wystawienia dokumentu już po
opłaceniu zapisu — szczegóły w
Wystawianiu faktur.
Akceptacja regulaminu
Na kroku płatności klient musi zaznaczyć akceptację regulaminu zajęć i RODO. Do tego czasu przycisk „Zapłać" jest nieaktywny.
To nie jest wyłącznie blokada w interfejsie: /api/regular-enrollment odrzuca zapis bez
znacznika akceptacji błędem statute_required, więc żaden zapis na zajęcia stałe nie
powstanie bez zgody. Akceptacja zapisuje się w historii pierwszych zajęć cyklu jako
zdarzenie consent_accepted — po jednym wpisie na wybrany termin, widoczne tam, gdzie
recepcja czyta historię zajęć.
„Żaden termin mi nie pasuje"
Działa jak przy konwersji: zgłoszenie trafia jako zadanie na tablicę
(Dashboard ➔ Zadania) z kompletem kontekstu i notatką na leadzie. Etykieta źródła to
zapis powracającego klienta na zajęcia stałe, więc recepcja od razu wie, że to nie jest
klient po próbnych.
Kto może kogo zapisać
Opiekun zapisuje wyłącznie swoich uczestników. Recepcja i biuro (ADMIN,
BACKOFFICE) mogą zapisać dowolnego uczestnika, bo prowadzą ten sam zapis przez telefon.
W obu przypadkach płatność trafia na opiekuna uczestnika, nie na osobę klikającą. Gdyby zapis wykonała sesja recepcji, opiekun nigdy nie zobaczyłby tej płatności ani nie mógłby jej opłacić.
🛠 Dokumentacja techniczna
Kluczowe moduły
| Plik | Rola |
|---|---|
lib/recurring-series-options.ts | Lista stałych terminów + przypisane grupy — wspólna dla obu wejść |
lib/recurring-enrollment.ts | Zapis na cały cykl + oznaczenie płatności monthly_only — wspólne |
lib/regular-enrollment.ts | Autoryzacja uczestnika, opcje i zapis dla zalogowanego klienta |
lib/player-invoice-preferences.ts | Zapis preferencji faktury na uczestniku — wspólny z rejestracją na próbne |
lib/enrollment-consent.ts | Zapis akceptacji regulaminu w historii zajęć — wspólny |
app/api/regular-enrollment/route.ts | options / enroll / no_match |
components/forms/recurring-series/series-option-list.tsx | Karty terminów — wspólne dla obu okien |
components/forms/recurring-series/enrollment-payment-step.tsx | Krok płatności (rabat, faktura, regulamin) — wspólny dla obu okien |
components/forms/regular-enrollment/ | Okno dwuetapowe: wybór terminu → płatność |
.../user-activities/[playerId]/components/EnrollBannerWrapper.tsx | Przycisk w banerze nad kalendarzem |
Migracja 0200 dodaje wiersz route_access dla /api/regular-enrollment.
Dlaczego to nie jest kopia konwersji
AP-649 miał odtworzyć flow z AP-646/647/648 dla innego wejścia. Zamiast kopiować,
wspólna część została wyciągnięta: zapytanie o terminy, wyliczenie pierwszego
miesiąca, oznaczanie płatności i karty w UI mają po jednej implementacji. lib/trial-followup-options.ts
i lib/trial-followup-enroll.ts zostały z tym, co faktycznie dotyczy zaproszenia:
tokenem, jego cyklem życia i mintowaniem sesji.
Konsekwencja praktyczna: zmiana zasad wyliczania pierwszego miesiąca albo tego, co znaczy „wolne miejsce", działa od razu na obu ścieżkach.
Zabezpieczenie przed podwójnym zapisem
Konwersja z próbnych ma token, który można „zająć" (completed_at). Tutaj takiego wiersza
nie ma, więc rolę strażnika pełni sama lista: zapytanie o terminy odrzuca serie, w których
uczestnik już jest (already_enrolled), a enrollInRegularSeries przelicza listę na
nowo i wymaga, żeby wybrana seria wciąż na niej była. Drugie kliknięcie dostaje
series_unavailable zamiast drugiego miesiąca płatności.
Dodatkowo addPlayerToRecurringSeries pomija zajęcia, w których uczestnik już figuruje, i
wystawia płatności wyłącznie za faktycznie dopisane wystąpienia.
Autoryzacja
Endpoint wymaga sesji (getAuthSession), waliduje CSRF i jest limitowany (20 żądań/min).
Właściciel jest rozstrzygany zapytaniem po player.owner_email, a nie po tym, co przyszło
w żądaniu — playerId z ciała żądania nie daje dostępu do cudzego uczestnika.
Miasto pochodzi z sesji (preferred_city), nie z żądania: lista terminów i ustawienia
pierwszego miesiąca są per lokalizacja, więc przeglądarka nie może wskazać klubu, którego
klient nie wybrał.