Pełny opis systemu moduł po module: co robi każdy element, jakie udostępnia trasy, jakie metody zawiera, jakie ma pola i z których tabel korzysta oraz co dokładnie dzieje się podczas działania. Dokument wygenerowany na podstawie analizy kodu źródłowego.
Legenda statusów:Aktywny — działa na produkcji ·
Demo — zbudowany, tryb pokazowy/testowy ·
Osobny tenant — pod inną domeną ·
Gotowy — kod gotowy, włączany konfiguracją.
Kwoty pieniężne w bazie przechowywane są w groszach (liczby całkowite).
1.
Architektura multi-tenant
Aktywny
Jedna aplikacja Laravel obsługuje kilka niezależnych serwisów. To, który moduł i która baza danych obsłużą żądanie, zależy od hostu (domeny). Rozstrzyga to middleware ResolveTenant, uruchamiany jako pierwszy — przed sesją i tokenem CSRF — bo musi ustawić bazę i kontekst zanim cokolwiek innego się wykona.
Czyta host żądania i mapuje go na tenant (moduł + tryb + baza + klucz API bramki).
Dynamicznie podmienia bazę połączenia MySQL (config.database.connections.mysql.database) — per żądanie, bez restartu.
Przełącza ścieżki widoków Blade zależnie od trybu (church / products), więc ten sam kontroler renderuje inny wygląd.
Rejestruje singleton app('tenant') dostępny w całej aplikacji oraz klucz API bramki w config('shop.gateway_api_key').
Połączenia bazy
Połączenie mysql jest przełączane per host (sklepy), a dedykowane połączenie gateway wskazuje zawsze na nfc_pay — modele bramki czytają z niego niezależnie od tego, jaki tenant obsługuje bieżące żądanie.
Płatności obsługuje osobny moduł Gateway (własna baza nfc_pay), z którym sklepy komunikują się przez REST API i webhooki. Sklep nigdy nie rozmawia bezpośrednio z PayU — robi to bramka, co pozwala obsłużyć wiele sklepów jednym, bezpiecznym połączeniem z operatorem. Pełny cykl jest wielowarstwowy: webhook + aktywny polling + podpisy kryptograficzne.
Przepływ end-to-end
Inicjacja. Sklep tworzy transakcję w bramce na wybraną kwotę — POST /api/v1/transactions z nagłówkiem X-Api-Key. Bramka zwraca uuid i payment_url.
Autoryzacja. Klient na stronie bramki płaci BLIK-iem (kod 6-cyfrowy) lub pay-by-link (przekierowanie do aplikacji banku). Bramka tworzy zamówienie w PayU (REST v2.1, OAuth).
Webhook PayU → bramka. PayU wysyła powiadomienie POST /webhooks/payu z nagłówkiem OpenPayu-Signature (weryfikacja MD5/SHA256). Bramka oznacza transakcję jako opłaconą/nieudaną.
Polling (gwarancja). Ekran powrotu odpytuje status; bramka aktywnie rekonsyliuje z PayU (getOrderStatus), a transakcje „oczekujące na potwierdzenie” domyka przez capture().
Tryby płatności
classic — klasyczny przepływ z 3DS; app2app — BLIK / pay-by-link z przekierowaniem do aplikacji bankowej. Tryb ustawiany per sklep w polu payment_mode.
3.
Sklep donacyjny NFC (/)
Aktywny
Strona główna serwisu. Wyświetla produkty oznaczone tagami NFC (Serduszko, Kubek, Koszulka, Pin, Brelok). Każdy produkt ma własną minimalną kwotę. Domyślny produkt („Serduszko”, min. 1 zł) otwiera się automatycznie w modalu po wejściu. Kwota jest edytowalna w modalu, a walidacja minimum działa zarówno w przeglądarce, jak i na serwerze. Obsługiwane przez CompanyStoreController.
Trasy
Metoda
URL
Nazwa
Opis
GET
/
home
Lista produktów + modal KUP
POST
/sklep/kup/{slug}
shop.buy
Zakup produktu na wybraną kwotę
Metody kontrolera
index()Pobiera aktywne produkty (sortowanie po sort), wyznacza domyślny (is_default) i renderuje stronę z siatką oraz danymi produktów dla modala (JSON).
purchase($slug)Waliduje kwotę (≥ minimum produktu, ≤ 5000 zł) po stronie serwera, tworzy zamówienie (product_id = null), woła bramkę i przekierowuje do płatności. Komunikat błędu zawiera nazwę produktu i jego minimum.
Pola produktu (model ShopItem → tabela shop_items)
Pole
Typ
Opis
slug
string, unikalny
Identyfikator URL (np. serduszko)
name
string
Nazwa produktu
image
string
Ścieżka do grafiki (raster lub SVG serca)
min_amount
int (grosze)
Minimalna kwota wpłaty (100 = 1 zł)
is_default
bool
Czy produkt domyślny (auto-modal); tylko jeden naraz
tag_uid
string, nullable
UID taga NFC kierującego wprost na ten produkt
active
bool
Widoczność w sklepie
sort
int
Kolejność na liście
Co się dzieje (przepływ zakupu)
Użytkownik wchodzi na / — domyślny produkt otwiera się w modalu (raz na sesję, przez sessionStorage), albo otwiera się produkt wskazany w ?produkt={slug}.
Klik karty produktu otwiera modal z edytowalnym polem kwoty (wstępnie = minimum produktu).
Walidacja w przeglądarce: przy kwocie poniżej minimum przycisk KUP jest blokowany i pojawia się komunikat.
POST /sklep/kup/{slug} — serwer ponownie waliduje kwotę (warstwa nie do obejścia), tworzy Order i transakcję w bramce.
Główna funkcja platformy. Darczyńca zbliża telefon do znacznika NFC w kościele, trafia na stronę parafii, wybiera kwotę i płaci, a na końcu widzi ekran „Bóg zapłać”. Wszystkim steruje StorefrontController; dane parafii zarządzane są w panelu (sekcja 16). Każdy etap loguje zdarzenie do analityki.
Trasy
Metoda
URL
Nazwa
Opis
GET
/main
main
Landing (sekcja 5)
GET
/kategoria/{slug}
category
Lista parafii w kategorii + wyszukiwarka
GET
/t/{tag_uid}
tag
Wejście z taga NFC → przekierowanie
GET
/p/{slug}
product.show
Strona parafii + wybór kwoty
POST
/p/{slug}/kup
product.buy
Utworzenie zamówienia i transakcji
Metody kontrolera
index()Renderuje landing: kategorie wsparcia z bazy (aktywne, najwyższy poziom) z ikoną, etykietą i opisem.
category($slug)Pokazuje parafie danej kategorii (gdy source = parishes) lub pusty stan. Po stronie klienta działa wyszukiwarka po nazwie, mieście i województwie.
tag($tagUid)Szuka parafii po tag_uid → przekierowuje na jej stronę i loguje tag_open. Jeśli to tag produktu sklepu → przekierowuje na sklep z modalem produktu. Nieznany tag → strona 404 „tabliczka nieprzypisana”.
show($slug)Strona parafii; loguje page_view; renderuje formularz wyboru kwoty z presetami.
buy($slug)Waliduje kwotę (2–5000 zł), loguje buy_click, tworzy Order powiązany z parafią i transakcję w bramce, przekierowuje do płatności.
Pola parafii (model Product → tabela products)
Pole
Typ
Opis
name
string
Nazwa parafii
city
string
Miasto
purpose
string
Cel zbiórki (np. „Remont dachu”)
slug
string, unikalny
Identyfikator URL
description_html
text
Opis parafii (WYSIWYG)
price
int (grosze)
Sugerowana kwota tacy (preset bazowy)
tag_uid
string, unikalny
UID taga NFC przypisanego do parafii
main_image
string
Zdjęcie główne parafii
active
bool
Publikacja (sterowana statusem CRM)
phone, website, voivodeship
string
Dane kontaktowe (CRM)
status
enum
CRM: kontakt · test · wdrożenie · aktywna
salesperson_id
FK
Handlowiec opiekujący się parafią
Presety kwot (strona parafii)
Domyślne przyciski: 10, 20, 50, 100, 200 zł + kafelek „inna kwota” zamieniający się w pole liczbowe. Zakres dozwolony: 2–5000 zł. Wybrana kwota aktualizuje etykietę przycisku „Wesprzyj — X zł”.
Co się dzieje (pełny przepływ tacy)
Telefon przy tagu NFC otwiera /t/{tag_uid}.
tag() znajduje parafię, loguje tag_open (lokalnie + asynchronicznie do bramki) i przekierowuje na /p/{slug}.
show() loguje page_view i pokazuje stronę z presetami.
Po wyborze kwoty POST /p/{slug}/kup: buy() loguje buy_click, tworzy zamówienie i transakcję, przekierowuje do PayU.
Po płatności następuje powrót na ekran zwrotu (sekcja 10) — „Bóg zapłać”.
Landing „Technologia, która pomaga czynić dobro” — zbudowany pixel-perfect z makiet Figmy (desktop i mobile). Sekcje są dynamiczne: kategorie „Kogo wspieramy?” pobierane są z bazy i zarządzane z panelu.
Sekcje
Hero — nagłówek z misją platformy.
Kogo wspieramy? — kafelki kategorii (ikona, etykieta HTML, opis); linki do /kategoria/{slug}.
Jak to działa? — 4 kroki: zbliż telefon do NFC → wybierz wsparcie → zapłać → otrzymaj podziękowanie.
Tekst zamykający + stopka z danymi firmy i linkami.
Modal „Wesprzyj” i podgląd linku (OG)
Przycisk „Wesprzyj” w nagłówku otwiera modal domyślnego produktu (/?produkt=serduszko). W <head> ustawione są tagi Open Graph i Twitter z grafiką serca z logo — przy udostępnianiu linku na WhatsApp / Facebook pojawia się ładny podgląd z serduszkiem.
Statyczna strona marketingowa (GET /inwestorzy, nazwa trasy investors) zbudowana 1:1 z makiety Figmy. Hero „Inwestorzy i akcjonariusze”, sekcja misji kapitałowej („Wierzymy, że kapitał może służyć dobru”) oraz karty akcjonariuszy z kwotami inwestycji (kapitałowa + wsparcie usługowe). Renderowana bez kontrolera (Route::view).
Pełny system rekrutacyjny: publiczna lista ofert (zarządzana z panelu), strony ofert, formularz aplikacji z uploadem CV oraz powiadomienie e-mail z załączonym CV. Obsługiwany przez CareersController. CV trafia na prywatny dysk (niedostępny publicznie), a pobrać je można tylko z panelu.
Trasy
Metoda
URL
Nazwa
Opis
GET
/praca
careers
Lista aktywnych ofert
GET
/praca/oferta/{position}
careers.show
Szczegóły oferty
GET/POST
/praca/aplikuj
careers.apply.general
Aplikacja spontaniczna (bez oferty)
GET/POST
/praca/{position}/aplikuj
careers.apply
Aplikacja na konkretną ofertę
Metody kontrolera
index()Lista aktywnych stanowisk; karty z typem zatrudnienia i lokalizacją; wykrywa „pracę zdalną” z treści.
show($position)Pełny opis oferty (WYSIWYG), chipy meta, sekcja „inne oferty”.
applyForm($position?)Renderuje formularz aplikacji (na ofertę lub spontaniczny).
applyStore($position?)Waliduje dane i plik CV, zapisuje zgłoszenie, przenosi CV na prywatny dysk, wysyła e-mail (jeśli skonfigurowany mailer), pokazuje podziękowanie.
Pola oferty (JobPosition → job_positions)
Pole
Typ
Opis
title
string
Nazwa stanowiska
location
string
Lokalizacja
employment_type
string
Rodzaj zatrudnienia
description_html
text
Opis (WYSIWYG)
active
bool
Widoczność publiczna
sort
int
Kolejność
Pola zgłoszenia (JobApplication → job_applications)
Pole
Typ
Opis
job_position_id
FK, nullable
Oferta (null = aplikacja spontaniczna)
name, email, phone
string
Dane kandydata
message
text, nullable
List motywacyjny
cv_path
string
Ścieżka pliku CV na prywatnym dysku
cv_original_name
string
Oryginalna nazwa pliku
is_read
bool
Czy odczytane w panelu
status
enum
pending · accepted · rejected
Walidacja i e-mail
CV: wymagane, do 5 MB, typy pdf/doc/docx (sprawdzane rozszerzenie i MIME). Zgoda RODO: wymagana. Po zapisie generowany jest mail JobApplicationReceived na config('shop.careers_email') z CV w załączniku i adresem kandydata w Reply-To.
Powiadomienia e-mailGotowy — kod wysyłki maila jest kompletny; na produkcji działa tryb „tylko panel” (zgłoszenia + CV trafiają do skrzynki w panelu). Wysyłkę mailem włącza się konfiguracją mailera.
Publiczny formularz kontaktowy (ContactController). Wiadomości zapisywane są do bazy i trafiają do skrzynki w panelu z licznikiem nieprzeczytanych. Temat może być wstępnie wypełniony z linku z oferty pracy (?stanowisko=…).
Po powrocie z bramki OrderReturnController synchronizuje status zamówienia i pokazuje właściwy ekran: sukces („Bóg zapłać”), oczekiwanie (z pollingiem) lub niepowodzenie (z opcją ponowienia). To zabezpiecza przed sytuacją, gdy klient wróci wcześniej niż dotrze webhook.
Trasy i metody
show($order)GET /zwrot/{order} (nazwa order.return) — synchronizuje status z bramki; jeśli opłacone, loguje purchase; renderuje ekran wg statusu.
status($order)GET /zwrot/{order}/status (nazwa order.status) — zwraca JSON {status} dla pollingu.
Ekrany (wg statusu zamówienia)
Status
Widok
Zachowanie
paid
return-success
Animacja świecy, kwota, nazwa parafii, nr potwierdzenia
pending
return-pending
Spinner + polling co 2 s (do ~60 s); po zmianie statusu przeładowanie
failed
return-failure
Komunikat o niepowodzeniu, przyciski „spróbuj ponownie” / „inna parafia”
Sklep komunikuje się z bramką przez serwis GatewayClient oraz odbiera powiadomienia zwrotne (webhooki) o opłaceniu. Zdarzenia analityczne (np. tag_open) wysyłane są asynchronicznie, by nie spowalniać odpowiedzi.
Metody GatewayClient
createTransaction($data)POST do API bramki z danymi: product_external_id, product_name, amount, currency, return_url, notify_url, tag_uid. Zwraca uuid i payment_url.
getTransaction($uuid)Pobiera bieżący status transakcji (używane przy synchronizacji ekranu zwrotu).
sendEvent($type, $tagUid)Wysyła zdarzenie do bramki (np. tag_open).
verifyWebhookSignature($payload, $sig)Weryfikuje podpis HMAC-SHA256 przychodzącego webhooka kluczem API sklepu.
Odbiór webhooka
GatewayWebhookController::handle() (POST /webhooks/gateway) weryfikuje podpis, a następnie ustawia Order.status = paid + paid_at i loguje zdarzenie purchase. SendGatewayEvent (job kolejkowy, dispatchAfterResponse) wysyła eventy do bramki już po odesłaniu odpowiedzi do użytkownika.
Samodzielna bramka płatnicza działająca jako odrębna aplikacja (pay.please-support-me.com, baza nfc_pay). Integruje PayU, obsługuje wiele sklepów, tagi NFC, transakcje, zdarzenia i statystyki. Zawiera 15 kontrolerów, własne API, panel oraz warstwę dostawców płatności (provider).
Dostawca płatności — PayUProvider
createTransaction()Tworzy zamówienie w PayU (POST /api/v2_1/orders, OAuth client_credentials); zwraca provider_order_id i URL przekierowania.
getOrderStatus()Pobiera status zamówienia z PayU (aktywna rekonsyliacja).
capture()Domyka płatność oczekującą na potwierdzenie (PUT statusu → COMPLETED).
payByLinks()Zwraca listę banków do ekranu wyboru (pay-by-link).
handleWebhook()Weryfikuje OpenPayu-Signature i parsuje powiadomienie. Obsługa BLIK Level 0 i pay-by-link.
Interfejs PaymentProviderInterface pozwala podmienić dostawcę; alternatywą jest MockProvider (tryb testowy).
Logika transakcji — TransactionService
reconcileWithProvider()Aktywnie sprawdza status u PayU (gdy webhook się spóźnia).
markPaid() / markFailed()Idempotentnie ustawia status transakcji.
logEvent()Zapisuje zdarzenie transakcji do bazy.
notifyShop()Wysyła webhook wychodzący do sklepu, podpisany HMAC-SHA256.
Bramka udostępnia REST API dla sklepów (autoryzacja kluczem) oraz endpointy płatności i webhooki. Wszystkie powiadomienia są podpisywane i weryfikowane kryptograficznie.
Panel bramki (osobne logowanie) służy do zarządzania sklepami, tagami NFC, statystykami i leadami. Zawiera też moduły demonstracyjne (anti-theft, tryb testowy płatności).
Panel sklepu (/panel) chroniony jest logowaniem (middleware auth). Po zalogowaniu dashboard pokazuje kondycję sprzedaży. Łącznie panel udostępnia ponad 50 tras.
Logowanie (LoginController)
show()GET /panel/login — formularz (przekierowanie do dashboardu, jeśli zalogowany).
Drzewo kategorii „Kogo wspieramy?” (CategoryController) sterujące sekcjami na stronie głównej. Obsługuje zagnieżdżanie (parent/child), zmianę kolejności i ikony, z ochroną przed cyklami.
Metody
index()Spłaszczone drzewo z wcięciami (rekurencja).
store() / update() / destroy()CRUD; usunięcie przenosi dzieci na poziom wyższy.
reorder()Zamiana kolejności (swap pozycji) w górę/dół.
Pola (Category → categories)
Pole
Opis
parent_id
Kategoria nadrzędna (zagnieżdżenie)
label / label_html / label_text
Etykiety (tekst + wersja HTML)
slug
Identyfikator URL (auto z nazwy)
intro
Opis sekcji
icon
Ikona (upload)
source
none (pusta) lub parishes (lista parafii)
position
Kolejność w drzewie
active
Widoczność
Tabele
categories
18.
Panel: Handlowcy
Aktywny
CRUD handlowców (SalespersonController) z przypisaniem obsługiwanych województw i licznikiem przypisanych parafii. Integruje się z CRM parafii i mapą pokrycia.
Pola (Salesperson → salespeople)
Pole
Opis
name
Imię i nazwisko
email, phone
Kontakt (opcjonalne)
voivodeships
Tablica obsługiwanych województw (JSON, 16 do wyboru)
active
Czy aktywny
Tabele
salespeopleproductspotential_parishes
19.
Panel: Parafie do obdzwonienia + mapa pokrycia
Aktywny
Lista leadów parafialnych (PotentialParishController) zaimportowanych z OpenStreetMap, z lejkiem obdzwaniania i interaktywną mapą pokrycia (Leaflet). Filtry: województwo, status, handlowiec, obecność telefonu. Paginacja po 50.
Metody
index()Lista z filtrami i paginacją; liczniki per status.
updateStatus()Zmiana statusu/handlowca/notatki/telefonu (AJAX); ustawia called_at przy pierwszym kontakcie.
coverageData()Zwraca punkty (JSON) do mapy — tylko rekordy ze współrzędnymi, z kolorem wg statusu.
Mapa pokrycia
GET /panel/coverage — mapa Leaflet z klastrowaniem markerów, licznikami per województwo i status, popupami (nazwa, miasto, telefon, status, handlowiec) i filtrami przeładowującymi dane AJAX-em.
CRUD produktów sklepu donacyjnego (ShopItemController): minimalna kwota, tag NFC, produkt domyślny (tylko jeden), upload grafiki, kolejność i aktywność. Cena wpisywana w złotych, zapisywana w groszach.
Metody
index()Lista produktów (sort, miniatura, min. kwota, tag, domyślny, status).
store() / update()Zapis; ustawienie „domyślny” zdejmuje flagę z pozostałych produktów.
CRUD ofert pracy (PositionController) z opisem WYSIWYG, lokalizacją, typem zatrudnienia, kolejnością i licznikiem aplikacji per oferta. Oferty pojawiają się publicznie na /praca.
Skrzynka zgłoszeń rekrutacyjnych (ApplicationController) z pobieraniem CV z prywatnego dysku, statusami i filtrami. Licznik nieprzeczytanych widoczny w nawigacji panelu.
Metody
index()Skrzynka (najnowsze na górze), filtry po ofercie i statusie, liczniki.
show()Szczegóły zgłoszenia; oznacza jako przeczytane.
cv()Pobranie pliku CV z prywatnego dysku.
updateStatus()Zmiana statusu: do sprawdzenia / zaakceptowany / odrzucony.
destroy()Usunięcie zgłoszenia wraz z plikiem CV.
Tabele
job_applicationsjob_positions
23.
Panel: Wiadomości
Aktywny
Skrzynka wiadomości z formularza kontaktowego (MessageController) z licznikiem nieprzeczytanych.
Metody
index()Lista wiadomości (najnowsze na górze).
show()Szczegóły; oznacza jako przeczytane.
destroy()Usunięcie.
Tabele
contact_messages
24.
Eventy i analityka
Aktywny
System rejestruje każde istotne zdarzenie — od zbliżenia telefonu po finalną wpłatę — co zasila statystyki w panelu i pozwala mierzyć konwersję. Zdarzenia trzymane są w tabeli events (z indeksem po typie i dacie), a agregacją zajmuje się ShopStatsService.
Dokument wygenerowany automatycznie na podstawie analizy kodu źródłowego. Moduły oznaczone „Aktywny” działają na produkcji please-support-me.com; „Demo” są zbudowane i działają w trybie pokazowym/testowym.