Przejdź do treści

Dziennik Statusów (StatusLog)

Ostatnia aktualizacja dokumentacji: 2026-02-26 Stan synchronizacji z kodem: Zsynchronizowany


1. Opis ogólny

Domena Dziennika Statusów rejestruje pełną historię wszystkich zmian statusów encji w systemie. Działa jak elektroniczna pieczątka — każde przejście statusu jest nieodwracalnie odnotowane z informacją o tym, kto zmienił, kiedy zmienił i z jakiego na jaki status.

System opiera się na wzorcu Observer Eloquent — modele oznaczone atrybutem #[ObservedBy(StatusLogObserver::class)] automatycznie rejestrują każdą zmianę pola status bez żadnej dodatkowej logiki w kontrolerach ani serwisach.

Co domena rozwiązuje

  • Pełny audit trail zmian statusów wymagany przez przepisy dot. JST
  • Identyfikacja użytkownika odpowiedzialnego za każdą zmianę
  • Rejestracja stanu początkowego przy tworzeniu encji
  • Podstawa do monitorowania SLA — np. czas obsługi reklamacji
  • Podstawa dla narzędzi raportowania historycznego

Modele objęte śledzeniem

Aktualnie śledzenie statusów włączone jest w trzech modelach:

Model Klasa PHP Śledzone pole
Rezerwacja Domain\Reservation\Model\Reservation status
Reklamacja Domain\Complaint\Model\Complaint status
Dokument prawny Domain\LegalDocument\Model\LegalDocument status (przez is_active)

Uwaga: LegalDocument posiada atrybut #[ObservedBy(StatusLogObserver::class)], jednak model nie ma pola status — observer nie zapisze wpisu przy tworzeniu (warunek if ($model->status)) ani aktualizacji (brak wasChanged('status')). Wpis jest tworzony jedynie gdy model rzeczywiście posiada pole status i to pole zostanie zmienione.


2. Architektura domeny

2.1 Modele danych

StatusLog

Plik: app/Domain/StatusLog/Model/StatusLog.php

Pole Typ w PHP Typ w bazie Nullable Opis
id string (UUID) uuid Nie Klucz główny generowany przez HasUuids
loggable_type string string Nie Klasa PHP modelu źródłowego (np. Domain\Reservation\Model\Reservation)
loggable_id string (UUID) uuid Nie UUID encji źródłowej
old_status string\|null string Tak Poprzedni status (null przy pierwszym wpisie)
new_status string\|null string Tak Nowy status
changed_by string\|null (UUID) uuid Tak UUID użytkownika — null dla operacji systemowych
note string\|null text Tak Opcjonalna notatka do zmiany (nie wypełniana automatycznie przez observer)
created_at Carbon timestamp Nie Data i godzina zmiany
updated_at Carbon timestamp Nie Data modyfikacji wpisu

Klucze:

  • id — klucz główny UUID, nie auto-increment
  • changed_by — klucz obcy do tabeli users z onDelete('cascade')
  • loggable_type + loggable_id — UUID morph (kolumny generowane przez $table->uuidMorphs('loggable'))

Cechy modelu:

  • Używa HasUuids — UUID generowany automatycznie
  • $keyType = 'string', $incrementing = false
  • Rozszerza Infrastructure\Vendor\CustomModel — posiada globalne scope'y UserAccessesGlobalScope i IsActiveGlobalScope
  • Activity log wyłączony (logOnly([])->dontSubmitEmptyLogs())

Relacje:

// Polimorficzna relacja do encji źródłowej
public function loggable(): MorphTo

// Użytkownik który zmienił status
public function changedBy(): BelongsTo  // -> User

2.2 Diagram relacji (Mermaid)

erDiagram
    StatusLog {
        uuid id PK
        string loggable_type
        uuid loggable_id
        string old_status
        string new_status
        uuid changed_by FK
        text note
        datetime created_at
        datetime updated_at
    }

    User {
        uuid id PK
        string name
        string email
    }

    Reservation {
        uuid id PK
        string status
        uuid organization_id
    }

    Complaint {
        uuid id PK
        string status
        uuid organization_id
    }

    LegalDocument {
        uuid id PK
        boolean is_active
    }

    StatusLog }o--|| User : "changed_by"
    StatusLog }o--o| Reservation : "loggable (morphTo)"
    StatusLog }o--o| Complaint : "loggable (morphTo)"
    StatusLog }o--o| LegalDocument : "loggable (morphTo)"

2.3 Struktura tabel w bazie danych

