Rozliczenie zaległością (kredyt na portfelu)
Rozliczenie zaległością pozwala zamknąć płatność za zajęcia lub rezerwację bez przyjmowania pieniędzy. Kwota trafia na portfel klienta jako saldo ujemne, a klient reguluje ją później — najczęściej raz na koniec miesiąca.
Funkcja powstała dla trenerów, którzy rezerwują korty przez cały miesiąc i płacą zbiorczo. Dlatego nie jest dostępna dla wszystkich — trzeba ją włączyć konkretnemu klientowi.
Nie należy jej mylić z blokadą zapisu przy zaległościach: tamta reaguje na nieopłacone płatności po terminie, ta jest świadomie udzielonym kredytem.
👤 Instrukcja dla pracownika
Włączenie klientowi
Ścieżka: Dashboard ➔ Klienci ➔ (klient) ➔ Edycja ➔ Ustawienia ➔ Zaległość
- Włącz przełącznik „Zezwól na rozliczenie zaległością".
- Opcjonalnie wpisz maksymalną zaległość — to limit łącznego minusa na portfelu, a nie pojedynczej operacji.
- Puste pole oznacza limit domyślny dla klubu.
- Pod limitem dodaj usługi na dokumencie przy doładowaniu — bez nich klient nie będzie miał czym uregulować zaległości.
Bez zaznaczenia tej opcji przycisk „Zaległość" w oknie rozliczenia w ogóle się nie pojawia.
Limit domyślny dla klubu
Ścieżka: Dashboard ➔ Ustawienia ➔ Ograniczenia klientów ➔ Domyślny limit zaległości
Ustawiany osobno dla każdego miasta. Obowiązuje klientów, którzy mają włączoną
zaległość i nie mają własnego limitu. Wartość 0 oznacza brak limitu kwotowego.
Rozliczenie przy ladzie
Ścieżka: Dashboard ➔ Grafik ➔ (zajęcia lub rezerwacja) ➔ Rozlicz
Obok metod „Gotówka", „Karta" i „Portfel" pojawia się przycisk „Zaległość". Pod przyciskami widać, ile limitu klientowi jeszcze zostało. Przycisk jest nieaktywny, gdy kwota do rozliczenia przekracza pozostały limit.
Po kliknięciu:
- płatność zostaje oznaczona jako opłacona, metodą „Zaległość" i znika z listy nieopłaconych,
- saldo portfela klienta maleje o tę kwotę i może zejść poniżej zera,
- nie powstaje żaden dokument — paragon ani faktura — bo pieniądze nie wpłynęły,
- kwota nie wchodzi do utargu ani do przychodu.
Co widzi klient
Portfel w aplikacji pokazuje saldo ujemne na czerwono, a w historii portfela pojawia się wpis typu Zaległość z nazwiskiem uczestnika.
Spłata
Klient nie „opłaca zaległości" osobno — doładowanie portfela automatycznie zmniejsza minus, bo dług jest po prostu ujemnym saldem. Wpłata 500 zł przy saldzie −300 zł daje +200 zł.
Gdzie sprawdzić, kto zalega
Ścieżka: Dashboard ➔ Zaległe płatności
Nad zwykłą listą nieopłaconych płatności jest sekcja „Zaległości na portfelu" z klientami, którzy mają ujemne saldo: kwota do uregulowania, limit i data ostatniego obciążenia. Sekcja pokazuje się tylko wtedy, gdy ktoś rzeczywiście ma minus.
W raporcie finansowym rozliczenia zaległością są w osobnej pozycji „Rozliczone na konto" i można je filtrować metodą płatności „Zaległość".
Wpływ na zapisy
Klient, który wyczerpał swój limit, jest traktowany jak klient z zaległością —
recepcja nie dopisze go na zajęcia, dopóki nie ureguluje części długu. Klient z limitem
0 (bez limitu) nie jest blokowany nigdy.
🛠️ Dokumentacja techniczna
Model danych
Migracja 0262_wallet_arrears.sql:
wallet_transaction.transaction_typeprzyjmuje nową wartość'arrears'(przebudowa tabeli — SQLite nie pozwala zmienićCHECKw miejscu),client_settings.allow_arrears(0/1) orazclient_settings.max_arrears_amount(REAL,NULL= limit klubu),- limit domyślny klubu żyje w
app_settingspod kluczemmax_arrears_amount_default(per miasto), więc nie wymagał migracji.
Dlaczego status to paid_wallet, a nie osobny status
Rozliczenie zaległością zapisuje się jako status = 'paid_wallet' z
payment_method = 'arrears'. To celowe — ten status daje za darmo trzy zachowania,
których osobny status wymagałby zaimplementowania od nowa w kilkudziesięciu miejscach:
| Efekt | Skąd wynika |
|---|---|
| Nie powstaje dokument | processPaymentStatusChange generuje dokumenty tylko dla paid, paid_cash, paid_card, paid_online, paid_mixed |
| Nie wchodzi do utargu | getCashBalance liczy wyłącznie cash, card i mixed |
| Płatność jest „rozliczona" wszędzie indziej | paid_wallet należy do SETTLED_PAYMENT_STATUSES |
Metoda arrears jest jedynym rozróżnieniem między długiem a realną zapłatą z portfela.
Rozpoznają ją:
getDisplayStatus— zwraca sztuczny statuspaid_arrears, żeby etykieta brzmiała „Rozliczone zaległością", a nie „Opłacone portfelem",transformPaymentToTransactionwlib/actions/finances.ts— kwalifikuje wpis do osobnego kubełkaarrears, zanim sprawdzipaid_wallet.
Tabela payment nie ma już CHECK na status ani payment_method (zdjęte w
migracji 0137), więc nowa metoda nie wymagała zmiany schematu.
Kluczowe funkcje
getArrearsAllowance(userEmail) zwraca { allowed, limit, balance, available }.
Dialog rozliczenia woła ją zamiast getUserWallet — saldo jest już w odpowiedzi, więc
okno robi jedno zapytanie do serwera zamiast dwóch.
Limit jest sprawdzany dwa razy
Raz w JS (żeby dać czytelny błąd) i raz w samym UPDATE:
UPDATE wallet SET balance = balance - ?
WHERE id = ? AND tenant_id = ? AND (balance - ?) >= ?
Ostatni parametr to -limit. Bez tego dwie osoby przy dwóch stanowiskach mogłyby
jednocześnie przejść walidację na tym samym, nieaktualnym saldzie. Gdy UPDATE nie
trafi w żaden wiersz, operacja zwraca ARREARS_LIMIT_EXCEEDED. Przy limicie 0
(bez limitu) warunek nie jest dokładany.
debitWalletAsArrears celowo nie korzysta z addWalletTransaction — tamta funkcja
odrzuca każde obciążenie, którego saldo nie pokrywa (WHERE balance >= ?), czyli
dokładnie to zabezpieczenie, które tu trzeba ominąć.
Ujemne saldo w odczytach
getUserWallet i getUserWalletBalances przestały obcinać saldo do zera
(Math.max(0, …)), bo minus musi być widoczny. Obcięcie zostało tylko tam, gdzie
saldo oznacza siłę nabywczą — w calculateWalletDiscount. Ścieżki płatności portfelem
(payWithWallet, deductFromWallet, przyciski „Zapłać z portfela") i tak sprawdzają
balance >= amount albo balance > 0, więc klient na minusie nie zapłaci portfelem.
Blokada zapisów
findPlayersWithExhaustedArrearsLimit w lib/enrollment-debt-guard.ts dokłada do
blokady uczestników, których opiekun ma balance + limit <= 0. Działa niezależnie
od przełącznika „Blokuj zapis przez pracownika przy zaległościach" — limit kredytowy
obowiązuje niezależnie od tego, czy klub blokuje zapisy przy zwykłych zaległościach po
terminie.
Testy
__tests__/lib/actions/wallet-arrears.test.ts— zgoda, limity, wyścig na saldzie, trzy scenariusze z ticketu AP-437 (saldo 0, nadpłata większa i mniejsza od kwoty),__tests__/lib/enrollment-debt-guard.test.ts— blokada po wyczerpaniu limitu i brak blokady przy kredycie bez limitu.