Zawieszanie konta klienta
Mechanizm automatycznego zawieszania klientów z zaległościami w płatnościach. Zawieszony klient jest oznaczany, traci część benefitów i nie może dokonywać nowych zapisów/rezerwacji — dopóki nie ureguluje zaległości. Uregulowanie może nastąpić od razu przy rezerwacji (spłata zaległości razem z nową rezerwacją w jednej płatności online).
Zawieszenie jest wyliczane w locie na podstawie stanu zaległości i ustawień miasta — nie jest zapisywane jako trwały stan, dzięki czemu automatycznie znika po spłacie.
👤 Instrukcja dla pracownika (Administrator)
Ścieżka: Dashboard ➔ Ustawienia ➔ Ograniczenia dla klientów
Włączenie zawieszania
Przełącznik „Blokuj klientów z zaległościami" jest głównym włącznikiem funkcji. Gdy jest wyłączony, żaden klient nie jest zawieszany, niezależnie od zaległości.
Kryteria zawieszenia
Po włączeniu głównego przełącznika można wybrać jedno lub kilka kryteriów. Spełnienie któregokolwiek z włączonych kryteriów zawiesza konto (logika „LUB"):
- Według liczby dni zwłoki – zawieszenie, gdy najstarsza zaległość przekroczy ustawioną liczbę dni (np. 30).
- Według kwoty zaległości – zawieszenie, gdy suma zaległości osiągnie ustawioną kwotę (zł).
- Według liczby zaległych płatności – zawieszenie, gdy liczba zaległych płatności osiągnie ustawioną wartość.
Kryteria można łączyć dowolnie. Przykład: „30 dni lub 500 zł" oznacza, że klient zostanie zawieszony, gdy którakolwiek zaległość ma ponad 30 dni albo suma zaległości sięga 500 zł.
Ustawienia są definiowane per miasto — każde miasto może mieć własne progi.
Pozostałe ograniczenia
- Blokuj odwoływanie zajęć – zawieszony klient nie odwoła zaplanowanych zajęć.
- Blokuj odrabianie zajęć – zawieszony klient nie zapisze się na odrabianie.
Administrator i backoffice nie podlegają blokadom (mogą działać w imieniu klienta).
👤 Instrukcja dla klienta
Gdy konto jest zawieszone:
- Na ekranie Płatności pojawia się nakładka informująca o zawieszeniu i łącznej kwocie zaległości z przyciskiem szybkiej płatności.
- Zapisy na zajęcia, treningi indywidualne oraz standardowe rezerwacje są zablokowane.
- Rezerwacja kortu jest możliwa pod warunkiem jednoczesnej spłaty zaległości. Przy rezerwacji wyświetlany jest komunikat o zaległościach; po potwierdzeniu klient jest przekierowany do jednej płatności online obejmującej rezerwację + wszystkie zaległości. Po zaksięgowaniu wpłaty konto przestaje być zawieszone.
- W trakcie zawieszenia klient traci benefity: indywidualne (negocjowane) ceny nie są stosowane (obowiązuje cena podstawowa), a kody rabatowe są niedostępne. Karty benefit (np. Multisport) pozostają aktywne.
👨🏫 Widok trenera
Zawieszony uczestnik pozostaje zapisany na zajęcia — nie jest usuwany z grafiku ani listy obecności, więc:
- cena zajęć i rozliczenia nie ulegają zmianie,
- trener widzi uczestnika z czerwoną etykietą „Zawieszony" oraz przekreślonym nazwiskiem,
- obecność można zaznaczać normalnie.
Dzięki temu zawieszenie jest sygnałem informacyjnym dla trenera, a nie operacją wypisania z zajęć.
🛠️ Dokumentacja techniczna
Model danych
Zawieszenie nie ma własnej kolumny w bazie — jest polem wyliczanym (is_suspended, suspension_reason) doklejanym do obiektu Player w czasie odczytu.
Ustawienia przechowywane są w tabeli app_settings (klucz/wartość, per city + tenant_id). Klucze funkcji:
| Klucz | Typ | Domyślnie | Znaczenie |
|---|---|---|---|
block_clients_with_overdue | bool | 0 | Główny włącznik funkcji |
overdue_by_days_enabled | bool | 1 | Kryterium: dni zwłoki |
overdue_threshold_days | int | 30 | Próg dni |
overdue_by_amount_enabled | bool | 0 | Kryterium: kwota |
overdue_threshold_amount | number | 0 | Próg kwoty (zł) |
overdue_by_count_enabled | bool | 0 | Kryterium: liczba płatności |
overdue_threshold_count | int | 0 | Próg liczby zaległych płatności |
Brak migracji schematu — nowe klucze są odczytywane z domyślnymi wartościami, co zachowuje zgodność wsteczną (istniejące konfiguracje działają jak dotąd — tryb „tylko dni").
Centralna logika (lib/actions/payment.ts)
Cała ewaluacja zaległości przechodzi przez jeden zestaw funkcji, aby uniknąć rozjazdu między miejscami użycia:
getOwnerOverdueStats(ownerEmail)→{ totalArrears, overdueCount, maxOverdueDays, paymentIds }— jedno zapytanie agregujące zaległe płatności właściciela (statuspending/overdue,due_datew przeszłości, z pominięciem wygasłych linków płatności).evaluateSuspensionCriteria(settings, stats)→boolean— czysta funkcja reguł wlib/utils/suspension.ts(poza plikiem'use server', bo jest synchroniczna). Zwracatrue, gdy główny włącznik jest aktywny, istnieją zaległości i spełnione jest którekolwiek z włączonych kryteriów (LUB). Gdy nie włączono żadnego kryterium, stosuje próg dni (fallback zgodności wstecznej).isOwnerSuspendedBySettings(ownerEmail, settings)→Promise<boolean>— połączenie powyższych.getCurrentUserOverdueInfo()→{ isSuspended, totalArrears, paymentIds }— dla zalogowanego klienta (na podstawie jegoowner_email).
Miejsca korzystające z tej logiki: appendSuspensionStatus/appendSuspensionStatusBatch (lib/actions/players.ts), blokady zapisów (lib/actions/game.ts), blokady odwołań/odrabiania (lib/actions/substitution.ts), blokada rezerwacji (lib/actions/booking.ts), formularze klienta (grafik, treningi indywidualne).
Utrata benefitów
- Ceny indywidualne — funkcja
applyClientPricingOverride(lib/utils/client-pricing.ts) przyjmuje opcję{ skipForSuspension }. Gdytrue, zwraca bazowy typ aktywności bez nadpisań cenowych. Włączane w ścieżce rezerwacji klienta (createCourtBooking) na podstawieoverdueInfo.blocked. - Kody rabatowe —
validateDiscountCode(lib/actions/discount-codes.ts) na wstępie sprawdza zawieszenie zalogowanego klienta (getCurrentUserOverdueInfo) i zwracareason: 'account_suspended', blokując zastosowanie kodu.
Spłata zaległości przy rezerwacji
Trasa POST /api/payments/initialize przyjmuje tablicę paymentIds (do 50) i tworzy jedną transakcję Przelewy24. Dialog rezerwacji (components/schedule/court-booking-dialog.tsx) po utworzeniu rezerwacji, jeśli istnieją overduePaymentIds, wywołuje processPayments([reservationPaymentId, ...overduePaymentIds]) i przekierowuje do bramki płatności. Po opłaceniu zaległości znikają, więc zawieszenie ustaje automatycznie.
Widok trenera
Etykieta „Zawieszony" i przekreślenie nazwiska renderowane są w app/(dashboard)/dashboard/schedule/components/AttendanceChecker.tsx na podstawie pola is_suspended. Uczestnik nie jest usuwany z zajęć, więc rozliczenia i obecności pozostają nienaruszone.
Testy
__tests__/lib/actions/payment-overdue.test.ts— agregacjagetOwnerOverdueStatsoraz regułyevaluateSuspensionCriteria(dni / kwota / liczba / kombinacja / fallback).__tests__/lib/actions/players.suspension.test.ts— doklejanie statusu zawieszenia do gracza.