Skip to main content

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ść

  1. Włącz przełącznik „Zezwól na rozliczenie zaległością".
  2. Opcjonalnie wpisz maksymalną zaległość — to limit łącznego minusa na portfelu, a nie pojedynczej operacji.
  3. Puste pole oznacza limit domyślny dla klubu.
  4. 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_type przyjmuje nową wartość 'arrears' (przebudowa tabeli — SQLite nie pozwala zmienić CHECK w miejscu),
  • client_settings.allow_arrears (0/1) oraz client_settings.max_arrears_amount (REAL, NULL = limit klubu),
  • limit domyślny klubu żyje w app_settings pod kluczem max_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:

EfektSkąd wynika
Nie powstaje dokumentprocessPaymentStatusChange generuje dokumenty tylko dla paid, paid_cash, paid_card, paid_online, paid_mixed
Nie wchodzi do utargugetCashBalance liczy wyłącznie cash, card i mixed
Płatność jest „rozliczona" wszędzie indziejpaid_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 status paid_arrears, żeby etykieta brzmiała „Rozliczone zaległością", a nie „Opłacone portfelem",
  • transformPaymentToTransaction w lib/actions/finances.ts — kwalifikuje wpis do osobnego kubełka arrears, zanim sprawdzi paid_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.