Moduł: Voucher shop (sprzedaż online)
Sklep voucherów to moduł publicznej strony klubu. Pozwala klientom (nie-członkom) kupić online vouchery do realizacji w klubie — bez kontaktu z biurem, 24/7.
URL sklepu: portal.shootero.pl/SLUG/vouchers.
Wymaga włączonego public_module_vouchers w /club/public-page + skonfigurowanej bramki P24.
Typy voucherów
Ticket (bilet na konkretną usługę)
Voucher z fixed ceną i fixed zakresem (np. "1 godzina strzelania + 50 sztuk amunicji 9mm za 120 PLN").
Realizacja: jednorazowa, w 100%. Po wpisaniu kodu i potwierdzeniu — status redeemed, nie można użyć ponownie.
Use case: standardowa oferta klubu, prezent dla osoby która chce konkretnie postrzelać.
Gift card (karta podarunkowa)
Voucher z kwotą PLN (np. 200 PLN). Klient kupuje, klub rozlicza dowolnie:
- jedna duża transakcja (200 PLN za sesję) → status
redeemed - wiele małych (50 + 30 + 70 + 50) → status
partially_redeemed→redeemedgdy saldo = 0
Use case: prezent dla osoby, która sama wybierze co zrobi w klubie.
Konfiguracja katalogu
Wymagana rola: admin lub zarzad.
/club/vouchers → przycisk "Nowy voucher".
| Pole | Opis |
|---|---|
| Nazwa | np. "1h strzelania + amunicja", "Karta podarunkowa 200 PLN" |
| Typ | ticket / giftcard |
| Cena | w PLN, większa od zera |
| Termin ważności | dni od zakupu (1-3650), domyślnie 365 |
| Opis | dowolny tekst, wyświetlony w sklepie i na PDF |
| Sortowanie | porządek w sklepie (mniejsze pierwsze) |
| Zakres (tylko ticket) | czas w min, sztuki amunicji, typ amunicji, dodatkowe uwagi |
| Aktywny | jeśli odznaczone — niewidoczny w sklepie |
Soft delete: "Usunięcie" w panelu = is_active=0 (chroni przed kaskadą gdy istnieją zamówienia/kody).
Workflow zakupu
- Klient wchodzi na
portal.shootero.pl/SLUG/vouchers→ lista aktywnych voucherów. - Wybiera voucher → strona detalu z formularzem (email, imię i nazwisko, telefon opcjonalny, zgoda RODO).
- POST
/checkout→ tworzymyvoucher_order(status=pending) +online_paymentsrow → P24 register → redirect na bramkę. - P24 → klient płaci → redirect na
/SLUG/vouchers/return?session=...(pokazujemy "Sprawdzamy płatność"). - P24 → webhook na
/SLUG/vouchers/notifyz signed payload (SHA384) → weryfikacja + verifyTransaction API. - Po weryfikacji:
voucher_order.status='paid',voucher_codegenerowany (XXXX-XXXX-XXXX), email do klienta z kodem + linkiem do PDF. - Klient pobiera PDF z
/vouchers/code/CODE/pdf(public, kod = auth) — branding klubu + QR code z kodem.
Workflow realizacji
Wymagana rola: admin, zarzad, instruktor lub księgowość.
/club/vouchers/redeem:
- Recepcja wpisuje kod (z PDF lub QR pokazanego przez klienta).
- System znajduje voucher w bazie (tenant-scoped) i wyświetla:
- Typ, nazwę, opis
- Status, ważność
- Dla giftcard: bieżące saldo
- Historia poprzednich realizacji (audit log)
- Dla ticket: 1-click "Zrealizuj" → status=
redeemed. - Dla giftcard: wpisz kwotę (≤ saldo) → odejmowane od
balance_current→ status=partially_redeemedlubredeemedgdy saldo=0.
Każda realizacja loguje się w voucher_code_redemptions (audit: kto, kiedy, ile, notatka).
Historia zamówień
/club/vouchers/orders — lista wszystkich zamówień klubu (filterowalna po status: pending/paid/failed/cancelled).
Pokazuje: data, voucher, kupujący (email/imię), kwota, status.
Wymagana rola: admin, zarzad lub księgowość.
Cykl życia kodu
[guest checkout]
↓
voucher_order(status=pending) + online_payments(status=pending)
↓ (P24 redirect)
↓ (klient płaci na bramce)
↓ (P24 webhook notify)
voucher_order(status=paid) + voucher_code(status=active) + email wysłany
↓ (klient pokazuje kod na recepcji)
↓ (admin wpisuje kod w /club/vouchers/redeem)
voucher_code(status=redeemed | partially_redeemed) + voucher_code_redemptions log
Edge cases:
- P24 anuluje/odrzuci → webhook ustawia
voucher_order.status='failed', klient widzi "Płatność nieudana". - Webhook się nie zdarzy → klient widzi "Sprawdzamy płatność" z auto-refresh; cron może po godzinie oznaczyć jako
expired. - Kod wygasły (valid_until < today) → przy próbie realizacji błąd "voucher wygasł".
- Próba 2× tego samego ticket → atomic UPDATE z
status='active'filterem — drugi raz fail. - Gift card amount > saldo → atomic check
balance_current >= amount— fail.
Email do klienta
Po pomyślnej płatności automatyczny email z:
- Kod vouchera (monospace, duży, widoczny)
- Link "Pobierz voucher PDF" →
/vouchers/code/CODE/pdf(PDF z QR + branding klubu) - Termin ważności
- Link na publiczną stronę klubu
Email wysyłany przez EmailService (SMTP klubu jeśli skonfigurowany, fallback globalny).
RODO / dane osobowe
Voucher_orders przechowuje dane osobowe klienta: email, imię i nazwisko, opcjonalny telefon.
- Klient musi wyrazić zgodę RODO przy checkout (checkbox z linkiem do polityki).
- Klub jest administratorem tych danych (Shootero = procesor wg DPA).
- Realizacja praw osób (eksport, usunięcie) — przez funkcje admin paneli +
gdpr/anonymize(TODO: rozszerzyć żeby anonimizować voucher_orders). - Retencja: zalecane 5 lat (księgowość) dla statusu
paid, można usunąć po tym okresie.
Bezpieczeństwo
- Tenant isolation: queries scoped per
club_id(przez ClubScopedModel + defense in depthAND club_idw atomic UPDATE-ach). - Code generation: 12-char alphanumeric (alfabet bez 0/O/I/1 — 32^12 ≈ 10^18 kombinacji), unique check z retry max 50.
- Webhook signature: SHA384 verify (P24 native).
- Webhook idempotency:
markPaidjest atomic zWHERE status='pending'— duplicate webhook = no-op. - Cross-verify: po signature OK, dodatkowo wywołujemy P24
verifyTransactionAPI (defense in depth przed sygnatura collision attack). - PDF auth: kod jako URL token — wystarczająca entropia. Klient otrzymuje link tylko emailem (chyba że dostanie się do skrzynki).
Wymagania pre-deploy
- Migracja
migration_v40.sqlzaaplikowana (Phase 1 PR #11). - Konfiguracja P24 per klub w
club_settings. - Włączony moduł
public_module_vouchersw/club/public-page. - Skonfigurowany SMTP per klub (lub globalny) dla wysyłki emaili z kodami.
- Internet access z serwera (api.qrserver.com dla QR fallback).
Powiązane
- Publiczna strona klubu — kontekst (moduł nadrzędny)
- Konfiguracja klubu — P24, SMTP
- Polityka prywatności — RODO klauzula
- Regulamin — sekcja "Plany i opłaty"