# 07. Płatności i abonament

## Plany

| | BASIC | PRO |
|---|---|---|
| Cena miesięczna | 39 zł | 69 zł |
| Cena roczna | 390 zł (2 miesiące gratis) | 690 zł |
| Kalendarz, klientki, wizyty, usługi | tak | tak |
| Konta pracowników | 1 | bez limitu |
| Uprawnienia pracowników | nie | tak |
| Statystyki | podstawowe | pełne, z podziałem na pracowników |
| Historia klientki | ostatnie 3 wizyty | pełna |
| Automatyczne SMS-y | nie | tak |
| Rezerwacje online | nie | tak |
| Wsparcie | e-mail | e-mail z priorytetem |

Pakiety SMS sprzedawane są osobno, niezależnie od planu. Plan BASIC nie ma automatycznych wysyłek, ale ma wysyłkę ręczną z panelu, żeby każdy salon mógł kupić kredyty.

Definicje planów leżą w `config/plans.php` i w tabeli `plans`. Identyfikatory cen Stripe trzymane są w bazie, nie w kodzie, żeby zmiana cennika nie wymagała wgrywania plików.

## Okres próbny

14 dni pełnego planu PRO, bez podawania karty. Po założeniu konta `salons.status` to `trial`, a `trial_ends_at` to data założenia plus 14 dni.

| Dzień | Co widzi użytkowniczka |
|---|---|
| 1 | Nic, pracuje normalnie |
| 7 | Dyskretna informacja, ile dni zostało |
| 11 | Pasek z propozycją wyboru planu |
| 14 | Ekran wyboru planu przy logowaniu, z możliwością pominięcia |
| 15 | Tryb tylko do odczytu, dane widoczne, dodawanie zablokowane |
| 45 | Konto zawieszone |

Brak karty na starcie zwiększa liczbę rejestracji. Kto ma wpisywać kartę przed sprawdzeniem produktu, zwykle go nie sprawdza.

## Stripe

### Co robi Stripe, a czego nie

Stripe prowadzi subskrypcje, pobiera płatności, ponawia nieudane obciążenia i wystawia dokumenty płatnicze. Salonio przechowuje wyłącznie identyfikatory obiektów Stripe i status dostępu. Numery kart nie przechodzą przez nasz serwer i nie ma ich w bazie. Cały proces płatności odbywa się na stronie Stripe Checkout.

### Konfiguracja

| Klucz | Gdzie |
|---|---|
| `stripe.secret_key` | `config/config.php`, poza katalogiem publicznym |
| `stripe.publishable_key` | `config/config.php` |
| `stripe.webhook_secret` | `config/config.php` |
| Identyfikatory cen | `plans.stripe_price_month`, `plans.stripe_price_year`, `sms_packages.stripe_price_id` |

### Rozpoczęcie subskrypcji

```
/abonament → wybór planu i okresu
  → serwer tworzy Checkout Session w trybie subscription
     z client_reference_id = salon_id
     i metadata: salon_id, plan_code
  → przekierowanie na Stripe
  → powrót na /abonament/dziekujemy
  → dostęp zmienia dopiero webhook, nie powrót z bramki
```

Powrót z bramki tylko wyświetla informację, że płatność jest przetwarzana. Nadawanie dostępu na podstawie powrotu z bramki jest klasyczną dziurą, bo adres powrotu da się otworzyć ręcznie.

### Zdarzenia webhooka

Adres `/webhook/stripe`, bez CSRF, z obowiązkową weryfikacją podpisu `Stripe-Signature`. Żądanie bez poprawnego podpisu kończy się kodem 400 i wpisem do logu.