Tabela: status_logs

Migracja: database/migrations/0001_01_01_000011_create_status_logs.php

CREATE TABLE status_logs (
    id          UUID PRIMARY KEY,
    loggable_type VARCHAR(255) NOT NULL,   -- klasa modelu
    loggable_id   UUID NOT NULL,            -- UUID encji
    old_status    VARCHAR(255) NULL,
    new_status    VARCHAR(255) NULL,
    changed_by    UUID NULL REFERENCES users(id) ON DELETE CASCADE,
    note          TEXT NULL,
    created_at    TIMESTAMP NULL,
    updated_at    TIMESTAMP NULL
);

Kolumny loggable_type i loggable_id tworzone są przez $table->uuidMorphs('loggable'), który automatycznie dodaje indeks na parze (loggable_type, loggable_id).


3. Endpointy API

Przegląd endpointów

Metoda URL Akcja Middleware
GET /status-logs Lista wpisów z paginacją auth:api, role: admin/supervisor/employee

Wszystkie endpointy StatusLog są wyłącznie w grupie autoryzowanej (routes/api/auth/statusLog.php). Brak endpointów publicznych (no-auth).


GET /status-logs

Opis: Zwraca paginowaną listę wpisów dziennika statusów z możliwością filtrowania po encji źródłowej.

Middleware: auth:api, role:admin|supervisor|employee

Kontroler: Domain\StatusLog\Controller\StatusLogController::index

Akcja: Domain\StatusLog\Action\Controller\IndexStatusLogController

Parametry zapytania

Parametr Typ Wymagany Opis
page int (min: 1) Tak Numer strony
per_page int (min: 1, max: 50) Tak Liczba elementów na stronę
filter[id] string (UUID) Nie Filtr dokładny po ID wpisu
filter[loggable_id] string (UUID) Nie Filtr dokładny po ID encji
filter[loggable_type] string Nie Filtr dokładny po typie encji
filter[{fillable}] mixed Nie Filtry po dowolnym polu fillable (dla zalogowanych)
sort string Nie Sortowanie (np. sort=-created_at)
include string Nie Nie obsługiwane bezpośrednio — relacje ładowane automatycznie

Walidacja (klasa IndexStatusLogControllerRequest):

return $this->paginationDefaultRequestRules();
// Ekwiwalent:
[
    'page'     => ['required', 'int', 'min:1'],
    'per_page' => ['required', 'int', 'min:1', 'max:50'],
]

Przykład zapytania

GET /status-logs?page=1&per_page=10&filter[loggable_id]=550e8400-e29b-41d4-a716-446655440000&filter[loggable_type]=Domain\Reservation\Model\Reservation
Authorization: Bearer {jwt_token}

Przykład odpowiedzi (HTTP 200)

{
  "data": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "loggable_type": "Domain\\Reservation\\Model\\Reservation",
      "loggable_id": "550e8400-e29b-41d4-a716-446655440000",
      "old_status": "pending",
      "old_status_lang": "Pending",
      "new_status": "confirmed",
      "new_status_lang": "Confirmed",
      "changed_by": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "note": null,
      "can_be_edit": false,
      "created_at": "2026-02-26 10:15:00",
      "updated_at": "2026-02-26 10:15:00",
      "can": {
        "view": true,
        "update": false,
        "create": false
      },
      "changedBy": {
        "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
        "name": "Jan Kowalski",
        "email": "jan.kowalski@example.com"
      }
    },
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "loggable_type": "Domain\\Reservation\\Model\\Reservation",
      "loggable_id": "550e8400-e29b-41d4-a716-446655440000",
      "old_status": null,
      "old_status_lang": null,
      "new_status": "pending",
      "new_status_lang": "Pending",
      "changed_by": null,
      "note": null,
      "can_be_edit": false,
      "created_at": "2026-02-26 09:00:00",
      "updated_at": "2026-02-26 09:00:00",
      "can": {
        "view": true,
        "update": false,
        "create": false
      },
      "changedBy": null
    }
  ],
  "links": {
    "first": "http://api.zajmij.to/status-logs?page=1",
    "last": "http://api.zajmij.to/status-logs?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "per_page": 10,
    "to": 2,
    "total": 2
  }
}

Struktura obiektu StatusLog w odpowiedzi

Zdefiniowana w Domain\StatusLog\Resource\StatusLogResource:

Pole Typ Opis
id string (UUID) Identyfikator wpisu
loggable_type string\|null Pełna klasa PHP encji (np. Domain\Reservation\Model\Reservation)
loggable_id string (UUID) UUID encji źródłowej
old_status string\|null Poprzedni kod statusu (null przy tworzeniu)
old_status_lang string\|null Przetłumaczona nazwa poprzedniego statusu
new_status string\|null Nowy kod statusu
new_status_lang string\|null Przetłumaczona nazwa nowego statusu
changed_by string\|null UUID użytkownika (null = zmiana systemowa)
note string\|null Opcjonalna notatka
can_be_edit bool Zawsze false (logika z CustomModel::getCanBeEditAttribute)
created_at string Data zmiany w formacie Y-m-d H:i:s
updated_at string Data aktualizacji wpisu
can object Uprawnienia zalogowanego użytkownika (niedostępne bez auth)
changedBy object\|null Zagnieżdżony zasób User — zawsze eager-loaded

Uwaga dot. can: Klucz can jest ukrywany w odpowiedziach dla niezalogowanych użytkowników (stała UNALLOWED_NO_AUTH_KEYS = ['can']). Dla zalogowanych zawiera: { view: bool, update: bool, create: bool }.


4. Logika biznesowa

4.1 Główne procesy

Automatyczne rejestrowanie zmian statusów

Kluczowy mechanizm domeny to StatusLogObserver rejestrowany przez PHP atrybut #[ObservedBy] bezpośrednio na modelu Eloquent.

sequenceDiagram
    actor U as Użytkownik/System
    participant M as Model (Reservation / Complaint)
    participant O as StatusLogObserver
    participant SL as StatusLog (tabela)
    participant Auth as auth()

    U->>M: Model::create(['status' => 'pending', ...])
    M->>O: hook: created(Model $model)
    O->>O: if ($model->status) — sprawdza czy pole status istnieje
    O->>Auth: auth()->check()
    Auth-->>O: true/false
    O->>SL: StatusLog::create([loggable_type, loggable_id, old_status: null, new_status: 'pending', changed_by])
    SL-->>O: wpis utworzony

    U->>M: $reservation->update(['status' => 'confirmed'])
    M->>O: hook: updated(Model $model)
    O->>O: $model->wasChanged('status') — sprawdza czy status się zmienił
    O->>O: $model->getOriginal('status') — pobiera poprzedni status
    O->>SL: StatusLog::create([..., old_status: 'pending', new_status: 'confirmed', ...])
    SL-->>O: wpis utworzony

Lokalizacja statusów

Metoda statusLangModelHelper() z TraitSupport konwertuje kod statusu na czytelną nazwę.

Obsługiwane modele w Support\Action\Util\StatusLangModelHelper:

Klasa modelu Źródło tłumaczenia
Domain\Reservation\Model\Reservation ReservationEnumService::getReservationStatuses()
Domain\Complaint\Model\Complaint ComplaintEnumService::getComplaintStatuses()
Inne Zwracany oryginalny kod statusu

Pobieranie listy wpisów

Wywoływany przez IndexStatusLogController::__invoke():

  1. Konstruuje listę filtrów: id (exact), loggable_id (exact), loggable_type (exact) + pola fillable modelu
  2. Używa Spatie\QueryBuilder\QueryBuilder do budowania zapytania z filtrami i sortowaniem
  3. Domyślne sortowanie: created_at DESC (gdy brak sortu)
  4. Eager-loading relacji: ['changedBy']
  5. Paginacja przez fastPaginate($validated['per_page'])
  6. Każdy wpis sprawdzany przez StatusLogPolicy::view() (policy per-item)

4.2 Reguły biznesowe

  1. Wpis przy tworzeniu: Observer created() zapisuje wpis tylko gdy $model->status jest niepuste. old_status = null.
  2. Wpis przy aktualizacji: Observer updated() zapisuje wpis tylko gdy $model->wasChanged('status') zwraca true. old_status = $model->getOriginal('status').
  3. Zmiana bez logowania: Gdy auth()->check() = false, pole changed_by zapisywane jako null — operacje systemowe (komendy CRON, jobs) są w pełni obsługiwane.
  4. Pole note: Nie jest wypełniane automatycznie przez observer. Wartość null w zdecydowanej większości wpisów. Może być wypełnione przy ręcznym tworzeniu.
  5. Brak tworzenia/edycji przez API: PolicyI create() i update() zwracają false — nie ma żadnego endpointu do tworzenia ani edycji wpisów. Wpisy są tworzone wyłącznie przez observer.
  6. Niemodyfikowalność wpisów: can_be_edit zawsze false — historia statusów jest niezmienialnym dziennikiem audytowym.
  7. Kaskadowe usuwanie: Klucz obcy changed_by z onDelete('cascade') — usunięcie użytkownika usunie powiązane wpisy dziennika (identyfikacja autora zmiany przestaje być możliwa).

