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:
LegalDocumentposiada atrybut#[ObservedBy(StatusLogObserver::class)], jednak model nie ma polastatus— observer nie zapisze wpisu przy tworzeniu (warunekif ($model->status)) ani aktualizacji (brakwasChanged('status')). Wpis jest tworzony jedynie gdy model rzeczywiście posiada polestatusi 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-incrementchanged_by— klucz obcy do tabeliuserszonDelete('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'yUserAccessesGlobalScopeiIsActiveGlobalScope - 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():
- Konstruuje listę filtrów:
id(exact),loggable_id(exact),loggable_type(exact) + pola fillable modelu - Używa
Spatie\QueryBuilder\QueryBuilderdo budowania zapytania z filtrami i sortowaniem - Domyślne sortowanie:
created_at DESC(gdy brak sortu) - Eager-loading relacji:
['changedBy'] - Paginacja przez
fastPaginate($validated['per_page']) - Każdy wpis sprawdzany przez
StatusLogPolicy::view()(policy per-item)
4.2 Reguły biznesowe¶
- Wpis przy tworzeniu: Observer
created()zapisuje wpis tylko gdy$model->statusjest niepuste.old_status=null. - Wpis przy aktualizacji: Observer
updated()zapisuje wpis tylko gdy$model->wasChanged('status')zwracatrue.old_status=$model->getOriginal('status'). - Zmiana bez logowania: Gdy
auth()->check()=false, polechanged_byzapisywane jakonull— operacje systemowe (komendy CRON, jobs) są w pełni obsługiwane. - Pole
note: Nie jest wypełniane automatycznie przez observer. Wartośćnullw zdecydowanej większości wpisów. Może być wypełnione przy ręcznym tworzeniu. - Brak tworzenia/edycji przez API: PolicyI
create()iupdate()zwracająfalse— nie ma żadnego endpointu do tworzenia ani edycji wpisów. Wpisy są tworzone wyłącznie przez observer. - Niemodyfikowalność wpisów:
can_be_editzawszefalse— historia statusów jest niezmienialnym dziennikiem audytowym. - Kaskadowe usuwanie: Klucz obcy
changed_byzonDelete('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:
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: - Reservation — ReservationEnumService::getReservationStatuses() - Complaint — ComplaintEnumService::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:
10. Znane ograniczenia i TODO¶
Ograniczenia¶
-
Brak filtru po dacie — endpoint nie udostępnia filtrów
filter[created_at_from]/filter[created_at_to]. Możliwe tylko sortowanie pocreated_at. -
Brak obsługi
noteprzez API — polenotenie 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. -
Tłumaczenia tylko dla Reservation i Complaint — dla
LegalDocumenti ewentualnych przyszłych modeliold_status_lang/new_status_langzwróci surowy kod statusu (o ile model w ogóle ma polestatus). -
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. -
LegalDocumentnie śledzi statusów w praktyce — model posiada atrybutObservedBy(StatusLogObserver), ale nie ma polastatus— observer nie zapisuje żadnych wpisów. -
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
noteprzy automatycznych zmianach statusów (np. w jobach) - Rozszerzenie
StatusLangModelHelpero kolejne modele - Osobny endpoint lub raport SLA oparty na danych z
status_logs