# 08. Rezerwacje online

Funkcja, która zmienia Salonio z kalendarza w narzędzie, które samo przynosi wizyty. Klientka umawia się o 23:00, kiedy salon jest zamknięty, a wizyta trafia prosto do kalendarza.

## Adres

`salonio.pl/{slug}`, gdzie slug pochodzi z `salons.slug`. Przykład: `salonio.pl/studio-anna`.

Zasady slugu: małe litery, cyfry i myślniki, od 3 do 60 znaków, unikalny w systemie, nie może być na liście zarezerwowanej z `02-architektura.md`. Zmiana slugu jest możliwa, ale stary adres przestaje działać i system o tym ostrzega.

Strona jest publiczna, nie wymaga logowania i musi wyglądać dobrze na telefonie, bo tam trafi większość ruchu.

## Ścieżka klientki

```
1. Wybór usługi       lista pogrupowana w kategorie, z czasem i ceną
2. Wybór osoby        jeśli usługę wykonuje więcej niż jedna, opcja "dowolna"
3. Wybór dnia         kalendarz miesięczny, dni bez terminów wyszarzone
4. Wybór godziny      lista wolnych terminów w wybranym dniu
5. Dane               imię, nazwisko, telefon, opcjonalnie e-mail i uwaga
6. Zgody              regulamin i przetwarzanie danych, obowiązkowo
7. Potwierdzenie      podsumowanie z datą, godziną, usługą, ceną i adresem
```

Nie ma zakładania konta przez klientkę. Konto to bariera, a dane i tak trafiają do karty klientki w salonie.

## Wyliczanie wolnych terminów

Najtrudniejsza logika w całym systemie. Jedna klasa, `SlotFinder`, i jeden komplet testów, bo tutaj błąd oznacza podwójną rezerwację.

### Wejście

- usługa, a z niej czas trwania i bufor
- pracownik albo "dowolna osoba"
- zakres dat
- ustawienia z `booking_settings`

### Algorytm

```
dla każdego dnia w zakresie:
  dla każdego branego pod uwagę pracownika:
    1. weź godziny pracy z work_schedules dla tego dnia tygodnia
    2. przytnij do godzin otwarcia salonu z business_hours
    3. odejmij urlopy i przerwy z time_off
    4. odejmij istniejące wizyty o statusie innym niż cancelled,
       łącznie z buforami
    5. odejmij aktywne blokady z booking_holds
    6. z pozostałych przedziałów wytnij terminy co slot_interval_min,
       biorąc tylko te, w których zmieści się czas usługi plus bufor
    7. odrzuć terminy wcześniejsze niż teraz plus lead_time_hours
    8. odrzuć terminy dalsze niż dzisiaj plus max_days_ahead
```

### Reguły graniczne

| Sytuacja | Zachowanie |
|---|---|
| Wybrano "dowolna osoba" | Suma terminów wszystkich pracowników, każdy termin raz. Przy rezerwacji przypisywany jest pracownik z najmniejszą liczbą wizyt w tym dniu |
| Usługa bez przypisanych pracowników | Wykonują ją wszyscy umawialni |
| Pracownik nieaktywny albo nieumawialny | Pomijany |
| Wizyta wykraczająca poza godziny pracy | Termin niedostępny, nawet jeśli początek mieści się w grafiku |
| Zmiana czasu z marca i października | Konwersja przez strefę salonu, nie przez dodawanie godzin do znacznika UTC |
| Dzień, w którym salon jest zamknięty | Wyszarzony w kalendarzu |

### Wydajność

Widok miesiąca pyta o wolne terminy dla 30 dni naraz. Wszystkie dane pobierane są trzema zapytaniami na cały zakres: grafiki, wizyty, nieobecności. Reszta liczy się w pamięci. Zapytanie na każdy dzień to 90 zapytań na jedno otwarcie strony i tego być nie może.

Wynik dla widoku miesiąca jest cache'owany na 60 sekund z kluczem zawierającym `salon_id`, `service_id`, `user_id` i miesiąc. Cache jest czyszczony przy każdej zmianie wizyty w tym zakresie.

## Blokada terminu

Między wyborem godziny a potwierdzeniem mija do kilku minut, a w tym czasie ktoś inny może zająć ten sam termin.

```
klientka wybiera godzinę
  → POST /api/rezerwacja/blokuj tworzy wiersz w booking_holds
     z tokenem i czasem wygaśnięcia za 10 minut
  → token wraca do przeglądarki i jedzie razem z formularzem
  → SlotFinder pomija terminy objęte aktywną blokadą
  → potwierdzenie zamienia blokadę na wizytę w transakcji
  → wygasłe blokady kasuje cron co 5 minut
```

Potwierdzenie sprawdza jeszcze raz dostępność terminu w tej samej transakcji, w której zapisuje wizytę. Blokada zmniejsza ryzyko kolizji, ale nie zastępuje sprawdzenia przy zapisie.

## Po rezerwacji

