Przejdź do głównej zawartości

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óbnychPowracający klient
Wejścielink z SMS-a (/continue/{token})przycisk w kalendarzu
Logowaniesesja mintowana z tokenuklient jest już zalogowany
Zakres lokalizacjiadres, na którym odbyły się próbnewszystkie adresy w mieście klienta
Fakturatak, przed płatnościątak, przed płatnością
Akceptacja regulaminuwymagana przed płatnościąwymagana przed płatnością
Kod rabatowybraktak, 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

PlikRola
lib/recurring-series-options.tsLista stałych terminów + przypisane grupy — wspólna dla obu wejść
lib/recurring-enrollment.tsZapis na cały cykl + oznaczenie płatności monthly_onlywspólne
lib/regular-enrollment.tsAutoryzacja uczestnika, opcje i zapis dla zalogowanego klienta
lib/player-invoice-preferences.tsZapis preferencji faktury na uczestniku — wspólny z rejestracją na próbne
lib/enrollment-consent.tsZapis akceptacji regulaminu w historii zajęć — wspólny
app/api/regular-enrollment/route.tsoptions / enroll / no_match
components/forms/recurring-series/series-option-list.tsxKarty terminów — wspólne dla obu okien
components/forms/recurring-series/enrollment-payment-step.tsxKrok 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.tsxPrzycisk 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ł.