| Zdarzenie | Co robimy |
|---|---|
| `checkout.session.completed` | Zapisujemy `stripe_customer_id`, tworzymy wiersz w `subscriptions`. Przy zakupie pakietu SMS dopisujemy kredyty |
| `customer.subscription.created` | Tworzymy albo aktualizujemy subskrypcję, ustawiamy `plan_code` |
| `customer.subscription.updated` | Aktualizujemy status, plan i daty okresu. Obsługuje też zmianę planu i włączenie anulowania na koniec okresu |
| `customer.subscription.deleted` | Status `cancelled`, dostęp do końca opłaconego okresu |
| `customer.subscription.trial_will_end` | E-mail trzy dni przed końcem okresu próbnego w Stripe, jeśli używamy okresu próbnego po stronie Stripe |
| `invoice.paid` | Przedłużamy `access_until` do `current_period_end` plus 2 dni zapasu, zapisujemy płatność, status salonu na `active` |
| `invoice.payment_failed` | Status `past_due`, e-mail do właścicielki, start procedury z `04-uprawnienia-i-role.md` |
| `invoice.payment_action_required` | E-mail z linkiem do dokończenia uwierzytelnienia karty |
| `charge.refunded` | Zapis zwrotu, przy pakiecie SMS odjęcie kredytów, jeśli nie zostały zużyte |

### Odporność webhooka

1. **Jednokrotne przetworzenie.** Każde zdarzenie najpierw trafia do `stripe_events` z unikalnym `stripe_event_id`. Naruszenie unikalności oznacza duplikat i kończy obsługę odpowiedzią 200. Stripe potrafi wysłać to samo zdarzenie kilka razy.
2. **Najpierw zapis, potem praca.** Odbieramy, zapisujemy surowe zdarzenie, odpowiadamy 200, przetwarzamy w kolejce. Długie przetwarzanie w trakcie żądania kończy się przekroczeniem czasu i ponowieniem po stronie Stripe.
3. **Kolejność.** Zdarzenia potrafią przyjść w innej kolejności niż powstały. Przy każdej aktualizacji subskrypcji porównujemy znacznik czasu zdarzenia z `subscriptions.updated_at` i starsze pomijamy.
4. **Widoczność.** Nieprzetworzone zdarzenia starsze niż 15 minut pokazują się w `/admin/health` jako ostrzeżenie.

### Źródło prawdy o dostępie

Dostęp do systemu wynika wyłącznie z `salons.access_until` i `salons.status`. Aplikacja nie pyta Stripe przy każdym logowaniu, bo awaria Stripe zablokowałaby wszystkich użytkowników. Webhook jest jedynym miejscem, które te pola zmienia. Raz na dobę zadanie `ReconcileSubscriptions` porównuje stan lokalny ze Stripe i wypisuje różnice do logu oraz do `/admin/health`.

## Zmiana planu

- **Podwyższenie** działa natychmiast, Stripe wylicza proporcjonalną dopłatę.
- **Obniżenie** działa od kolejnego okresu, żeby nie wystawiać zwrotów.
- Obniżenie do BASIC przy więcej niż jednym aktywnym pracowniku wymaga wcześniejszej dezaktywacji kont. Ekran mówi wprost, ile kont trzeba wyłączyć.
- Obniżenie wyłącza automatyczne SMS-y i rezerwacje online od pierwszego dnia nowego okresu. Kredyty SMS zostają, bo są kupione osobno.

## Anulowanie

Anulowanie działa na koniec opłaconego okresu, `cancel_at_period_end`. Do tego dnia nic się nie zmienia. Przy anulowaniu użytkowniczka dostaje propozycję eksportu danych i informację, kiedy dane zostaną usunięte.

## Faktury

Stripe wystawia dokumenty płatnicze i udostępnia je pod adresem `invoice_url`, który zapisujemy w `payments`. Lista faktur w `/abonament` jest listą linków do Stripe.

Salon z numerem NIP dostaje fakturę z tym numerem, pole NIP jest w ustawieniach salonu i trafia do Stripe jako `tax_id`. Kwestię VAT i tego, czy sprzedaż prowadzi polska firma z kasą fiskalną, trzeba ustalić z księgowością przed uruchomieniem sprzedaży. To decyzja biznesowa, nie techniczna, i musi zapaść przed etapem 6.

## Tryb testowy

Etap 6 powstaje w całości na kluczach testowych Stripe. Przejście na klucze produkcyjne to osobny punkt kryteriów odbioru, z listą do sprawdzenia:

- webhook produkcyjny wskazuje na adres produkcyjny i ma własny sekret
- identyfikatory cen w bazie są produkcyjne
- karta testowa `4242 4242 4242 4242` przestaje działać, co jest oczekiwane
- pierwsza prawdziwa płatność wykonana własną kartą i sprawdzona w bazie
- procedura zwrotu przetestowana na tej płatności