4.3 Walidacje

Jedyna walidacja wejściowa dotyczy paginacji w IndexStatusLogControllerRequest:

Pole Reguły
page required, int, min:1
per_page required, int, min:1, max:50

Filtry przekazywane przez query string (filter[*]) są walidowane przez Spatie\QueryBuilder na podstawie listy allowedFilters. Próba użycia niezdefiniowanego filtru skutkuje błędem 400.


5. Autoryzacja i uprawnienia

Middleware routingowe

Endpoint GET /status-logs wymaga roli:

$this->middleware(
    'role:admin|supervisor|employee',
    ['only' => ['index']]
);

Klienci (client) nie mają dostępu do tego endpointu.

Policy: StatusLogPolicy

Plik: app/Domain/StatusLog/Policy/StatusLogPolicy.php

Zarejestrowana w AuthServiceProvider: StatusLog::class => StatusLogPolicy::class

Metoda Policy Wynik Opis
before() (z BasePolicy) true Administrator zawsze ma dostęp do wszystkiego
view(User, StatusLog) Zależy Sprawdza przynależność do organizacji
create(User) false Tworzenie zabronione przez API
update(User, StatusLog) false Edycja zabroniona przez API

Logika view():

flowchart TD
    A[StatusLogPolicy::view] --> B[resolveOrganizationId: Pobierz loggable bez globalnych scope'ów]
    B --> C{loggable istnieje?}
    C -- Nie --> D[return true - dostęp przyznany]
    C -- Tak --> E{loggable ma organization_id?}
    E -- Nie --> D
    E -- Tak --> F[belongsToOrganization: Czy user należy do organizacji?]
    F -- Tak --> G[return true]
    F -- Nie --> H[return false]

Kluczowe zachowanie: Policy używa withoutGlobalScopes() przy pobieraniu loggable — dzięki temu może zweryfikować organizację nawet gdy encja jest nieaktywna lub ukryta przez globalne scope'y.

Dostęp Administratora: Metoda before() z BasePolicy zwraca true dla użytkowników z rolą admin — pomijają oni sprawdzanie view().

Macierz uprawnień

Rola GET /status-logs Widoczność wpisów
admin Tak Wszystkie (via before() w policy)
supervisor Tak Własna organizacja
employee Tak Własna organizacja
Klient (client) Nie (403)
Niezalogowany Nie (401)

6. Eventy i efekty uboczne

Zdarzenia wyzwalające zapis do StatusLog

flowchart LR
    subgraph Reservation
        R1[Reservation::create] --> OC1[Observer::created]
        R2[Reservation::update status] --> OU1[Observer::updated]
    end
    subgraph Complaint
        C1[Complaint::create] --> OC2[Observer::created]
        C2[Complaint::update status] --> OU2[Observer::updated]
    end
    subgraph LegalDocument
        L1[LegalDocument::create] --> OC3[Observer::created - brak pola status]
        L2[LegalDocument::update] --> OU3[Observer::updated - brak wasChanged status]
    end
    OC1 --> SL[(status_logs)]
    OU1 --> SL
    OC2 --> SL
    OU2 --> SL
    OC3 -. brak wpisu .-> SL
    OU3 -. brak wpisu .-> SL

Brak efektów ubocznych

Domena StatusLog sama w sobie nie emituje żadnych zdarzeń (events), nie wysyła powiadomień ani nie wywołuje jobów. Jest wyłącznie konsumentem zdarzeń modeli.


7. Notyfikacje

Domena StatusLog nie posiada żadnych powiadomień. Nie ma klasy Notification w tej domenie.

Powiadomienia związane ze zmianami statusów (np. powiadomienie o zmianie statusu rezerwacji) są obsługiwane przez domeny: Notification i Reservation.


8. Powiązania z innymi domenami

Modele śledzące statusy

Domena Model Pole status Możliwe wartości
Rezerwacje Reservation status pending, confirmed, canceled, rejected
Reklamacje Complaint status new, in_progress, resolved, closed
Dokumenty prawne LegalDocument (brak pola status)

Statusy Reservation

Zdefiniowane w Domain\Reservation\Service\ReservationEnumService:

Kod Nazwa EN Opis
pending Pending Nowa, oczekuje na potwierdzenie
confirmed Confirmed Potwierdzona przez administratora
canceled Canceled Anulowana przez klienta lub system
rejected Rejected Odrzucona przez administratora

Statusy Complaint

Zdefiniowane w Domain\Complaint\Service\ComplaintEnumService:

Kod Nazwa EN Opis
new New Nowo złożona reklamacja
in_progress In progress W trakcie rozpatrywania
resolved Resolved Problem rozwiązany
closed Closed Sprawa definitywnie zamknięta

Relacja zwrotna z Reservation

Model Reservation posiada relację morphMany do StatusLog:

// Domain\Reservation\Model\Reservation
public function statusLogs(): MorphMany
{
    return $this->morphMany(StatusLog::class, 'loggable');
}

Relacja statusLogs.changedBy jest eager-loadowana w kontrolerach: IndexReservationController, ShowReservationController, FindReservationController, CancelReservationSlotController.

Lokalizacja statusów

Przetłumaczenia kodów statusów na nazwy użytkownika są realizowane przez Support\Action\Util\StatusLangModelHelper. Obsługiwane modele: - ReservationReservationEnumService::getReservationStatuses() - ComplaintComplaintEnumService::getComplaintStatuses() - Inne — kod statusu zwracany bez tłumaczenia


9. Konfiguracja

Domena StatusLog nie posiada dedykowanych parametrów konfiguracyjnych w tabeli configurations.

Jedyne konfigurowalne aspekty:

Aspekt Wartość Zmiana
Limit per_page max 50 Zdefiniowany w PaginationDefaultRequestRules (brak specjalnego przypadku dla status-logs.index)
Sortowanie domyślne created_at DESC Zakodowane w ReturnControllerActionResponseStructure

Dodawanie śledzenia statusów do nowego modelu

Aby dodać automatyczne śledzenie statusów do nowego modelu:

Krok 1: Upewnij się, że model ma pole status w fillable i casts.

Krok 2: Dodaj atrybut PHP na klasie modelu:

use Domain\StatusLog\Observer\StatusLogObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy(StatusLogObserver::class)]
class MyModel extends Model
{
    public $fillable = ['status', ...];