1. Wizyta trafia do kalendarza ze statusem `scheduled` i `source = online`.
2. Jeśli numer telefonu istnieje w bazie salonu, wizyta podpina się do istniejącej klientki. Jeśli nie, powstaje nowa klientka z `source = online`.
3. Klientka dostaje SMS potwierdzający, jeśli salon ma kredyty i włączone potwierdzenia. Jeśli nie ma kredytów, dostaje e-mail, o ile podała adres.
4. Salon dostaje powiadomienie: e-mail do właścicielki, a przy włączonej opcji także SMS.
5. Na dashboardzie pojawia się oznaczenie nowych rezerwacji online od ostatniego logowania.

### Potwierdzanie przez salon

Ustawienie `auto_confirm`:

- **włączone**, domyślnie: wizyta od razu blokuje termin i jest widoczna jako zwykła wizyta
- **wyłączone**: wizyta ma status `scheduled` i wyróżnienie w kalendarzu, salon musi ją potwierdzić albo odrzucić. Odrzucenie wysyła SMS z informacją i proponuje kontakt telefoniczny

## Odwoływanie przez klientkę

Potwierdzenie zawiera link `/{slug}/wizyta/{token}`. Token jest losowy, długi i jednorazowy w tym sensie, że przestaje działać po wizycie.

Pod linkiem klientka widzi szczegóły i, jeśli do wizyty zostało więcej niż `cancel_window_hours`, przycisk odwołania. Po odwołaniu termin wraca do puli, salon dostaje powiadomienie, a wizyta zostaje w bazie ze statusem `cancelled` i powodem "odwołane przez klientkę online".

Poniżej okna czasowego przycisku nie ma, jest za to telefon do salonu. Odwołanie na godzinę przed wizytą to strata dla salonu i nie powinno być wygodne.

## Konfiguracja w panelu

Zakładka "Rezerwacje online" w `/ustawienia`:

| Ustawienie | Domyślnie | Opis |
|---|---|---|
| Rezerwacje włączone | wyłączone | Główny przełącznik |
| Adres | slug salonu | Z podglądem pełnego linku i przyciskiem kopiowania |
| Podział terminów | 15 minut | Co ile minut proponować godziny |
| Minimalne wyprzedzenie | 2 godziny | Ile przed wizytą można się jeszcze zapisać |
| Maksymalne wyprzedzenie | 60 dni | Jak daleko w przód widać terminy |
| Okno odwołania | 24 godziny | Do kiedy klientka może odwołać sama |
| Automatyczne potwierdzanie | włączone | Czy wizyta wymaga akceptacji salonu |
| Tekst powitalny | pusty | Widoczny nad listą usług |
| Zdjęcie w tle | brak | Nagłówek strony |
| Kolor | domyślny | Kolor przycisków |

Pod ustawieniami przycisk "Zobacz, jak to wygląda", otwierający stronę publiczną w nowej karcie.

## Wygląd strony publicznej

To jedyna część systemu, którą widzą klientki salonu, więc musi wyglądać profesjonalnie.

- Nagłówek: zdjęcie w tle, logo, nazwa salonu, adres, telefon.
- Treść: kroki jeden pod drugim, bez przeładowania strony.
- Każdy krok zwija się po wyborze i pokazuje wybraną wartość z możliwością zmiany.
- Podsumowanie widoczne cały czas na dole ekranu na telefonie.
- Bez logo Salonio na eksponowanym miejscu, jedynie dyskretny podpis w stopce.
- Czas wczytania poniżej sekundy, bez zewnętrznych bibliotek.

## Bezpieczeństwo strony publicznej

Jedyny ekran w systemie dostępny bez logowania i zapisujący dane, więc wymaga osobnych zabezpieczeń.

| Zagrożenie | Zabezpieczenie |
|---|---|
| Zalewanie fałszywymi rezerwacjami | Limit 3 rezerwacji na numer telefonu na dobę i 10 na adres IP na godzinę |
| Wyciąganie bazy klientek | Strona publiczna nigdy nie zwraca danych klientek, także przy wpisaniu znanego numeru |
| Podglądanie zajętości salonu | Terminy zwracane są jako lista wolnych godzin, bez informacji, co stoi w zajętych |
| Boty | Pole ukryte typu honeypot i minimalny czas wypełniania formularza, bez captcha na start |
| Odgadywanie tokenów wizyt | Token 32 znaki losowe, limit prób na IP |
| Nadużycie blokad terminów | Maksymalnie 3 aktywne blokady na adres IP |

## Kryteria odbioru etapu 7

- Rezerwacja przechodzi na telefonie w mniej niż 60 sekund.
- Dwie równoległe rezerwacje tego samego terminu kończą się jedną wizytą i czytelnym komunikatem dla drugiej osoby.
- Wolne terminy zgadzają się z kalendarzem w panelu co do minuty, także dla usług z buforem.
- Wizyta z rezerwacji online wygląda w kalendarzu tak samo jak każda inna i ma oznaczenie źródła.
- Klientka istniejąca w bazie nie tworzy duplikatu.
- Test na przejściu czasu zimowego i letniego daje poprawne godziny.
