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_redeemedredeemed gdy 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".

PoleOpis
Nazwanp. "1h strzelania + amunicja", "Karta podarunkowa 200 PLN"
Typticket / giftcard
Cenaw PLN, większa od zera
Termin ważnościdni od zakupu (1-3650), domyślnie 365
Opisdowolny tekst, wyświetlony w sklepie i na PDF
Sortowanieporządek w sklepie (mniejsze pierwsze)
Zakres (tylko ticket)czas w min, sztuki amunicji, typ amunicji, dodatkowe uwagi
Aktywnyjeśli odznaczone — niewidoczny w sklepie

Soft delete: "Usunięcie" w panelu = is_active=0 (chroni przed kaskadą gdy istnieją zamówienia/kody).

Workflow zakupu

  1. Klient wchodzi na portal.shootero.pl/SLUG/vouchers → lista aktywnych voucherów.
  2. Wybiera voucher → strona detalu z formularzem (email, imię i nazwisko, telefon opcjonalny, zgoda RODO).
  3. POST /checkout → tworzymy voucher_order (status=pending) + online_payments row → P24 register → redirect na bramkę.
  4. P24 → klient płaci → redirect na /SLUG/vouchers/return?session=... (pokazujemy "Sprawdzamy płatność").
  5. P24 → webhook na /SLUG/vouchers/notify z signed payload (SHA384) → weryfikacja + verifyTransaction API.
  6. Po weryfikacji: voucher_order.status='paid', voucher_code generowany (XXXX-XXXX-XXXX), email do klienta z kodem + linkiem do PDF.
  7. 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:

  1. Recepcja wpisuje kod (z PDF lub QR pokazanego przez klienta).
  2. 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)
  1. Dla ticket: 1-click "Zrealizuj" → status=redeemed.
  2. Dla giftcard: wpisz kwotę (≤ saldo) → odejmowane od balance_current → status=partially_redeemed lub redeemed gdy 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 depth AND club_id w 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: markPaid jest atomic z WHERE status='pending' — duplicate webhook = no-op.
  • Cross-verify: po signature OK, dodatkowo wywołujemy P24 verifyTransaction API (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

  1. Migracja migration_v40.sql zaaplikowana (Phase 1 PR #11).
  2. Konfiguracja P24 per klub w club_settings.
  3. Włączony moduł public_module_vouchers w /club/public-page.
  4. Skonfigurowany SMTP per klub (lub globalny) dla wysyłki emaili z kodami.
  5. Internet access z serwera (api.qrserver.com dla QR fallback).

Powiązane