    protected $casts = ['status' => 'string', ...];
}

Krok 3 (opcjonalnie): Dodaj tłumaczenia statusów w Support\Action\Util\StatusLangModelHelper:

return match ($modelClass) {
    Reservation::class => ReservationEnumService::getReservationStatuses()[$status] ?? $status,
    Complaint::class   => ComplaintEnumService::getComplaintStatuses()[$status] ?? $status,
    MyModel::class     => MyEnumService::getStatuses()[$status] ?? $status, // nowe
    default            => $status,
};

Krok 4 (opcjonalnie): Dodaj relację w nowym modelu:

public function statusLogs(): MorphMany
{
    return $this->morphMany(StatusLog::class, 'loggable');
}

10. Znane ograniczenia i TODO

Ograniczenia

  1. Brak filtru po dacie — endpoint nie udostępnia filtrów filter[created_at_from] / filter[created_at_to]. Możliwe tylko sortowanie po created_at.

  2. Brak obsługi note przez API — pole note nie może być wypełnione przez żaden endpoint API. Observer nie wypełnia go automatycznie. Jedyna droga to bezpośredni zapis do bazy lub przyszły endpoint.

  3. Tłumaczenia tylko dla Reservation i Complaint — dla LegalDocument i ewentualnych przyszłych modeli old_status_lang / new_status_lang zwróci surowy kod statusu (o ile model w ogóle ma pole status).

  4. Kaskadowe usuwanie użytkownika — usunięcie użytkownika usuwa powiązane wpisy w status_logs. Może to naruszać wymagania audit trail w środowiskach wymagających nienaruszalności historii.

  5. LegalDocument nie śledzi statusów w praktyce — model posiada atrybut ObservedBy(StatusLogObserver), ale nie ma pola status — observer nie zapisuje żadnych wpisów.

  6. Brak endpointu no-auth — historia statusów niedostępna dla niezalogowanych użytkowników, nawet dla encji publicznych.

Możliwe rozszerzenia

  • Filtry po zakresie dat dla zapytań historycznych
  • Endpointy do przeglądania historii konkretnej encji bez konieczności filtrowania po loggable_id
  • Wypełnianie pola note przy automatycznych zmianach statusów (np. w jobach)
  • Rozszerzenie StatusLangModelHelper o kolejne modele
  • Osobny endpoint lub raport SLA oparty na danych z status_logs