Przejdź do treści

Opinie (Review)

Ostatnia aktualizacja dokumentacji: 26 lutego 2026 Stan synchronizacji z kodem: ✅ Zsynchronizowany

1. Opis ogólny

Domena Opinii zarządza ocenami i komentarzami klientów po zakończonych rezerwacjach. To jak książka gości w hotelu — po zakończonej rezerwacji klient może wystawić ocenę (1–5 gwiazdek) i napisać komentarz, który pomoże innym w wyborze obiektu lub zajęć. System obsługuje polimorficzne opinie (przypisywane do obiektów Item lub aktywności Activity), moderację przez administratorów oraz automatyczne przypomnienia o wystawieniu opinii.


2. Architektura domeny

2.1 Model danych — Review

Właściwość Wartość
Klasa Domain\Review\Model\Review
Tabela reviews
Klucz główny UUID (HasUuids)
Bazuje na Infrastructure\Vendor\CustomModel
Observer ReviewObserver (atrybut #[ObservedBy])

Fillable:

Pole Typ (cast) Opis
reviewable_type string Typ polimorficzny (Item::class lub Activity::class)
reviewable_id string (UUID) ID obiektu/aktywności
organization_id string (UUID) ID organizacji
reservation_id string (UUID) ID rezerwacji (unique)
rating integer Ocena 1–5
comment string Komentarz tekstowy (nullable)
is_active boolean Czy opinia jest widoczna
disabled_by string (UUID) ID użytkownika, który ukrył opinię
disabled_at datetime Data ukrycia opinii

Filtry publiczne ($publicFilters): id, reviewable_type, reviewable_id, organization_id, reservation_id, rating

Relacje:

Relacja Typ Model docelowy Opis
reviewable() MorphTo Item / Activity Oceniany obiekt lub aktywność
organization() BelongsTo Organization Organizacja powiązana z opinią
reservation() BelongsTo Reservation Rezerwacja, z której wynika opinia
disabler() BelongsTo (FK: disabled_by) User Użytkownik, który ukrył opinię

Relacje odwrotne w innych modelach:

Model Relacja Typ
Reservation review() HasOne
Item reviews() MorphMany
Activity reviews() MorphMany
Organization reviews() HasMany

2.2 Diagram relacji

erDiagram
    Review ||--o| Reservation : "reservation_id"
    Review }o--|| Organization : "organization_id"
    Review }o--o| User : "disabled_by"
    Review }o--|| Item : "reviewable (morph)"
    Review }o--|| Activity : "reviewable (morph)"

    Reservation ||--o| Review : "hasOne"
    Item ||--o{ Review : "morphMany"
    Activity ||--o{ Review : "morphMany"
    Organization ||--o{ Review : "hasMany"

2.3 Struktura tabeli reviews

Kolumna Typ Nullable Default Opis
id uuid Nie Klucz główny
reviewable_type varchar Nie Typ polimorficzny (MorphTo)
reviewable_id uuid Nie ID obiektu polimorficznego
organization_id uuid Tak NULL FK → organizations.id (CASCADE)
reservation_id uuid Tak NULL FK → reservations.id (CASCADE), UNIQUE
rating unsigned tinyint Nie Ocena 1–5
comment text Tak NULL Komentarz
is_active boolean Nie true Widoczność
disabled_by uuid Tak NULL FK → users.id (SET NULL)
disabled_at datetime Tak NULL Data dezaktywacji
created_at timestamp Tak NULL Utworzenie
updated_at timestamp Tak NULL Ostatnia aktualizacja

Indeksy: organization_id (index), reservation_id (index + unique), reviewable_type + reviewable_id (morph index)

Klucze obce:

  • organization_idorganizations.id ON DELETE CASCADE
  • reservation_idreservations.id ON DELETE CASCADE
  • disabled_byusers.id ON DELETE SET NULL

3. Endpointy API

3.1 Endpointy publiczne (bez autoryzacji)

GET /api/reviews

  • Opis: Lista opinii z filtrowaniem i paginacją (publiczny dostęp)
  • Route name: no-auth-reviews.index
  • Autoryzacja: Brak — endpoint publiczny
  • Kontroler: ReviewController@indexIndexReviewController
  • Request: IndexReviewControllerRequest

Query Parameters:

Parametr Typ Reguły walidacji Opis
page integer Paginacja domyślna Numer strony
per_page integer Paginacja domyślna Rekordów na stronę
filter[id] uuid exact Filtr po ID
filter[reviewable_type] string exact Filtr po typie obiektu
filter[reviewable_id] uuid exact Filtr po ID obiektu
filter[organization_id] uuid exact Filtr po organizacji
filter[reservation_id] uuid exact Filtr po rezerwacji
filter[rating] integer exact Filtr po ocenie
filter[organization.name] string partial Filtr po nazwie organizacji
filter[disabler.name] string partial Filtr po nazwie osoby ukrywającej
filter[reservation.first_name] string partial Filtr po imieniu z rezerwacji
filter[reservation.last_name] string partial Filtr po nazwisku z rezerwacji

Eager loading: reviewable, reservation, organization, disabler, reservation.latestNote, reviewable.media

Response (sukces, 200):

Redukcja pól dla niezalogowanych

Dla niezalogowanych użytkowników następujące pola są ukrywane z odpowiedzi: reservation, reservation_id, disabler, disabled_by, disabled_at, can.

{
  "data": [
    {
      "id": "uuid",
      "reviewable_type": "Domain\\Item\\Model\\Item",
      "reviewable_id": "uuid",
      "organization_id": "uuid",
      "rating": 4,
      "comment": "Świetne wyposażenie",
      "can_be_edit": true,
      "is_active": true,
      "created_at": "2026-02-20 14:30:00",
      "updated_at": "2026-02-20 14:30:00",
      "reviewable": { "..." },
      "organization": { "..." }
    }
  ],
  "meta": { "..." }
}

POST /api/reviews

  • Opis: Dodanie nowej opinii do rezerwacji (publiczny dostęp)
  • Route name: no-auth-reviews.store
  • Autoryzacja: Brak — endpoint publiczny
  • Middleware: throttle:public-store (rate limiting)
  • Kontroler: ReviewController@storeStoreReviewControllerCreateReview
  • Request: StoreReviewControllerRequest

Body (Request):

Pole Typ Reguły walidacji Opis
reservation_id string required\|uuid\|exists:reservations,id + ReservationStillReviewableRule ID rezerwacji
reservation_uuid string required\|uuid\|max:255 UUID rezerwacji
reservation_access_code string required\|string\|min:1\|max:20 + VerifyReservationCorrectDataRule Kod dostępu do rezerwacji
rating integer required\|integer\|min:1\|max:5 Ocena (1–5 gwiazdek)
comment string nullable\|string\|max:2000\|blasp_check Komentarz (opcjonalny)
website string sometimes\|max:0 Honeypot — pole musi być puste
company_website string sometimes\|max:0 Honeypot — pole musi być puste
fax_number string sometimes\|max:0 Honeypot — pole musi być puste

Pola honeypot

Pola website, company_website i fax_number służą do wykrywania botów. Są walidowane tylko dla niezalogowanych użytkowników. Jeśli bot wypełni te ukryte pola, walidacja odrzuci request.

Reguły biznesowe (walidacja):

Reguła Klasa Opis
VerifyReservationCorrectDataRule Weryfikacja tożsamości Sprawdza, czy reservation_id + reservation_uuid + reservation_access_code pasują do istniejącej rezerwacji
ReservationStillReviewableRule Limit czasowy Sprawdza: (1) rezerwacja istnieje, (2) nie ma jeszcze opinii, (3) rezerwacja się zakończyła, (4) nie upłynął termin MAX_DAY_TO_ADD_REVIEW
blasp_check Filtr treści Sprawdza komentarz pod kątem nieodpowiednich słów

Response (sukces, 200):

{
  "message": "The Review has been successfully added",
  "status": 200
}

Response (błędy, 422):

{
  "message": "Sorry, the reservation you are trying to access has already been reviewed.",
  "errors": {
    "reservation_id": ["Sorry, the reservation you are trying to access has already been reviewed."]
  }
}

Możliwe błędy walidacji:

Komunikat Przyczyna
"Sorry, the reservation you are trying to access does not exist." Nieprawidłowe reservation_id + uuid + access_code
"Sorry, the reservation you are trying to access has already been reviewed." Rezerwacja ma już opinię
"Sorry, you can only add a review after the reservation has ended." Rezerwacja jeszcze trwa
"Sorry, the reservation you are trying to access is no longer reviewable." Upłynął termin na dodanie opinii

3.2 Endpointy uwierzytelnione

GET /api/reviews (auth)

  • Opis: Lista opinii z filtrowaniem — wersja uwierzytelniona (pełne dane)
  • Autoryzacja: Brak wymogu roli (wszyscy zalogowani użytkownicy)
  • Kontroler: ReviewController@indexIndexReviewController
  • Request: IndexReviewControllerRequest
  • Zachowanie: Identyczne jak endpoint publiczny, ale odpowiedź zawiera wszystkie pola (w tym reservation, disabled_by, disabled_at, can)

GET /api/reviews/{reviewId} (auth)

  • Opis: Szczegóły pojedynczej opinii
  • Autoryzacja: role:ADMIN|SUPERVISOR|EMPLOYEE
  • Kontroler: ReviewController@showShowReviewController
  • Eager loading: reviewable, reservation, organization, disabler, reservation.latestNote, reviewable.media

Response (sukces, 200):

{
  "data": {
    "id": "uuid",
    "reviewable_type": "Domain\\Item\\Model\\Item",
    "reviewable_id": "uuid",
    "organization_id": "uuid",
    "reservation_id": "uuid",
    "rating": 4,
    "comment": "Świetne wyposażenie, klimatyzacja działa doskonale",
    "can_be_edit": true,
    "is_active": true,
    "disabled_by": null,
    "disabled_at": null,
    "created_at": "2026-02-20 14:30:00",
    "updated_at": "2026-02-20 14:30:00",
    "can": {
      "view": true,
      "update": true
    },
    "reviewable": { "..." },
    "reservation": { "..." },
    "organization": { "..." },
    "disabler": null
  }
}

Response (błąd, 404): NotFoundModelException — opinia nie istnieje


PATCH /api/reviews/{reviewId}/change-state (auth)

  • Opis: Zmiana widoczności opinii (aktywacja/dezaktywacja)
  • Autoryzacja: role:ADMIN|SUPERVISOR|EMPLOYEE + Policy update (użytkownik musi należeć do organizacji opinii)
  • Kontroler: ReviewController@changeStatechangeStateXController
  • Zachowanie: Przełącza pole is_active na przeciwną wartość. Wywołuje event updating na modelu (withEvents = true), co triggeruje UpdatingReviewObserver — automatycznie ustawia disabled_by i disabled_at.

Response (sukces, 200):

{
  "message": "State changed successfully",
  "status": 200
}

POST /api/reviews (auth)

  • Opis: Dodanie opinii (wersja uwierzytelniona)
  • Autoryzacja: Brak wymogu roli — dostępne dla wszystkich zalogowanych
  • Zachowanie: Identyczne jak endpoint publiczny, ale bez pól honeypot (walidacja honeypot stosowana tylko dla niezalogowanych)

4. Logika biznesowa

4.1 Główne procesy

Proces tworzenia opinii

sequenceDiagram
    participant K as Klient
    participant V as Walidacja
    participant O as Observer
    participant DB as Baza danych

    K->>V: POST /api/reviews (dane + kody dostępu)
    V->>V: VerifyReservationCorrectDataRule
    V->>V: ReservationStillReviewableRule
    V->>V: blasp_check (komentarz)
    V->>V: Honeypot (jeśli no-auth)

    alt Walidacja OK
        V->>O: CreatingReviewObserver
        O->>O: Ustal reviewable_type/id z rezerwacji
        O->>O: Ustal organization_id z rezerwacji
        O->>DB: Zapisz opinię
        DB-->>K: 200 + komunikat sukcesu
    else Walidacja błąd
        V-->>K: 422 + komunikat błędu
    end
  1. Klient wysyła dane opinii wraz z reservation_id, reservation_uuid i reservation_access_code
  2. VerifyReservationCorrectDataRule weryfikuje, że trójka identyfikatorów odpowiada istniejącej rezerwacji
  3. ReservationStillReviewableRule sprawdza:
    • Rezerwacja nie ma jeszcze opinii
    • Rezerwacja się zakończyła (slot date_time_to w przeszłości)
    • Nie upłynął limit MAX_DAY_TO_ADD_REVIEW dni
  4. Filtr blasp_check sprawdza komentarz pod kątem nieodpowiednich słów
  5. Pola honeypot (tylko no-auth) — odrzucenie bota
  6. CreatingReviewObserver automatycznie uzupełnia:
    • reviewable_type i reviewable_id — na podstawie reservationable_type rezerwacji (ItemItem, ActivityItemActivity)
    • organization_id — z rezerwacji
  7. Opinia jest tworzona jako hasOne na rezerwacji ($reservation->review()->create(...))

Proces moderacji opinii

sequenceDiagram
    participant A as Administrator
    participant S as System
    participant O as UpdatingReviewObserver
    participant DB as Baza danych

    A->>S: PATCH /reviews/{id}/change-state
    S->>S: Autoryzacja (role + policy)
    S->>O: UpdatingReviewObserver
    alt Dezaktywacja (is_active: true → false)
        O->>DB: disabled_by = Auth::id()
        O->>DB: disabled_at = now()
    else Aktywacja (is_active: false → true)
        O->>DB: disabled_by = null
        O->>DB: disabled_at = null
    end
    DB-->>A: 200 sukces

4.2 Reguły biznesowe

  1. Jedna opinia na rezerwację — kolumna reservation_id ma constraint UNIQUE
  2. Opinia tylko po zakończeniu rezerwacji — wszystkie sloty muszą się zakończyć przed dodaniem opinii
  3. Limit czasowy — opinia musi zostać dodana w ciągu MAX_DAY_TO_ADD_REVIEW dni od zakończenia rezerwacji (parametr konfigurowalny)
  4. Weryfikacja tożsamości — wymagane podanie reservation_id + reservation_uuid + reservation_access_code
  5. Filtr blasfemii — komentarze sprawdzane regułą blasp_check
  6. Ochrona antyspamowa — honeypot dla niezalogowanych, rate limiting throttle:public-store
  7. Automatyczne przypisanie obiektu — observer ustala reviewable_type/id i organization_id na podstawie rezerwacji
  8. Polimorficzność obiektuItem przypisywany bezpośrednio, ActivityItem → przypisywana Activity (rodzic)
  9. Śledzenie moderacji — przy dezaktywacji automatycznie zapisywany jest moderator (disabled_by) i czas (disabled_at)
  10. Reaktywacja — przy ponownej aktywacji pola disabled_by i disabled_at są czyszczone

4.3 Walidacje

StoreReviewControllerRequest

Pole Reguły
reservation_id required\|uuid\|exists:reservations,id + ReservationStillReviewableRule
reservation_uuid required\|uuid\|max:255
reservation_access_code required\|string\|min:1\|max:20 + VerifyReservationCorrectDataRule
rating required\|integer\|min:1\|max:5
comment nullable\|string\|max:2000\|blasp_check
website sometimes\|max:0 (honeypot, tylko no-auth)
company_website sometimes\|max:0 (honeypot, tylko no-auth)
fax_number sometimes\|max:0 (honeypot, tylko no-auth)

IndexReviewControllerRequest

Standardowe reguły paginacji (paginationDefaultRequestRules()) + filtry publiczne modelu Review (publicFilterRequestRules()).


5. Autoryzacja i uprawnienia

5.1 Policy — ReviewPolicy

Metoda Logika Opis
before() Admin → true Administratorzy mają pełny dostęp (z BasePolicy)
view() belongsToOrganization() Użytkownik musi należeć do organizacji opinii
create() return false Tworzenie zablokowane przez policy — tworzenie odbywa się publicznie
update() belongsToOrganization() Użytkownik musi należeć do organizacji opinii

5.2 Middleware na endpointach

Endpoint Middleware roli Policy
GET /reviews (no-auth) Brak Brak
POST /reviews (no-auth) Brak + throttle:public-store Brak
GET /reviews (auth) Brak Brak
POST /reviews (auth) Brak Brak
GET /reviews/{id} (auth) ADMIN\|SUPERVISOR\|EMPLOYEE Brak (ale filtrowane przez UserAccessesGlobalScope)
PATCH /reviews/{id}/change-state (auth) ADMIN\|SUPERVISOR\|EMPLOYEE update (belongsToOrganization)

5.3 Matryca uprawnień

Akcja Administrator Supervisor Employee Klient (z kodem) Gość
Lista opinii (publiczna)
Szczegóły opinii (swojej org.) (swojej org.)
Dodanie opinii
Moderacja (zmiana stanu) (swojej org.) (swojej org.)

6. Observery i efekty uboczne

6.1 ReviewObserver

Plik: Domain\Review\Observer\ReviewObserver Rejestracja: Atrybut #[ObservedBy(ReviewObserver::class)] na modelu

Hook Delegacja Opis
creating CreatingReviewObserver Ustala reviewable_type, reviewable_id, organization_id na podstawie rezerwacji
updating UpdatingReviewObserver Przy zmianie is_active ustawia/czyści disabled_by i disabled_at

6.2 CreatingReviewObserver — szczegóły

  1. Sprawdza, czy opinia ma reservation_id i czy rezerwacja istnieje
  2. Na podstawie reservationable_type rezerwacji określa reviewable:
    • Item::class → obiekt bezpośrednio
    • ActivityItem::class$res->reservationable->activity (nadrzędna aktywność)
    • inne → null (brak przypisania)
  3. Ustawia reviewable_type, reviewable_id, organization_id na modelu opinii

6.3 UpdatingReviewObserver — szczegóły

  1. Sprawdza, czy pole is_active uległo zmianie (isDirty)
  2. Jeśli dezaktywacja (is_activefalse):
    • disabled_by = ID zalogowanego użytkownika
    • disabled_at = aktualny czas
  3. Jeśli aktywacja (is_activetrue):
    • disabled_by = null
    • disabled_at = null

7. Notyfikacje — przypomnienie o opinii

Przypomnienie o opinii jest obsługiwane przez domenę Reservation, nie Review:

Parametr Wartość
Komenda CRON reservation:send-review-reminders
Klasa akcji SendReviewRemindersCommandAction
Klasa notyfikacji ReservationReviewReminderNotification
Opóźnienie 5 godzin po zakończeniu ostatniego slotu
Kanał Email (przez SimpleSendNotificationJob)
Kolejka notifications

7.1 Warunki wysłania przypomnienia

Warunek Opis
Status rezerwacji RESERVATION_STATUS_CONFIRMED
Brak poprzedniego przypomnienia review_reminder_sent_at IS NULL
Wszystkie sloty zakończone Ostatni aktywny slot zakończony ≥ 5h temu
Brak opinii Rezerwacja nie ma jeszcze review
Email istnieje email IS NOT NULL AND email != ''
Powiadomienia włączone send_notifications = true

7.2 Treść powiadomienia

  • Temat: "Zaproszenie do wystawienia opinii - rezerwacja nr {uuid}"
  • Treść: Podziękowanie za skorzystanie z usług + zachęta do wystawienia opinii
  • Dane: Numer rezerwacji, kod dostępu, nazwa usługi, data
  • Przycisk CTA: "Wystaw opinię" → link do frontendu /rezerwacje/{id}?uuid={uuid}&access_code=

7.3 Po wysłaniu

Pole review_reminder_sent_at na rezerwacji jest ustawiane na aktualny czas — zapobiega wielokrotnemu wysyłaniu.


8. Powiązania z innymi domenami

Domena Powiązanie Opis
Rezerwacje BelongsTo (reservation) Opinia dotyczy konkretnej rezerwacji; weryfikacja uuid + access_code
Obiekty MorphTo (reviewable) Opinie o obiektach — polimorficzna relacja MorphMany w Item
Aktywności MorphTo (reviewable) Opinie o zajęciach — polimorficzna relacja MorphMany w Activity
Organizacje BelongsTo (organization) Opinie wpływają na statystyki organizacji (HasMany)
Reklamacje Współdzielona reguła VerifyReservationCorrectDataRule jest reużywana w StoreComplaintControllerRequest
Statystyki Odczyt danych GetReviewStatistics, GetRecentReviews — analiza opinii w dashboardzie
Raporty Generator ReviewsSummaryGenerator — raport podsumowania opinii per obiekt
Support Atrybuty GetTotalReviewsAttr, GetAvgReviewsAttr — obliczanie statystyk na modelach

8.1 Statystyki opinii (domena Statistic)

GetReviewStatistics

Zwraca: - total — łączna liczba opinii - average_rating — średnia ocena - by_rating — rozkład ocen (1–5) - with_comment — liczba opinii z komentarzem - without_comment — liczba opinii bez komentarza

Filtrowanie: zakres dat (date_from, date_to), lista itemIds.

GetRecentReviews

Zwraca ostatnie aktywne opinie (domyślnie 10), z relacjami reservation, reviewable, organization.

8.2 Raport opinii (domena Report)

Klucz raportu: reviews.summary

Filtry:

Filtr Typ Opis
period enum this_month, last_month, this_year, last_year, custom
date_from date Początek zakresu (wymagany dla custom)
date_to date Koniec zakresu (wymagany dla custom)
organization_id uuid Filtr po organizacji
item_id uuid Filtr po obiekcie

Kolumny raportu: Nazwa obiektu, Średnia ocena, Liczba opinii, 5 gwiazdek, 4 gwiazdki, 3 gwiazdki, 2 gwiazdki, 1 gwiazdka

8.3 Atrybuty obliczeniowe (Support)

GetTotalReviewsAttr

Oblicza łączną liczbę aktywnych opinii (is_active = true) dla modelu. Optymalizacja: 1. Wykorzystuje reviews_count jeśli dostępny (agregat) 2. Filtruje załadowaną relację jeśli dostępna 3. W ostateczności wykonuje loadCount z filtrem

GetAvgReviewsAttr

Oblicza średnią ocenę aktywnych opinii. Analogiczna optymalizacja jak wyżej, z zaokrągleniem do 2 miejsc po przecinku.


9. Konfiguracja

Parametr Enum Opis
MAX_DAY_TO_ADD_REVIEW ReservationEnumService::MAX_DAY_TO_ADD_REVIEW Maksymalna liczba dni od zakończenia rezerwacji na dodanie opinii

Parametr jest odczytywany przez TraitConfiguration::getConfigurationBySlug() w ReservationStillReviewableRule.


10. Zabezpieczenia

Zabezpieczenie Mechanizm Opis
Weryfikacja tożsamości VerifyReservationCorrectDataRule Trzy kody: reservation_id + uuid + access_code
Filtr treści blasp_check (custom validation rule) Sprawdzanie nieodpowiednich słów w komentarzu
Honeypot Ukryte pola website, company_website, fax_number Tylko dla niezalogowanych — bot wypełniający te pola zostaje odrzucony
Rate limiting throttle:public-store Ochrona przed masowym tworzeniem opinii
Jedna opinia/rezerwację UNIQUE constraint na reservation_id + walidacja Nie można dodać dwóch opinii do tej samej rezerwacji
Redukcja danych UNALLOWED_NO_AUTH_KEYS w ReviewResource Niezalogowani nie widzą wrażliwych pól

11. Skala ocen

Ocena Znaczenie
5 Doskonale
4 Bardzo dobrze
3 Dobrze
2 Slabo
1 Bardzo slabo

12. Co mozna oceniac?

System obsługuje opinie dla dwóch typów encji (ustalanych automatycznie przez CreatingReviewObserver):

Obiekty (Item)

Przyklad Opis
Sala konferencyjna "Swietne wyposazenie, klimatyzacja dziala doskonale"
Hala sportowa "Duza przestrzen, ale slabe oswietlenie"
Boisko "Nawierzchnia w dobrym stanie"

Zajecia (Activity)

Przyklad Opis
Joga "Trenerka bardzo cierpliwa, polecam poczatkujacym"
Aqua aerobik "Swietna zabawa, woda czysta"
Warsztaty ceramiczne "Duzo sie nauczylam, prowadzacy profesjonalny"

Automatyczne przypisanie

Klient nie wybiera, czy ocenia obiekt czy zajęcia. System automatycznie ustala typ na podstawie rezerwacji:

  • Rezerwacja na obiekt (Item) → opinia o obiekcie
  • Rezerwacja na sesję zajęć (ActivityItem) → opinia o aktywności (nadrzędna Activity)

13. Dane demo (Seeder)

ReservationSeeder automatycznie tworzy opinie dla ~30% zakończonych rezerwacji z losowymi ocenami (1–5) i komentarzami. Observer ReviewObserver automatycznie ustala reviewable_type/id i organization_id na podstawie rezerwacji.


14. Pliki domeny — struktura

app/Domain/Review/
├── Action/
│   ├── Controller/
│   │   ├── IndexReviewController.php      # Lista opinii z filtrowaniem
│   │   ├── ShowReviewController.php       # Szczegóły opinii
│   │   └── StoreReviewController.php      # Tworzenie opinii
│   └── Other/
│       ├── CreateReview.php               # Logika tworzenia opinii
│       ├── CreatingReviewObserver.php      # Observer: ustalanie reviewable/organization
│       └── UpdatingReviewObserver.php      # Observer: śledzenie moderacji
├── Controller/
│   └── ReviewController.php               # Główny kontroler
├── Model/
│   └── Review.php                         # Model Eloquent
├── Observer/
│   └── ReviewObserver.php                 # Dispatcher observerów
├── Policy/
│   └── ReviewPolicy.php                   # Polityka autoryzacji
├── Request/
│   ├── IndexReviewControllerRequest.php   # Walidacja listy
│   ├── StoreReviewControllerRequest.php   # Walidacja tworzenia
│   └── Rule/
│       ├── ReservationStillReviewableRule.php   # Limit czasowy + deduplikacja
│       └── VerifyReservationCorrectDataRule.php # Weryfikacja kodów rezerwacji
├── Resource/
│   └── ReviewResource.php                 # Transformacja JSON
└── Service/
    ├── ReviewEnumService.php              # Enum (pusty — brak case'ów)
    ├── TraitReview.php                    # Trait serwisowy
    └── TraitControllerReview.php          # Trait kontrolera

16. Historia zmian

Data Typ Opis
26 lut 2026 Dokumentacja Pełna aktualizacja dokumentacji domeny — synchronizacja ze stanem kodu
18 lut 2026 Optymalizacja Eliminacja N+1 query w GET /api/reviews — Dodano 'reservation.latestNote' i 'reviewable.media' do eager loading w IndexReviewController i ShowReviewController