Aktywności (Activity)¶
Zajęcia grupowe i wydarzenia — joga, aqua aerobik, warsztaty ceramiczne i inne aktywności z harmonogramem sesji.
Ostatnia aktualizacja dokumentacji: 2026-02-26 Stan synchronizacji z kodem: Zsynchronizowany
Co to jest?¶
Domena Aktywności zarządza zajęciami grupowymi i wydarzeniami. To jak karta kursów w domu kultury — każda aktywność (np. "Joga dla początkujących") ma wiele sesji (poniedziałki o 18:00), na które zapisują się uczestnicy. System zarządza harmonogramem, pojemnością, trenerami i rezerwacjami.
Kluczowa koncepcja: Activity (szablon) → ActivityItem (sesja)
flowchart TD
A[Activity: Joga dla początkujących] --> B[Sesja: Pon 18:00]
A --> C[Sesja: Śr 18:00]
A --> D[Sesja: Pt 18:00]
B --> E[Rezerwacje uczestników]
C --> F[Rezerwacje uczestników]
D --> G[Rezerwacje uczestników] Jakie problemy rozwiązuje?¶
Korzyści dla JST
- Automatyzacja harmonogramu — generowanie sesji na wiele tygodni
- Zarządzanie pojemnością — system pilnuje limitu miejsc
- Przypisanie trenerów — kontrola dostępności instruktorów
- Online rezerwacje — mieszkańcy zapisują się samodzielnie
Kluczowe funkcjonalności¶
Dla administratora¶
| Funkcja | Opis | Status |
|---|---|---|
| Tworzenie aktywności | Definiowanie nowych zajęć | Dostępne |
| Generowanie sesji | Automatyczne tworzenie harmonogramu | Dostępne |
| Przypisanie trenera | Wybór instruktora do sesji | Dostępne |
| Zarządzanie pojemnością | Min/max uczestników | Dostępne |
| Cennik | Płatne/bezpłatne zajęcia | Dostępne |
| Anulowanie sesji | Odwoływanie pojedynczych zajęć | Dostępne |
| Zmiana kategorii | Przeniesienie aktywności do innej kategorii | Dostępne |
| Zmiana stanu | Aktywacja/dezaktywacja aktywności i sesji | Dostępne |
Dla klienta¶
| Funkcja | Opis | Status |
|---|---|---|
| Przeglądanie zajęć | Lista dostępnych aktywności | Dostępne |
| Sprawdzanie miejsc | Ile wolnych miejsc | Dostępne |
| Rezerwacja | Zapis na sesję | Dostępne |
| Opinie | Ocena po zajęciach | Dostępne |
Activity vs ActivityItem¶
Activity (Szablon aktywności)¶
Co to jest: Definicja zajęć — nazwa, opis, pojemność, cena.
| Pole | Typ | Opis | Przykład |
|---|---|---|---|
name | string | Nazwa zajęć (max 255) | "Joga dla początkujących" |
desc | string | Opis zajęć (max 5000) | "Relaksujące zajęcia jogi..." |
comment | string? | Komentarz wewnętrzny (max 5000) | "Uwagi dla administracji" |
min_participants | int | Minimalna liczba uczestników | 5 |
max_participants | int | Maksymalna liczba uczestników | 20 |
is_paid | bool | Czy płatne | true |
is_active | bool | Czy aktywne | true |
is_public | bool | Czy widoczne publicznie | true |
is_public_price_list | bool | Czy ceny widoczne bez logowania | true |
can_pay_online | bool | Czy dostępna płatność online (Przelewy24) | true |
category_id | UUID | Kategoria | "Fitness" |
item_id | UUID | Lokalizacja (sala) — musi być aktywna | "Sala gimnastyczna A" |
organization_id | UUID | Organizacja (ustawiane automatycznie z item) | — |
meta_title | string? | Tytuł SEO (max 255) | "Joga - zajęcia grupowe" |
meta_description | string? | Opis SEO (max 5000) | "Zajęcia jogi w centrum..." |
tags | array? | Tagi | ["joga", "relaks"] |
statute_src | file? | Regulamin PDF (max ~10MB) | — |
cancellation_deadline_hours | int? | Godziny przed sesją na anulowanie | 24 |
cancellation_policy_description | string? | Opis zasad anulowania (max 1000) | "Bezpłatne anulowanie do 24h przed..." |
Atrybuty wyliczane Activity¶
| Atrybut | Typ | Opis |
|---|---|---|
avg_reviews | float | Średnia ocen (z aktywnych recenzji) |
total_reviews | int | Liczba recenzji |
current_pricing | object? | Aktualny aktywny cennik (bez client_id) |
current_price | float | Aktualna cena netto |
current_price_gross | float | Aktualna cena brutto (z VAT) |
current_price_vat_rate | string? | Stawka VAT aktualnej ceny |
lowest_pricing | object? | Najniższy cennik z ostatnich 31 dni |
lowest_price | float | Najniższa cena netto z 31 dni |
lowest_price_gross | float | Najniższa cena brutto z 31 dni |
lowest_price_vat_rate | string? | Stawka VAT najniższej ceny |
total_reservations | int | Łączna liczba rezerwacji (przez ActivityItems) |
thumbnail | string? | URL miniaturki zdjęcia |
is_in_use | bool | Czy ma powiązane rezerwacje lub opinie |
can_be_edit | bool | Czy można edytować |
ActivityItem (Sesja)¶
Co to jest: Konkretny termin zajęć — data, godzina, trener.
| Pole | Typ | Opis | Przykład |
|---|---|---|---|
activity_id | UUID | Powiązana aktywność | — |
date | date | Data sesji | 2024-02-12 |
start_time | time | Godzina rozpoczęcia | 18:00 |
end_time | time | Godzina zakończenia | 19:30 |
trainer_id | UUID? | Trener | "Anna Kowalska" |
level | string | Poziom trudności | "beginner" |
is_active | bool | Czy sesja aktywna | true |
is_canceled | bool | Czy anulowana | false |
reservation_deadline_hours | int? | Godziny przed sesją kiedy zamykamy zapisy | 24 |
comment | string? | Komentarz wewnętrzny (max 5000) | "Zastępstwo trenera" |
Atrybuty wyliczane ActivityItem¶
| Atrybut | Typ | Zwracane w API | Opis |
|---|---|---|---|
level_lang | string | Tak | Przetłumaczona nazwa poziomu trudności (z ActivityEnumService::getLevels()) |
date_start_time | string | Nie (wewnętrzne) | Data + godzina rozpoczęcia (Y-m-d H:i:s) — używane wewnętrznie do obliczeń (np. duration_time_in_minutes) |
date_end_time | string | Nie (wewnętrzne) | Data + godzina zakończenia (Y-m-d H:i:s) — używane wewnętrznie do obliczeń |
total_relations | int | Nie (wewnętrzne) | Liczba powiązanych relacji — używane wewnętrznie |
duration_time_in_minutes | float | Tak | Czas trwania w minutach |
available_slots | int | Tak | Liczba wolnych miejsc (uwzględnia rezerwacje oczekujące) |
active_waiting_reservations_count | int | Tak | Liczba aktywnych rezerwacji oczekujących (niewygasłych) |
is_in_use | bool | Tak | Czy są powiązane rezerwacje |
can_be_edit | bool | Tak | Czy można edytować |
Struktura tabel w bazie danych¶
Tabela activities¶
| Kolumna | Typ | Nullable | Default | Opis |
|---|---|---|---|---|
id | UUID | auto | Klucz główny | |
organization_id | UUID | — | FK → organizations.id (CASCADE) | |
item_id | UUID | — | FK → items.id (CASCADE) | |
category_id | UUID | — | FK → categories.id (CASCADE) | |
name | string | — | Nazwa aktywności | |
desc | text | — | Opis | |
comment | text | NULL | Komentarz wewnętrzny | |
min_participants | integer | — | Min. uczestników | |
max_participants | integer | — | Max. uczestników | |
is_paid | boolean | false | Czy płatne | |
is_active | boolean | true | Czy aktywne | |
is_public | boolean | true | Czy publiczne | |
meta_title | string | NULL | Tytuł SEO | |
meta_description | text | NULL | Opis SEO | |
tags | json | NULL | Tagi | |
statute_src | text | NULL | Ścieżka regulaminu PDF | |
is_public_price_list | boolean | true | Czy cennik publiczny (indeksowane) | |
can_pay_online | boolean | true | Czy dostępna płatność online | |
cancellation_deadline_hours | integer | NULL | Godziny na anulowanie | |
cancellation_policy_description | text | NULL | Opis polityki anulowania | |
created_at | timestamp | — | Data utworzenia | |
updated_at | timestamp | — | Data aktualizacji |
Tabela activity_items¶
| Kolumna | Typ | Nullable | Default | Opis |
|---|---|---|---|---|
id | UUID | auto | Klucz główny | |
activity_id | UUID | — | FK → activities.id (CASCADE) | |
trainer_id | UUID | NULL | FK → trainers.id (SET NULL), indeksowane | |
date | date | — | Data sesji | |
start_time | time | — | Godzina rozpoczęcia | |
end_time | time | — | Godzina zakończenia | |
comment | text | NULL | Komentarz | |
is_public | boolean | true | Czy publiczna | |
is_active | boolean | true | Czy aktywna | |
is_canceled | boolean | false | Czy anulowana | |
level | enum | NULL | Poziom: beginner, intermediate, advanced, not_applicable | |
reservation_deadline_hours | integer | NULL | Deadline rezerwacji (h) | |
created_at | timestamp | — | Data utworzenia | |
updated_at | timestamp | — | Data aktualizacji |
Indeksy:
activity_items_date_activity_start_index— indeks złożony na (date,activity_id,start_time)activity_items_trainer_id_index— indeks natrainer_id
Jak to działa?¶
Scenariusz 1: Tworzenie nowych zajęć¶
sequenceDiagram
participant A as Administrator
participant S as System
participant Q as Kolejka
A->>S: Tworzy aktywność "Aqua aerobik"
A->>S: Dodaje sesje: Pon/Śr/Pt 10:00
A->>S: Ustawia powtarzanie: 10 tygodni
S->>S: Observer ustawia organization_id z item
S->>S: Tworzy cennik (jeśli is_paid=true)
S->>Q: Dispatchuje CreateManyActivityItemsJob
Q->>S: Generuje 30 sesji (3×10)
S-->>A: Aktywność gotowa - Administrator definiuje aktywność (max 10 definicji sesji jednocześnie)
- Wybiera dni tygodnia i godziny
- Ustawia ile tygodni z góry generować (max 100)
- System asynchronicznie tworzy sesje (kolejka
domain) - Zajęcia gotowe do rezerwacji
Scenariusz 2: Klient zapisuje się na zajęcia¶
sequenceDiagram
participant K as Klient
participant S as System
participant R as Rezerwacja
K->>S: Przegląda zajęcia
K->>S: Wybiera "Joga" w poniedziałek
S->>S: Sprawdza dostępność miejsc
S->>S: Sprawdza deadline rezerwacji
K->>R: Tworzy rezerwację
R-->>K: Potwierdzenie zapisu Scenariusz 3: Anulowanie sesji¶
flowchart TD
A[Administrator anuluje sesję] --> B[System ustawia is_canceled=true]
B --> C[Observer: is_active = false]
C --> D[Observer: dispatch CancelReservationsForActivityItemJob]
D --> E[Job na kolejce cancellations]
E --> F[Dla każdej rezerwacji: CancelReservation.cancelDueToActivityCancellation]
F --> G[Anulowanie slotów + zwroty + notyfikacja email] Gdy sesja zostaje anulowana (is_canceled=true), observer automatycznie:
- Dezaktywuje sesję (
is_active = false) - Dispatchuje
CancelReservationsForActivityItemJobna kolejkęcancellations(zafterCommit()) - Job przetwarza każdą rezerwację osobno — pełny flow anulowania:
- Zmiana statusu rezerwacji na
canceled - Anulowanie aktywnych slotów
- Automatyczny zwrot płatności (jeśli opłacona)
- Wysyłka powiadomienia email do klienta z informacją o odwołanych zajęciach
- Zmiana statusu rezerwacji na
Scenariusz 4: Dezaktywacja aktywności¶
flowchart TD
A[Activity.is_active = false] --> B[Observer wykrywa zmianę]
B --> C[CancelItemsForDisabledActivity]
C --> D[Wszystkie ActivityItems.is_active = false] Sesje są dezaktywowane w partiach po 2000 dla wydajności.
Scenariusz 5: Zmiana is_paid na false¶
Gdy aktywność zostanie przełączona na bezpłatną (is_paid=false), observer UpdatedActivityObserver automatycznie tworzy nowy cennik z wartością 0 (aktywny, VAT 23%, bez client_id). Poprzednie cenniki pozostają bez zmian.
Powiązania z innymi domenami¶
| Domena | Powiązanie | Typ relacji |
|---|---|---|
| Rezerwacje (Reservation) | Zapisy na sesje (przez ActivityItem) | HasManyThrough / MorphMany |
| Zasoby (Item) | Lokalizacja zajęć (sala) | BelongsTo |
| Trenerzy (Trainer) | Instruktorzy prowadzący (na poziomie sesji) | BelongsTo |
| Kategorie (Category) | Kategoryzacja zajęć | BelongsTo |
| Cennik (Pricing) | Ceny za uczestnictwo | MorphMany (priceable) |
| Opinie (Review) | Oceny zajęć | MorphMany (reviewable) |
| Notatki (Note) | Notatki wewnętrzne | MorphMany (notable) |
| Organizacje (Organization) | Organizator | BelongsTo |
| Media | Zdjęcia aktywności | Spatie MediaLibrary |
Wartość biznesowa¶
Argumenty dla decydentów
- Automatyzacja — harmonogram generowany automatycznie
- Oszczędność czasu — brak ręcznego zarządzania listami
- Transparentność — mieszkańcy widzą dostępność online
- Kontrola — monitoring frekwencji i przychodów
Zarządzanie pojemnością¶
Definicja limitów¶
| Parametr | Poziom | Opis |
|---|---|---|
min_participants | Activity | Minimalna liczba do uruchomienia (min: 0) |
max_participants | Activity | Twarda granica pojemności (min: 1) |
Obliczanie dostępnych miejsc¶
Gdzie:
active_reservations_count— rezerwacje z wykluczeniem statusówCANCELEDiREJECTEDactive_waiting_reservations_count— rezerwacje oczekujące zexpires_at > now()(niewygasłe)
Filtr wolnych miejsc
Scope activity_item_has_available_slots działa spójnie — liczy rezerwacje ze statusami PENDING i CONFIRMED plus aktywne rezerwacje oczekujące (niewygasłe). Suma musi być mniejsza od max_participants.
Walidacja przy aktualizacji¶
Ograniczenie
Nie można zmniejszyć max_participants poniżej aktualnej liczby aktywnych rezerwacji na sesji z największą liczbą uczestników. System waliduje to regułą MaxParticipantsActivityUpdateRule.
Poziomy trudności¶
| Poziom | Kod | Opis |
|---|---|---|
| Początkujący | beginner | Dla osób bez doświadczenia |
| Średniozaawansowany | intermediate | Podstawowe umiejętności wymagane |
| Zaawansowany | advanced | Dla doświadczonych |
| Nie dotyczy | not_applicable | Bez określonego poziomu |
Poziomy zdefiniowane w ActivityEnumService z tłumaczeniami (getLevels()).
Przypisanie trenera¶
Walidacja dostępności¶
System sprawdza czy trener nie ma konfliktu (TrainerAvailabilityRule):
flowchart TD
A[Przypisz trenera do sesji] --> B{Sprawdź konflikty}
B -->|Brak konfliktów| C[Przypisz]
B -->|Konflikt| D{force_add_trainer?}
D -->|Tak| C
D -->|Nie| E[Błąd: Trener zajęty] Wykrywanie konfliktów¶
System sprawdza czy czasy się nakładają (tylko sesje nieanulowane):
Walidacja przy tworzeniu vs aktualizacji¶
| Scenariusz | Sprawdzane daty | Wykluczone |
|---|---|---|
| Store (nowa sesja) | Wszystkie wygenerowane daty (repeat) | — |
| Update (edycja sesji) | Jedna data (bieżąca lub nowa) | Aktualna sesja (po ID) |
Wymuszenie przypisania¶
Parametr force_add_trainer=true pozwala pominąć walidację (dla wyjątkowych sytuacji).
Cennik¶
Konfiguracja¶
| Pole | Opis |
|---|---|
is_paid | Czy zajęcia są płatne |
pricing | Obiekt cennika (wymagany jeśli is_paid=true) |
Atrybuty cenowe¶
| Atrybut | Opis |
|---|---|
current_price | Aktualna cena netto |
current_price_gross | Cena brutto (z VAT) |
current_price_vat_rate | Stawka VAT |
lowest_price | Najniższa cena z ostatnich 31 dni |
lowest_price_gross | Najniższa cena brutto |
lowest_price_vat_rate | Stawka VAT najniższej ceny |
Dyrektywa Omnibus
System automatycznie śledzi najniższą cenę z 31 dni dla zgodności z dyrektywą Omnibus. Na endpointach listujących (GET /activities) najniższa cena wyliczana z cenników aktywnych (is_active=true), bez cenników indywidualnych (client_id IS NULL). Na endpointach szczegółowych (GET /activities/{activityId}) accessor GetLowestPricingAttr nie filtruje po is_active — patrz sekcja "Znane uwagi techniczne".
Optymalizacja cenowa (Aggregates ORM)¶
Na listach (GET /activities) ceny wyliczane są za pomocą subqueries SQL (ApplyActivityAggregatesOrm) zamiast ładowania relacji, co eliminuje problem N+1 query.
Deadline rezerwacji¶
Konfiguracja¶
| Parametr | Poziom | Opis |
|---|---|---|
reservation_deadline_hours | ActivityItem | Godziny przed sesją kiedy zamykamy zapisy |
Przykład¶
Po przekroczeniu deadline'u nowe rezerwacje są blokowane.
Polityka anulowania¶
Pola konfiguracyjne¶
| Pole | Typ | Poziom | Opis |
|---|---|---|---|
cancellation_deadline_hours | int? | Activity | Godziny przed sesją na anulowanie |
cancellation_policy_description | text? | Activity | Opis zasad anulowania (max 1000 znaków) |
Wykorzystanie¶
- Pokazywane klientowi przy rezerwacji
- Sprawdzane przy próbie anulowania
- Wpływa na decyzję o zwrocie
Media i dokumenty¶
Zdjęcia¶
| Kolekcja | Rozmiar | Format |
|---|---|---|
preview | 300×300 | WebP |
web | 1200×1200 | WebP |
Konwersje niekolejkowane (nonQueued) — przetwarzane synchronicznie.
Regulamin zajęć¶
| Pole | Opis |
|---|---|
statute_src | Plik PDF (max ~10MB, walidacja mimes:pdf) |
Widoczność publiczna¶
Flagi widoczności¶
| Flaga | Opis |
|---|---|
is_public | Czy aktywność widoczna publicznie |
is_public_price_list | Czy ceny widoczne bez logowania |
can_pay_online | Czy dostępna płatność online (Przelewy24) |
is_active | Czy aktywność aktywna |
Logika wyświetlania¶
flowchart TD
A[Użytkownik przegląda zajęcia] --> B{is_public?}
B -->|Nie| C[Ukryj]
B -->|Tak| D{is_active?}
D -->|Nie| C
D -->|Tak| E[Pokaż]
E --> F{is_public_price_list?}
F -->|Tak| G[Pokaż cenę]
F -->|Nie| H{Zalogowany?}
H -->|Tak| G
H -->|Nie| I[Ukryj cenę] Ograniczenia dla niezalogowanych¶
ActivityResource — pola total_reservations i can są ukrywane dla niezalogowanych użytkowników. Dodatkowo, jeśli is_public_price_list=false, ukrywane są dodatkowe klucze cenowe (current_price, current_price_gross, current_price_vat_rate, lowest_price, lowest_price_gross, lowest_price_vat_rate).
ActivityItemResource — pole can jest ukrywane dla niezalogowanych użytkowników. Pola cenowe nie dotyczą sesji (brak logiki getExtraUnallowedNoAuthKeys).
Observery i cykl życia¶
ActivityObserver¶
| Zdarzenie | Klasa | Opis |
|---|---|---|
creating | CreatingActivityObserver | Ustawia organization_id na podstawie item.organization_id |
updating | UpdatingActivityObserver → UpdatingIsActiveActivityObserver | Gdy is_active zmienia się na false — dezaktywuje wszystkie sesje |
updated | UpdatedActivityObserver | Gdy is_paid zmienia się na false — tworzy nowy cennik z value=0 (aktywny, VAT 23%, bez client_id) |
ActivityItemObserver¶
| Zdarzenie | Klasa | Opis |
|---|---|---|
updating | UpdatingActivityItemObserver → UpdatingIsCanceledActivityItemObserver | Gdy is_canceled zmienia się na true — dezaktywuje sesję i dispatchuje CancelReservationsForActivityItemJob (pełne anulowanie rezerwacji ze zwrotami i notyfikacjami) |
Walidacja aktywacji (Exceptions)¶
Aktywacja Activity¶
Nie można aktywować aktywności (changeState) jeśli:
| Warunek | HTTP | Komunikat |
|---|---|---|
| Organizacja jest nieaktywna | 409 | "Cannot activate this activity because the organization is inactive." |
| Kategoria jest nieaktywna | 409 | "Cannot activate this activity because the category is inactive." |
Aktywacja ActivityItem¶
Nie można aktywować sesji (changeState) jeśli:
| Warunek | HTTP | Komunikat |
|---|---|---|
| Sesja jest anulowana | 409 | "Cannot activate this activity item because is already canceled." |
| Aktywność jest nieaktywna | 409 | "Cannot activate this activity item because the activity is inactive." |
| Organizacja jest nieaktywna | 409 | "Cannot activate this activity item because the organization is inactive." |
| Kategoria jest nieaktywna | 409 | "Cannot activate this activity item because the category is inactive." |
Anulowanie ActivityItem¶
Nie można anulować sesji (canceled) jeśli:
| Warunek | HTTP | Komunikat |
|---|---|---|
| Sesja już anulowana | 409 | "Sorry, but this activity item is already canceled." |
Wyszukiwanie i filtrowanie¶
Filtry Activity (GET /activities)¶
| Filtr | Typ | Opis |
|---|---|---|
id | exact | UUID aktywności |
organization_id | exact | UUID organizacji |
item_id | exact | UUID obiektu |
category_id | exact | UUID kategorii |
is_paid | exact | Czy płatne |
is_active | exact | Czy aktywne |
trainer_id | exact | UUID trenera |
item.voivodeship | partial | Województwo obiektu |
item.district | partial | Powiat obiektu |
item.commune | partial | Gmina obiektu |
item.city | partial | Miasto obiektu |
category.name | partial | Nazwa kategorii |
organization.name | partial | Nazwa organizacji |
item.name | partial | Nazwa obiektu |
Filtry ActivityItem (GET /activity-items)¶
| Filtr | Typ | Opis |
|---|---|---|
id | exact | UUID sesji |
activity_id | exact | UUID aktywności |
is_active | exact | Czy sesja aktywna |
is_canceled | exact | Czy sesja anulowana |
level | exact | Poziom trudności |
activity.is_active | exact | Czy aktywność aktywna |
activity.is_public | exact | Czy aktywność publiczna |
activity.category_id | exact | UUID kategorii aktywności |
activity.is_paid | exact | Czy aktywność płatna |
activity.is_public_price_list | exact | Czy cennik publiczny |
search | scope | Szukaj w nazwie, opisie, tagach, meta, kategorii aktywności |
price_from | scope | Minimalna cena (cennik aktywny, bez indywidualnych) |
price_to | scope | Maksymalna cena (cennik aktywny, bez indywidualnych) |
date_greater_than_today | scope | Sesje z date+start_time >= now() (uwzględnia godzinę) |
activity_item_has_available_slots | scope | Tylko z wolnymi miejscami |
dates_between | scope | Zakres dat (format: od,do lub od|do) |
Filtry geograficzne (z Activity → Item)¶
| Filtr | Opis |
|---|---|
activity.item.voivodeship | Województwo |
activity.item.district | Powiat |
activity.item.commune | Gmina |
activity.item.city | Miasto |
activity.item.name | Nazwa obiektu |
activity.name | Nazwa aktywności |
activity.organization.name | Nazwa organizacji |
activity.category.name | Nazwa kategorii |
Filtry udogodnień (z Activity → Item)¶
| Filtr | Opis |
|---|---|
activity.item.has_parking | Parking |
activity.item.has_wifi | WiFi |
activity.item.has_air_conditioning | Klimatyzacja |
activity.item.is_kid_friendly | Przyjazne dzieciom |
activity.item.are_there_facilities_for_disabled_people | Udogodnienia dla osób niepełnosprawnych |
activity.item.is_there_an_elevator | Winda |
activity.item.is_pet_friendly | Przyjazne zwierzętom |
activity.item.has_restrooms | Toalety |
activity.item.has_kitchen | Kuchnia |
activity.item.has_garden | Ogród |
Scope wyszukiwania (search)¶
Przeszukuje przez relację activity:
name— nazwa aktywnościdesc— opis aktywnościtags— tagimeta_title— tytuł SEOmeta_description— opis SEOcategory.name— nazwa kategorii
Sortowanie (ActivityItem)¶
| Sortowanie | Opis | Typ |
|---|---|---|
start_time | Godzina rozpoczęcia | Bezpośrednie |
date | Data sesji | Bezpośrednie |
name | Nazwa aktywności (join) | Custom sort |
created_at | Data utworzenia aktywności (join) | Custom sort |
avg_reviews | Średnia ocen aktywności | Custom sort (subquery) |
lowest_price | Najniższa cena aktywności | Custom sort (subquery) |
API Endpoints¶
Publiczne (bez autoryzacji)¶
| Endpoint | Metoda | Opis |
|---|---|---|
/no-auth/activities | GET | Lista aktywności |
/no-auth/activities/{activityId} | GET | Szczegóły aktywności |
/no-auth/activity-items | GET | Lista sesji |
/no-auth/activity-items/{activityItemId} | GET | Szczegóły sesji |
Dla zalogowanych użytkowników¶
| Endpoint | Metoda | Opis | Wymagana rola |
|---|---|---|---|
/activities | GET | Lista aktywności | Dowolny zalogowany |
/activities/{activityId} | GET | Szczegóły aktywności | Dowolny zalogowany |
/activities | POST | Utwórz aktywność | Admin / Supervisor / Employee |
/activities/{activityId} | PATCH | Aktualizuj aktywność | Admin / Supervisor / Employee |
/activities/{activityId}/change-state | PATCH | Zmień status (is_active) | Admin / Supervisor / Employee |
/activities/{activityId}/change-category | PATCH | Zmień kategorię | Admin / Supervisor / Employee |
/activity-items | GET | Lista sesji | Dowolny zalogowany |
/activity-items/{activityItemId} | GET | Szczegóły sesji | Dowolny zalogowany |
/activity-items | POST | Utwórz sesję | Admin / Supervisor / Employee |
/activity-items/{activityItemId} | PATCH | Aktualizuj sesję | Admin / Supervisor / Employee |
/activity-items/{activityItemId}/change-state | PATCH | Zmień status sesji | Admin / Supervisor / Employee |
/activity-items/{activityItemId}/canceled | PATCH | Anuluj sesję | Admin / Supervisor / Employee |
Walidacja — szczegółowe reguły¶
POST /activities (StoreActivityControllerRequest)¶
| Pole | Reguły | Opis |
|---|---|---|
item_id | required\|uuid\|exists:items,id\|in:aktywne_items | Musi być aktywnym obiektem |
category_id | required\|uuid\|exists:categories,id\|ActiveCategoryRule(activity) | Aktywna kategoria z zakresem activity |
name | required\|string\|max:255 | Nazwa |
desc | required\|string\|max:5000 | Opis |
comment | sometimes\|nullable\|string\|max:5000 | Komentarz |
min_participants | required\|integer\|min:0\|lte:max_participants | Min. uczestników |
max_participants | required\|integer\|min:1\|gte:min_participants | Max. uczestników |
is_paid | required\|boolean | Czy płatne |
is_active | required\|boolean | Czy aktywne |
meta_title | sometimes\|nullable\|string\|max:255 | SEO tytuł |
meta_description | sometimes\|nullable\|string\|max:5000 | SEO opis |
tags | sometimes\|array | Tagi |
tags.* | string\|max:255 | Każdy tag |
is_public_price_list | sometimes\|boolean | Cennik publiczny |
can_pay_online | sometimes\|boolean | Płatność online |
cancellation_deadline_hours | sometimes\|nullable\|integer\|min:0 | Godziny na anulowanie |
cancellation_policy_description | sometimes\|nullable\|string\|max:1000 | Polityka anulowania |
statute_src | sometimes\|nullable\|file\|mimes:pdf\|max:10048 | Regulamin PDF |
pricing | required_if:is_paid,true\|array | Cennik (zagnieżdżony z StorePricingControllerRequest) |
activity_items | sometimes\|array\|max:10 | Definicje sesji (max 10) |
activity_items.*.day | required\|integer\|min:1\|max:7 | Dzień tygodnia |
activity_items.*.start_time | required\|date_format:H:i\|before:end_time | Godzina start |
activity_items.*.end_time | required\|date_format:H:i\|after:start_time | Godzina koniec |
activity_items.*.comment | sometimes\|nullable\|string\|max:5000 | Komentarz |
activity_items.*.teacher_name | sometimes\|nullable\|string\|max:255 | Imię nauczyciela/instruktora |
activity_items.*.teacher_phone | sometimes\|nullable\|string\|max:255 | Telefon nauczyciela |
activity_items.*.teacher_email | sometimes\|nullable\|email\|max:255 | Email nauczyciela |
activity_items.*.level | required\|string\|in:beginner,intermediate,advanced,not_applicable | Poziom |
activity_items.*.reservation_deadline_hours | required\|integer\|min:0 | Deadline |
activity_items.*.start_date | required\|date_format:Y-m-d | Data pierwszej sesji |
activity_items.*.how_many_repeat_times | required\|integer\|min:0\|max:100 | Powtórzenia |
activity_items.*.trainer_id | sometimes\|nullable\|uuid\|exists:trainers,id | Trener |
PATCH /activities/{activityId} (UpdateActivityControllerRequest)¶
Wszystkie pola są opcjonalne (sometimes). Dodatkowe reguły:
item_id—sometimes|nullable|uuid|exists:items,id+ musi być aktywnym obiektemcategory_id—sometimes|nullable|uuid|exists:categories,id+ActiveCategoryRule(activity)max_participants— walidacjaMaxParticipantsActivityUpdateRule: nie można zmniejszyć poniżej aktualnej liczby aktywnych rezerwacjipricing— opcjonalny array, dodaje nowy cennik (nie zastępuje)
Duplikacja zmiany kategorii
Pole category_id jest dostępne zarówno w PATCH /activities/{activityId} jak i w dedykowanym PATCH /activities/{activityId}/change-category. Obie ścieżki pozwalają na zmianę kategorii.
POST /activity-items (StoreActivityItemControllerRequest)¶
| Pole | Reguły | Opis |
|---|---|---|
activity_id | required\|uuid\|exists:activities,id | Powiązana aktywność |
date | required\|date_format:Y-m-d | Data sesji |
start_time | required\|date_format:H:i\|before:end_time | Godzina start |
end_time | required\|date_format:H:i\|after:start_time | Godzina koniec |
comment | sometimes\|nullable\|string\|max:5000 | Komentarz |
trainer_id | sometimes\|nullable\|uuid\|exists:trainers,id\|TrainerAvailabilityRule | Trener (z walidacją dostępności) |
is_active | required\|boolean | Czy aktywna |
level | required\|string\|in:beginner,... | Poziom trudności |
reservation_deadline_hours | required\|integer\|min:0 | Deadline rezerwacji |
how_many_repeat_times | required\|integer\|min:0\|max:100 | Ile razy powtórzyć |
force_add_trainer | sometimes\|nullable\|boolean | Wymuszenie trenera (pomija walidację) |
PATCH /activity-items/{activityItemId} (UpdateActivityItemControllerRequest)¶
Wszystkie pola opcjonalne (sometimes). TrainerAvailabilityRule zastosowana do date, start_time, end_time i trainer_id — wyklucza aktualną sesję z kontroli konfliktów.
PATCH /activities/{activityId}/change-category (ChangeCategoryActivityControllerRequest)¶
| Pole | Reguły | Opis |
|---|---|---|
category_id | required\|uuid\|exists:categories,id\|ActiveCategoryRule(activity) | Nowa kategoria |
Generowanie powtarzających się sesji¶
Parametry¶
| Parametr | Typ | Walidacja | Opis |
|---|---|---|---|
start_date | date | Y-m-d, wymagane | Data pierwszej sesji |
day | int | 1-7, wymagane | Dzień tygodnia (1=Pon, 7=Nd) |
how_many_repeat_times | int | 0-100, wymagane | Ile tygodni generować |
start_time | time | H:i, wymagane, before end_time | Godzina rozpoczęcia |
end_time | time | H:i, wymagane, after start_time | Godzina zakończenia |
trainer_id | UUID? | exists:trainers | Opcjonalny trener |
level | string | wymagane | Poziom trudności |
reservation_deadline_hours | int | wymagane, min: 0 | Deadline rezerwacji |
Przykład¶
{
"activity_items": [
{
"start_date": "2024-02-12",
"day": 1,
"how_many_repeat_times": 10,
"start_time": "18:00",
"end_time": "19:30",
"trainer_id": "uuid-trenera",
"level": "beginner",
"reservation_deadline_hours": 24
}
]
}
Wygeneruje 10 sesji w kolejne poniedziałki.
Limity
- Maksymalnie 10 definicji sesji jednocześnie przy tworzeniu aktywności
- Maksymalnie 100 powtórzeń na definicję
- Sesje tworzone asynchronicznie przez
CreateManyActivityItemsJob(kolejkadomain)
Uprawnienia¶
Role wymagane (middleware)¶
Akcje store, update, changeState, changeCategory, canceled wymagają roli: Admin, Supervisor lub Employee.
| Rola | Podgląd | Tworzenie | Edycja | Zmiana stanu | Anulowanie sesji |
|---|---|---|---|---|---|
| Administrator | |||||
| Supervisor | |||||
| Employee | |||||
| Klient | Publiczne |
Polityki autoryzacji (Policy)¶
Poza wymaganymi rolami, system sprawdza polityki organizacyjne (ActivityPolicy, ActivityItemPolicy):
ActivityPolicy¶
| Akcja | Supervisor | Employee |
|---|---|---|
view | belongsToOrganization | belongsToOrganization |
create | belongsToOrganization (przez Item) | isItemManager (musi zarządzać danym Item) |
update | belongsToOrganization | isItemManager (Activity musi mieć item_id) |
ActivityItemPolicy¶
| Akcja | Supervisor | Employee |
|---|---|---|
view | belongsToOrganization (przez Activity) | belongsToOrganization (przez Activity) |
create | belongsToOrganization (przez Activity) | isItemManager (Activity musi mieć item_id) |
update | belongsToOrganization (przez Activity) | isItemManager (Activity musi mieć item_id) |
Kluczowa reguła
Employee może zarządzać aktywnościami tylko dla obiektów (Item), do których jest przypisany jako manager. Supervisor może zarządzać wszystkimi aktywnościami w swojej organizacji.
Struktura plików domeny¶
app/Domain/Activity/
├── Action/
│ ├── Attr/ # Atrybuty wyliczane
│ │ ├── GetAvailableSlotsAttr.php
│ │ ├── GetCurrentPriceAttr.php
│ │ ├── GetCurrentPriceGrossAttr.php
│ │ ├── GetCurrentPriceVatRateAttr.php
│ │ ├── GetCurrentPricingAttr.php
│ │ ├── GetDateXTimeAttr.php
│ │ ├── GetLowestPriceAttr.php
│ │ ├── GetLowestPriceGrossAttr.php
│ │ ├── GetLowestPriceVatRateAttr.php
│ │ └── GetLowestPricingAttr.php
│ ├── Controller/ # Akcje kontrolerów
│ │ ├── IndexActivityController.php
│ │ ├── IndexActivityItemController.php
│ │ ├── ShowActivityController.php
│ │ ├── ShowActivityItemController.php
│ │ ├── StoreActivityController.php
│ │ ├── StoreActivityItemController.php
│ │ ├── UpdateActivityController.php
│ │ └── UpdateActivityItemController.php
│ ├── Other/ # Logika biznesowa i observery
│ │ ├── ActivityItemWithMaxReservations.php
│ │ ├── ApplyActivityAggregatesOrm.php
│ │ ├── ApplyActivityItemAggregatesOrm.php
│ │ ├── CancelItemsForDisabledActivity.php
│ │ ├── CreateActivity.php
│ │ ├── CreateActivityItem.php
│ │ ├── CreateManyActivityItems.php
│ │ ├── CreatingActivityObserver.php
│ │ ├── ExceptionCanceledActivityItem.php
│ │ ├── ExceptionChangeStateActivity.php
│ │ ├── ExceptionChangeStateActivityItem.php
│ │ ├── UpdateActivity.php
│ │ ├── UpdatedActivityObserver.php
│ │ ├── UpdatingActivityItemObserver.php
│ │ ├── UpdatingActivityObserver.php
│ │ ├── UpdatingIsActiveActivityObserver.php
│ │ └── UpdatingIsCanceledActivityItemObserver.php
│ ├── Scope/ # Query scopes
│ │ ├── ScopeActivityItemHasAvailableSlotsFunc.php
│ │ ├── ScopeDateGreaterThanTodayFunc.php
│ │ ├── ScopeDatesBetweenFunc.php
│ │ ├── ScopePriceFromFunc.php
│ │ ├── ScopePriceToFunc.php
│ │ └── ScopeSearchFunc.php
│ └── Sort/ # Custom sorty
│ ├── SortByActivityCreatedAt.php
│ ├── SortByActivityName.php
│ ├── SortByAvgReviews.php
│ └── SortByLowestPrice.php
├── Controller/
│ ├── ActivityController.php
│ └── ActivityItemController.php
├── Job/
│ ├── CancelReservationsForActivityItemJob.php
│ └── CreateManyActivityItemsJob.php
├── Model/
│ ├── Activity.php
│ └── ActivityItem.php
├── Observer/
│ ├── ActivityObserver.php
│ └── ActivityItemObserver.php
├── Policy/
│ ├── ActivityPolicy.php
│ └── ActivityItemPolicy.php
├── Request/
│ ├── ChangeCategoryActivityControllerRequest.php
│ ├── IndexActivityControllerRequest.php
│ ├── IndexActivityItemControllerRequest.php
│ ├── Rule/
│ │ ├── MaxParticipantsActivityUpdateRule.php
│ │ └── TrainerAvailabilityRule.php
│ ├── StoreActivityControllerRequest.php
│ ├── StoreActivityItemControllerRequest.php
│ ├── UpdateActivityControllerRequest.php
│ └── UpdateActivityItemControllerRequest.php
├── Resource/
│ ├── ActivityItemResource.php
│ └── ActivityResource.php
└── Service/
├── ActivityEnumService.php
├── TraitActivity.php
└── TraitControllerActivity.php
Znane uwagi techniczne¶
⚠️ Cross-domain coupling: ExceptionChangeStateActivity
Klasa ExceptionChangeStateActivity (walidacja aktywacji aktywności) rzuca wyjątek ChangeStateItemException z domeny Item zamiast ChangeStateActivityException z domeny Activity. Dodatkowo używa TraitItem zamiast TraitActivity. Funkcjonalnie działa poprawnie (HTTP 409 z prawidłowym komunikatem), ale stanowi coupling między domenami. Klasa ExceptionChangeStateActivityItem poprawnie używa ChangeStateActivityItemException.
Niespójność filtrowania is_active w wyliczaniu najniższej ceny
Na endpointach listujących (GET /activities) najniższa cena wyliczana jest przez ApplyActivityAggregatesOrm z filtrem is_active = true na cennikach. Na endpointach szczegółowych (GET /activities/{activityId}) accessor GetLowestPricingAttr NIE filtruje po is_active i używa withoutGlobalScopes(). W rezultacie te dwie ścieżki mogą zwrócić różną wartość lowest_price — accessor uwzględnia historyczne (dezaktywowane) cenniki, a ORM aggregate tylko aktywne.
Pole is_public nie jest zwracane w odpowiedziach API
Zarówno ActivityResource jak i ActivityItemResource nie zawierają pola is_public w odpowiedzi JSON, mimo że pole istnieje w bazie danych. Widoczność publiczna jest używana wewnętrznie (filtrowanie na liście), ale nie jest eksponowana klientom API.
Niedeterministyczne computed_current_price w ApplyActivityAggregatesOrm
Subquery computed_current_price (i computed_current_price_vat_rate) używa LIMIT 1 bez ORDER BY. Jeśli aktywność ma więcej niż jeden aktywny cennik (z client_id IS NULL), zwrócony wiersz zależy od silnika bazy danych i nie jest gwarantowany. Dla porównania, computed_lowest_price poprawnie używa ->orderBy('pricings.value').
Rozbieżność metod zliczania rezerwacji: scope vs aggregates
ScopeActivityItemHasAvailableSlotsFunc liczy rezerwacje metodą inkluzji (statusy PENDING + CONFIRMED), natomiast ApplyActivityItemAggregatesOrm (active_reservations_count) metodą ekskluzji (wszystkie oprócz CANCELED i REJECTED). Jeśli istnieją rezerwacje z innymi statusami (np. completed, no_show), wyniki obu metod mogą się różnić.
Pole is_public brak w $casts modelu Activity
Pole is_public jest w $fillable modelu Activity, ale nie jest zdefiniowane w $casts (w odróżnieniu od is_active i is_paid rzutowanych na boolean). MySQL zwraca tinyint, co działa prawidłowo w warunkach logicznych, ale nie przechodzi explicit boolean cast.
CreateActivity nie przekazuje wszystkich pól walidowanych
Klasa CreateActivity (Action/Other/CreateActivity.php) tworzy model Activity za pomocą jawnej listy pól w Activity::create([...]). Pola is_public_price_list, can_pay_online, cancellation_deadline_hours i cancellation_policy_description nie są przekazywane mimo ich akceptacji w StoreActivityControllerRequest. Wartości wysłane przez klienta API w POST /activities dla tych pól są ignorowane — zastosowane zostaną domyślne wartości z migracji. Dla porównania, UpdateActivity używa $activity->fill($validated)->save(), więc update poprawnie obsługuje te pola.
Duplikat filtra category.name w IndexActivityController
W IndexActivityController filtr category.name (partial) pojawia się dwukrotnie w tablicy filtrów (linie 22 i 29). Spatie QueryBuilder ignoruje duplikaty, więc efekt funkcjonalny jest zerowy, ale stanowi niespójność w kodzie.
Różnice w eager loading: Index vs Show
IndexActivityController ładuje relacje ActivityResource::RELATIONS + ['item.childrens'], natomiast ShowActivityController ładuje tylko ActivityResource::RELATIONS (bez item.childrens). Może to powodować N+1 query na endpoincie Show dla accessora is_parent na relacji item. Analogicznie, IndexActivityItemController ładuje rozbudowany zestaw zagnieżdżonych relacji (activity.organization, activity.category, activity.media, activity.reviews, activity.item, activity.pricings, trainer), natomiast ShowActivityItemController ładuje jedynie ActivityItemResource::RELATIONS (['activity', 'trainer']), co wymusza lazy loading zagnieżdżonych relacji.
Potencjalny bug: parametr day — walidacja 1-7 vs algorytm 0-6
Walidacja activity_items.*.day w StoreActivityControllerRequest dopuszcza zakres min:1|max:7 (dokumentacja: 1=Pon, 7=Nd). Jednak FindDateXNameDaysToFuture (Support/Action/Util/) używa tablicy DAYS_OF_WEEK z indeksami 0-6 (0=niedziela, 6=sobota, standard Carbon dayOfWeek). Wartość day=7 przejdzie walidację, ale spowoduje array index out-of-bounds w runtime. Wartości 1-6 działają poprawnie (1=poniedziałek, 6=sobota). Problem dotyczy tylko endpoint POST /activities — w POST /activity-items pole day jest wyliczane automatycznie z Carbon::parse($date)->dayOfWeek (zakres 0-6).
Pole is_public w tabeli activity_items — brak w $fillable
Kolumna is_public istnieje w tabeli activity_items, ale nie jest zdefiniowana w $fillable modelu ActivityItem. Nie jest mass-assignable przez standardowe operacje Eloquent. Akcje CreateManyActivityItems i CreateActivityItem nie ustawiają tego pola — przyjmuje wartość domyślną z bazy (true).
Vestigial key reservations w ActivityItemResource::UNALLOWED_NO_AUTH_KEYS
Stała UNALLOWED_NO_AUTH_KEYS w ActivityItemResource zawiera klucz reservations, ale relacja reservations jest zakomentowana w RELATIONS i pole nie jest obecne w toArray(). Filtrowanie tego klucza w reduceResourceForNoAuth jest bezskuteczne (no-op). Prawdopodobnie pozostałość po wcześniejszej wersji resource, gdy reservations był eager-loaded.
Rozbieżność definicji najniższej ceny: Sort vs ORM vs Accessor
System posiada trzy różne definicje „najniższej ceny":
SortByLowestPrice(sortowanie na liście sesji) —MIN(value)z wszystkich aktywnych cenników (bez ograniczenia czasowego)ApplyActivityAggregatesOrm(wyświetlana wartość na liście) —MIN(value)z aktywnych cenników z ostatnich 31 dni (created_at >= 31 dni temu)GetLowestPricingAttr(wyświetlana wartość na szczegółach) —MIN(value)ze wszystkich cenników (bez filtrais_active, zwithoutGlobalScopes())
W rezultacie sortowanie po lowest_price może dać kolejność niespójną z wyświetlanymi wartościami lowest_price.
Brak server-side filtrowania is_public=false dla endpointów no-auth
Endpointy publiczne (/no-auth/activities, /no-auth/activity-items) używają tych samych kontrolerów co endpointy autoryzowane. System NIE filtruje automatycznie aktywności z is_public=false na endpointach no-auth. Aby wyświetlić tylko publiczne aktywności, frontend musi jawnie przekazać filter[is_public]=true lub filter[activity.is_public]=true jako parametr zapytania. Bez tego parametru niezalogowani użytkownicy mogą zobaczyć niepubliczne aktywności.
Scope date_greater_than_today uwzględnia godzinę rozpoczęcia
Scope ScopeDateGreaterThanTodayFunc porównuje STR_TO_DATE(CONCAT(date, ' ', start_time)) z now() — filtruje nie tylko po dacie, ale po dokładnej dacie+godzinie rozpoczęcia. Sesja zaplanowana na dziś, ale z godziną rozpoczęcia w przeszłości, zostanie wykluczona z wyników.
Brak walidacji dostępności trenera przy inline session items
Przy tworzeniu aktywności z sesjami inline (POST /activities z activity_items), pole trainer_id nie jest objęte regułą TrainerAvailabilityRule. Walidacja dostępności trenera odbywa się tylko na endpoincie POST /activity-items (tworzenie samodzielnych sesji). Sesje inline generowane asynchronicznie przez CreateManyActivityItemsJob mogą tworzyć konflikty trenerów.
Best practices¶
Wskazówki dla administratorów
- Generuj z wyprzedzeniem — twórz sesje na kilka tygodni naprzód
- Ustawiaj deadline — daj sobie czas na organizację
- Monitoruj frekwencję — analizuj popularne terminy
- Przypisuj trenerów — sprawdzaj dostępność przed przypisaniem
- Dokumentuj anulacje — informuj uczestników o przyczynach
- Sprawdzaj limity — nie zmniejszaj max_participants poniżej aktualnych rezerwacji
Historia zmian¶
| Data | Typ | Opis |
|---|---|---|
| 26 lut 2026 | Korekta | Audyt dokumentacji Activity (faza 5 — service traits, actions, resources, scopes, sorts) — dodano 4 nowe uwagi techniczne: rozbieżność trzech definicji lowest_price (Sort vs ORM vs Accessor); brak server-side filtrowania is_public=false na endpointach no-auth; scope date_greater_than_today uwzględnia godzinę (nie tylko datę); brak TrainerAvailabilityRule na inline session items w POST /activities. Poprawiono opis filtra date_greater_than_today w tabeli filtrów |
| 26 lut 2026 | Korekta | Audyt dokumentacji Activity (faza 4 — deep-verify action classes i resources) — dodano 3 nowe uwagi techniczne: potencjalny bug day walidacja 1-7 vs algorytm FindDateXNameDaysToFuture 0-6 (day=7 powoduje array out-of-bounds); pole is_public w tabeli activity_items brak w $fillable; vestigial key reservations w ActivityItemResource::UNALLOWED_NO_AUTH_KEYS. Rozszerzono sekcje "Ograniczenia dla niezalogowanych" o opis zachowania obu Resources (ActivityResource i ActivityItemResource) |
| 26 lut 2026 | Korekta | Audyt dokumentacji Activity (faza 3 — deep-verify controllers) — poprawiono opis Dyrektywy Omnibus (dodano rozroznienie miedzy ORM a Attr); dodano 3 nowe uwagi techniczne: CreateActivity nie przekazuje pol is_public_price_list, can_pay_online, cancellation_deadline_hours, cancellation_policy_description (bug w kodzie); duplikat filtra category.name w IndexActivityController; roznice w eager loading miedzy Index a Show (potencjalne N+1 na Show); rozszerzono tabele atrybutow wyliczanych ActivityItem o atrybuty wewnetrzne (date_start_time, date_end_time, total_relations) z oznaczeniem widocznosci w API |
| 26 lut 2026 | Korekta | Audyt dokumentacji Activity (faza 1) — dodano kody HTTP 409 do tabel wyjątków aktywacji/anulowania; dodano brakujące pola walidacji inline activity items (teacher_name, teacher_phone, teacher_email); dodano brakujący atrybut level_lang do tabeli atrybutów wyliczanych ActivityItem; dodano sekcję "Znane uwagi techniczne": cross-domain coupling w ExceptionChangeStateActivity (używa ChangeStateItemException z domeny Item), niespójność filtrowania is_active w wyliczaniu lowest_price między ORM a Attr, brak is_public w odpowiedziach API Resources |
| 26 lut 2026 | Korekta | Audyt dokumentacji Activity (faza 2 — deep verification) — poprawiono parametry routingu z {id} na {activityId} / {activityItemId} (16 endpointów + 3 nagłówki walidacji); poprawiono opis observera UpdatedActivityObserver z „zeruje aktywny cennik" na „tworzy nowy cennik z value=0"; dodano pola item_id i category_id jako aktualizowalne w PATCH /activities/{activityId} z adnotacją o duplikacji zmiany kategorii; dodano 3 nowe uwagi techniczne: niedeterministyczne computed_current_price (LIMIT 1 bez ORDER BY), rozbieżność metod zliczania rezerwacji (scope inkluzja vs aggregate ekskluzja), brak is_public w $casts modelu Activity |
| 25 lut 2026 | Aktualizacja dokumentacji | Rozszerzenie dokumentacji domeny Activity — dodano sekcję "Atrybuty wyliczane Activity" (14 atrybutów), sekcję "Struktura tabel w bazie danych" (pełne definicje kolumn obu tabel z indeksami i FK), rozszerzono sekcję "Uprawnienia" o szczegółowe polityki autoryzacji (ActivityPolicy, ActivityItemPolicy) z logiką organizacyjną (belongsToOrganization, isItemManager), dodano sekcję "Walidacja — szczegółowe reguły" (pełne reguły walidacji dla wszystkich 5 Form Requestów), uzupełniono strukturę plików domeny (dodano katalog Policy/ i CancelReservationsForActivityItemJob) |
| 18 lut 2026 | Optymalizacja | Eliminacja N+1 query w GET /api/activities — Dodano 'item.childrens' do eager loading w IndexActivityController (array_merge(ActivityResource::RELATIONS, ['item.childrens'])). Eliminuje x5 Item→Item N+1 wywoływane przez GetIsParentAttr gdy ActivityResource odpytuje is_parent na załadowanym obiekcie item |
| 17 lut 2026 | Nowa funkcjonalność | Pełne anulowanie rezerwacji przy odwołaniu zajęć — zamieniono cancelModelReservations (tylko zmiana statusu) na CancelReservationsForActivityItemJob dispatchowany na dedykowanej kolejce cancellations. Nowy flow: anulowanie slotów, automatyczne zwroty płatności, notyfikacja email (ReservationCancelledDueToActivityCancellationNotification) z informacją o odwołanych zajęciach (nazwa, data, godzina). Nowe enum cases: CANCELLATION_TYPE_DUE_TO_ACTIVITY_CANCELLATION, REFUND_TYPE_ACTIVITY_CANCELLATION. Job przetwarzany z afterCommit() — ruszy dopiero po commitcie transakcji |
| 15 lut 2026 | Aktualizacja dokumentacji | Kompleksowa aktualizacja dokumentacji — poprawiono opis filtrów (dodano brakujące filtry udogodnień: winda, zwierzęta, toalety, kuchnia, ogród), poprawiono nazwę filtra has_disabled_access → are_there_facilities_for_disabled_people, poprawiono opis scope search (szuka w name/desc/tags/meta/category, nie w organizacji/lokalizacji), dodano brakujący sort date, uzupełniono tabelę pól Activity (comment, meta_title, meta_description, tags) i ActivityItem (is_active, comment), dodano sekcję observerów, walidacji aktywacji (exceptions), strukturę plików domeny, atrybuty wyliczane, typy relacji, limity walidacyjne (max 10 definicji sesji, max 100 powtórzeń) |
| 20 lut 2026 | Aktualizacja | Rezerwacje oczekujące w obliczaniu wolnych miejsc — ApplyActivityItemAggregatesOrm dodaje active_waiting_reservations_count (withCount na waitingReservations z filtrem expires_at > now()). Formuła available_slots uwzględnia teraz rezerwacje oczekujące: max_participants - active_reservations_count - active_waiting_reservations_count. Scope activity_item_has_available_slots również uwzględnia rezerwacje oczekujące w zapytaniu SQL. ActivityItemResource zwraca nowe pole active_waiting_reservations_count |
| 11 lut 2026 | Optymalizacja | Eliminacja N+1 query w GET /api/activities i GET /api/activity-items — (1) Utworzono ApplyActivityAggregatesOrm dla Activity z subqueries cenowych (computed_current_price, computed_lowest_price + vat_rate), withCount (reviews, reservations), withAvg (reviews_avg_rating), withExists (reservations, reviews), with('media'). Zmodyfikowano 8 accessorów w modelu Activity, (2) Utworzono ApplyActivityItemAggregatesOrm z withCount (active_reservations_count), withExists (reservations). Zmodyfikowano getIsInUseAttribute w modelu ActivityItem. Zaktualizowano TraitActivity, IndexActivityController i IndexActivityItemController |