# Globális állapotgép — a `Ticket` életciklusa

**Dokumentum-típus:** állapotgép-referencia (a domain-modell kiegészítője)
**Státusz:** élő — `Ticket` állapotgép kész; `ContentStatus` blokk hozzáadva
**Verzió:** v0.2
**Dátum:** 2026.05.20
**Cél olvasó:** senior fejlesztő + Claude Code
**Épít:** `00_domain_model.md` (`TicketStatus`, `ContentStatus`,
`ActivityEventType`), `10_triage_flow_v1.md` (a `Ticket` státusz-modell
funkcionális forrása), `60_tartalom_v2.md` (a tartalmi életciklus
funkcionális forrása), `20_duplikacio_v1.md`, `05_jogosultsagok_v2.md`,
`99_donesnaplo.md` (Ü-3, Ü-4, SD-22, SD-70)

> **Mire való ez a fájl.** A `00_domain_model.md` rögzítette a `Ticket.status`
> öt értékét (`TicketStatus` enum) és a `ContentStatus` három értékét, de a
> *megengedett átmeneteket* tudatosan nem — azok ide tartoznak. Ez a fájl a
> `Ticket` életciklusának és a tartalmi entitások (`News`/`Event`/`CityInfo`)
> életciklusának **formális állapotgépe**: melyik állapotból melyikbe lehet
> lépni, milyen kiváltó akcióval, milyen jogosultsággal, milyen
> előfeltétellel, milyen mellékhatással. A vonatkozó feature-specek és a
> Claude Code ebből dolgozik, amikor a státusz-logikát implementálja.
>
> **Mi NEM ez.** Nem a `Ticket`/tartalmi entitások mezőlistája (az a
> `00_domain_model.md`). Nem API-szerződés (a végpontok a feature-specekben).
> Nem a triage UX-folyamata (az a `10_triage_flow_v1.md`). Ez a fájl
> **kizárólag az állapot-átmeneteket** formalizálja.
>
> **A `TenantStatus` NEM tartozik ide.** A `Tenant` életciklusa
> (`Setup`/`Active`/`Suspended`/`Archived`) Urbino-admin hatáskör; külön
> dokumentum tárgya lesz. Itt a bejelentés-állapotgép (1-8.) és a tartalmi
> állapotgép (9.) áll.

---

## 1. Az állapotok

A `Ticket` öt állapota (`TicketStatus` enum, `00_domain_model.md` 1.2.3,
K-029). Az állapot **a munka állapotát** írja le.

| # | Állapot (`enum`) | Felületi név | Jelleg | Jelentés |
|---|---|---|---|---|
| 1 | `New` | Új | kezdő | Beérkezett, triage még nem történt |
| 2 | `Assigned` | Kiosztva | köztes | Triage kész, felelőshöz rendelve, terepi munka még nem indult |
| 3 | `InProgress` | Folyamatban | köztes | A terepi munka elkezdődött |
| 4 | `Resolved` | Lezárt | **végállapot** | A munka elkészült, dokumentálva — a fő ág vége |
| 5 | `Rejected` | Elutasítva | **végállapot** | Nem valós / nem hatáskör / duplikáció — a mellékág vége |

**Kezdőállapot.** A `New` — kivéve a manuális nyitást azonnali triage-dzsel
(`10` 4.3), amely közvetlenül `Assigned`-ben jön létre (lásd 4.1).

**Végállapotok.** A `Resolved` és a `Rejected` egyaránt végállapot, de **nem
azonos jelleggel**: a `Resolved`-ből van *korrekciós* visszaút (`Reopen`,
lásd 3.4), a `Rejected`-ből **nincs** (Ü-4 — az `Elutasítva` kivétel nélkül
végállapot). A `Resolved` tehát "puha" végállapot (korrigálható), a `Rejected`
"kemény".

---

## 2. Az állapotgép — diagram

```mermaid
stateDiagram-v2
    [*] --> New : bejelentés létrejön (CitizenApp)
    [*] --> Assigned : manuális nyitás azonnali triage-dzsel (Manual)

    New --> Assigned : Triage kész — Kiosztás
    New --> Rejected : Elutasítás

    Assigned --> InProgress : Munka megkezdése
    Assigned --> Assigned : Felelős-váltás (nem státuszváltás)
    Assigned --> Rejected : Elutasítás

    InProgress --> Resolved : Lezárás
    InProgress --> Assigned : Visszaállítás Kiosztva állapotba
    InProgress --> Assigned : Felelős-váltás
    InProgress --> Rejected : Elutasítás

    Resolved --> InProgress : Korrekciós visszanyitás

    Rejected --> [*]
    Resolved --> [*]
```

> **A diagram olvasása.** A `New`-ból két kezdőnyíl is indul — a `[*]` (kezdet)
> kétféleképp léphet be: `New`-ba a polgári bejelentés, `Assigned`-be a
> manuális nyitás (4.1). Az `Assigned --> Assigned` önhurok a **felelős-váltás**
> — ez formailag nem státuszváltás (az állapot marad `Assigned`), de az
> állapotgépben jelöljük, mert szabályozott akció (3.5). A `Rejected`-ből
> **nem** indul nyíl — kemény végállapot (Ü-4).

---

## 3. Az átmenetek — formális tábla

Minden sor egy megengedett átmenet. Az oszlopok:

- **Átmenet** — `Honnan → Hová`.
- **Kiváltó akció** — a felhasználói/rendszer-művelet, ami az átmenetet
  indítja. Ez lesz tipikusan egy nem-standard API-végpont a `Ticket`
  feature-specben.
- **Jogosultság** — mely szerepkör végezheti (`05_jogosultsagok_v2.md`).
- **Előfeltétel** — aminek teljesülnie kell, különben az átmenet `409`/`422`
  hibával elutasul (`01_kozos_mintak` 6.3).
- **Mellékhatás** — `ActivityLog`-bejegyzés, mező-írás, értesítés.

### 3.1 `New → Assigned` — a triage befejezése

| | |
|---|---|
| **Kiváltó akció** | "Triage kész — Kiosztás" gomb (Ü-3) |
| **Jogosultság** | `dispatcher`, `manager` |
| **Előfeltétel** | A négy triage-mező közül a **kötelezők** kitöltve: `categoryId` **kötelező**; `priority` mindig kitöltött (default `Normal`); a felelős (`assignedGroupId` **vagy** `assignedUserId`, pontosan az egyik) **kötelező**. A `dueDate` opcionális. Lásd 5.1 |
| **Mellékhatás** | `status := Assigned`; `assignedAt := now` (SD-14); `ActivityLog`: `Assigned` esemény; értesítés a felelősnek és a bejelentőnek (lásd 6.) |

> **Az Ü-3 modell itt érvényesül.** A négy triage-mező inline-szerkesztéssel,
> optimistic update-tel töltődik (K-026) — ez **nem** vált státuszt. A
> státuszváltást **csak** a "Triage kész — Kiosztás" gomb hozza, és ez a gomb
> validál (az előfeltétel itt ellenőrződik). Részletes UX: `10_triage_flow`
> 3.4, 8.4.

### 3.2 `New → Rejected` — elutasítás triage nélkül/helyett

| | |
|---|---|
| **Kiváltó akció** | "Elutasítás" akció |
| **Jogosultság** | `dispatcher`, `manager` |
| **Előfeltétel** | `rejectionReasonCode` kötelezően megadva (`00_domain_model.md` 1.2.3, TD-5). Ha a kód `Duplicate`, lásd 3.6 — a duplikáció-összevonás külön út |
| **Mellékhatás** | `status := Rejected`; `rejectionReasonCode` (és opcionálisan `rejectionReasonText`) beírva; `ActivityLog`: `Rejected` esemény; értesítés a bejelentőnek (6.) |

### 3.3 `Assigned → InProgress` — a terepi munka indulása

| | |
|---|---|
| **Kiváltó akció** | "Munka megkezdése" akció |
| **Jogosultság** | `dispatcher`, `manager`, `field_worker` — a terepi dolgozó is jelezheti, hogy elkezdte (`05_jogosultsagok` 2.2) |
| **Előfeltétel** | Nincs külön előfeltétel a már `Assigned`-ben lévő ügyön túl |
| **Mellékhatás** | `status := InProgress`; `ActivityLog`: `StatusChanged` esemény; értesítés a bejelentőnek (6.) |

### 3.4 `InProgress → Resolved` — a lezárás

| | |
|---|---|
| **Kiváltó akció** | "Lezárás" akció |
| **Jogosultság** | `dispatcher`, `manager`, `field_worker` |
| **Előfeltétel** | **Lezárás-dokumentáció kötelező:** legalább egy `ResolutionPhoto` típusú `Attachment` **vagy** kitöltött `resolutionNote` (`10` 4.2, TD-7). Lásd 5.2 |
| **Mellékhatás** | `status := Resolved`; `resolvedAt := now` (SD-14); `ActivityLog`: `StatusChanged` esemény; értesítés a bejelentőnek (6.) — ez a polgári "Megoldódott" badge kiváltója |

### 3.5 `Resolved → InProgress` — korrekciós visszanyitás

| | |
|---|---|
| **Kiváltó akció** | "Visszanyitás" akció |
| **Jogosultság** | `dispatcher`, `manager` — a `field_worker` **nem** nyithat vissza (`10` 4.5) |
| **Előfeltétel** | `reopenReason` szöveg kötelezően megadva (a visszanyitás aktusakor; a `Ticket`-en nincs mező, az `ActivityLog`-ban él — TD-6) |
| **Mellékhatás** | `status := InProgress`; `resolvedAt := null` (a korábbi lezárás érvényét veszti); `ActivityLog`: `Reopened` esemény, a `note`-ban a visszanyitás indoka; értesítés a felelősnek és a bejelentőnek (6.) |

> **A `resolvedAt` visszaállítása.** A visszanyitás a `resolvedAt`-ot
> `null`-ra állítja — különben a riport (átlagos lezárási idő, `40` 3.5)
> egy "lezárt, majd újranyitott" ügyet tévesen késznek számolna. Ha az ügy
> később újra `Resolved`-be kerül, a `resolvedAt` az **új** lezárás idejét
> kapja. Következmény: az `assignedAt`/`resolvedAt` mindig a *legutóbbi*
> releváns átmenetet tükrözi; a teljes történet az `ActivityLog`-ban van.

### 3.6 `Assigned → InProgress` visszaút és a felelős-váltás

A `10_triage_flow` két további, **nem fő-ági** átmenetet/akciót ír le:

**(a) `InProgress → Assigned` — visszaállítás.** Ha egy ügy tévesen került
`InProgress`-be (pl. a terepi dolgozó elkezdte, majd kiderült, hogy mégsem ő
a felelős), visszaállítható `Assigned`-be.

| | |
|---|---|
| **Kiváltó akció** | "Visszaállítás Kiosztva állapotba" akció |
| **Jogosultság** | `dispatcher`, `manager` |
| **Előfeltétel** | — |
| **Mellékhatás** | `status := Assigned`; `ActivityLog`: `StatusChanged` esemény |

**(b) Felelős-váltás — `Reassigned`.** A felelős (`assignedGroupId` /
`assignedUserId`) megváltoztatása. **Ez nem státuszváltás** — az állapot
`Assigned` vagy `InProgress` marad —, de szabályozott akció.

| | |
|---|---|
| **Kiváltó akció** | "Felelős módosítása" akció |
| **Jogosultság** | `dispatcher`, `manager` |
| **Előfeltétel** | Az ügy `Assigned` vagy `InProgress` állapotban; az új felelős `assignedGroupId` **vagy** `assignedUserId` (pontosan az egyik) megadva, és az `isActive` rá |
| **Mellékhatás** | `assignedGroupId`/`assignedUserId` átírva (az egyik beállítva, a másik `null`-ra); `ActivityLog`: `Reassigned` esemény, `fromValue`/`toValue` a régi/új felelőssel; értesítés az új felelősnek (6.) |

### 3.7 A duplikáció-összevonás mint `→ Rejected` átmenet

Az összevonás (`20_duplikacio_v1.md` 4., K-034) **a duplikátum** szempontjából
egy `→ Rejected` átmenet, `Duplicate` indokkal. Az **eredeti** ügy állapota
**nem változik**.

| | |
|---|---|
| **Kiváltó akció** | "Összevonás" / "Duplikációként megjelölés" akció — a `20` 4. folyamata |
| **Jogosultság** | `dispatcher`, `manager` |
| **Előfeltétel** | A megjelölt eredeti ügy létezik és nem maga a duplikátum |
| **Mellékhatás** (duplikátum) | `status := Rejected`; `rejectionReasonCode := Duplicate`; `rejectionReasonText` automatikusan generált ("Duplikátuma a(z) `ALM-X`-nek"); `originalTicketId := <eredeti id>`; `ActivityLog`: `Merged` esemény |
| **Mellékhatás** (eredeti) | Állapot **változatlan**; `ActivityLog`: `Merged` esemény ("1 duplikátum összevonva: `ALM-Y`") |

> **A duplikátum bármely nem-végállapotból összevonható.** A duplikáció
> kiderülhet `New`, `Assigned` vagy `InProgress` állapotban is — mindegyikből
> megengedett a `→ Rejected (Duplicate)` átmenet. Az eredeti/duplikátum
> kiválasztása alapból a régebbi beérkezésű marad eredetinek (K-034).

> **A téves összevonás korrekciója (Ü-4).** Ha az összevonás téves volt, a
> duplikátum-ként `Rejected`-be került ügyet a `Resolved → InProgress`
> visszanyitás **nem** éri el (a `Rejected` kemény végállapot). Az Ü-4 döntés:
> a pilotra a téves összevonás **kézi korrekcióval** oldható — a diszpécser
> egy **új** bejelentést nyit manuálisan a téves duplikátum adataival, vagy a
> dedikált "összevonás visszavonása" funkció a 6-12. hó. A `Rejected`
> állapotgép-szinten kivétel nélkül végállapot; a korrekció nem
> állapot-átmenet, hanem új ügy. *(Lásd a 8. nyitott kérdést.)*

---

## 4. Belépési pontok — hogyan jön létre egy `Ticket`

### 4.1 A két létrejövési út

| Út | Eredet (`origin`) | Kezdő `status` | Forrás |
|---|---|---|---|
| Polgári bejelentés | `CitizenApp` | `New` | A polgári mobilapp új bejelentés flow-ja |
| Manuális nyitás | `Manual` | `New` **vagy** `Assigned` | `10` 4.3 — a diszpécser telefonos/e-mailes ügyet rögzít |

> **A manuális nyitás kétféle kezdőállapota.** A diszpécser a manuális
> nyitáskor **azonnal triage-elhet** — ha a négy döntést rögtön meghozza, az
> ügy egyenest `Assigned`-ben jön létre (a `[*] → Assigned` kezdőnyíl). Ha
> csak rögzíti az ügyet triage nélkül, `New`-ban jön létre, és a triage
> később történik. A polgári bejelentés **mindig** `New` — a polgár nem
> triage-el.

Mindkét úton a létrejövés `ActivityLog`: `Created` esemény. A `New`-ban
létrejött ügy a triage-várólistára kerül (`10` 2.).

### 4.2 Amit az állapotgép nem fed a létrejövésnél

A bejelentés *létrehozásának* validációja (kötelező `title`, a polgári
flow-ban kötelező fotó, a koordináta-igény) **nem** állapotgép-kérdés — az a
`Ticket` feature-spec és a polgári adatigény-spec dolga. Az állapotgép a
*már létező* `Ticket` állapot-mozgását szabályozza.

---

## 5. Az átmenet-előfeltételek részletei

Az állapotgép-szempontból kritikus két előfeltétel — ezeket a `Ticket`
feature-spec FluentValidation-szabályként implementálja, de az állapotgép
köti ki, *mely átmenetnél* érvényesek.

### 5.1 A `New → Assigned` előfeltétele — a triage teljessége

A "Triage kész — Kiosztás" gomb (3.1) **csak akkor** viheti az ügyet
`Assigned`-be, ha:

- `categoryId` kitöltve (bármely szintű `Category` — SD-18);
- `priority` kitöltve (a default `Normal` miatt ez gyakorlatilag mindig
  teljesül);
- **pontosan egy** felelős-mező kitöltve: `assignedGroupId` **xor**
  `assignedUserId` (TD-4);
- a felelősként megadott `Group`/`TenantUser` `isActive`.

A `dueDate` **nem** előfeltétel — opcionális marad. Ha bármely kötelező mező
hiányzik, a gomb-akció `422`-vel elutasul, és a hiányzó mező(k) `fieldErrors`-ban
térnek vissza (`01_kozos_mintak` 6.3).

### 5.2 Az `InProgress → Resolved` előfeltétele — a lezárás-dokumentáció

A "Lezárás" akció (3.4) **csak akkor** viheti az ügyet `Resolved`-be, ha:

- létezik **legalább egy** `ResolutionPhoto` típusú `Attachment` a ticketen,
  **vagy**
- a `resolutionNote` kitöltött (nem üres).

A "fotó **vagy** szöveg" — legalább az egyik. Ha egyik sincs, a "Lezárás"
akció `422`-vel elutasul.

> **Miért átmenet-feltétel, és nem mező-kötelezőség.** A `resolutionNote` és
> a `ResolutionPhoto` a `Ticket` életének nagy részében jogosan üres (egy
> `New` vagy `InProgress` ügynek nincs lezárás-dokumentációja). Ha
> mező-szinten kötelező lenne, az ügy létre sem jöhetne. A kötelezőség
> **csak a `→ Resolved` átmenet pillanatában** áll fenn — ezért
> állapotgép-feltétel. Ugyanez a logika a `New → Assigned` triage-mezőknél
> (5.1).

---

## 6. Mellékhatások — értesítések

Minden státuszváltás **értesítést** válthat ki. A pontos értesítés-mechanizmus
(push, e-mail, in-app) a polgári adatigény-spec és a `Ticket` feature-spec
dolga; az állapotgép azt rögzíti, **mely átmenet kit értesít**.

| Átmenet | Bejelentő (polgár) | Felelős | Megjegyzés |
|---|---|---|---|
| `→ Assigned` (triage) | ✓ "befogadtuk" | ✓ "kaptál egy ügyet" | `10` 3.4 |
| `→ Rejected` | ✓ indoklással | — | A polgár lássa, miért utasították el (`10` 5.) |
| `→ InProgress` | ✓ "elkezdtük" | — | — |
| `→ Resolved` | ✓ "megoldódott" | — | A polgári "Megoldódott" badge (`screen_bejelentes_lista`) |
| `Resolved → InProgress` (visszanyitás) | ✓ | ✓ | Mindkét fél tudja, hogy újra nyitott |
| Felelős-váltás (`Reassigned`) | — | ✓ az **új** felelős | A polgárt a felelős személye nem érdekli |

> **Az értesítés nem blokkolja az átmenetet.** Ha az értesítés-küldés
> meghiúsul (pl. a push-szolgáltatás elérhetetlen), az **nem** görgeti vissza
> a státuszváltást — az állapot már megváltozott, az `ActivityLog` rögzítette.
> Az értesítés "best effort" mellékhatás. A pontos hibatűrés a `Ticket`
> feature-spec dolga; az állapotgép annyit köt ki, hogy az átmenet
> *érvényessége* nem függ az értesítés sikerétől.

---

## 7. Teljes átmenet-mátrix — gépiesen ellenőrizhető

A `Ticket` feature-spec és a Claude Code számára a teljes "miből mibe"
mátrix. A cellák: ✓ megengedett (a kiváltó akcióval), ✗ tiltott.

| ↓ honnan / hová → | `New` | `Assigned` | `InProgress` | `Resolved` | `Rejected` |
|---|---|---|---|---|---|
| **`New`** | — | ✓ triage | ✗ | ✗ | ✓ elutasítás |
| **`Assigned`** | ✗ | ✓ felelős-váltás¹ | ✓ munka megkezdése | ✗ | ✓ elutasítás |
| **`InProgress`** | ✗ | ✓ visszaállítás / felelős-váltás¹ | — | ✓ lezárás | ✓ elutasítás |
| **`Resolved`** | ✗ | ✗ | ✓ visszanyitás | — | ✗ |
| **`Rejected`** | ✗ | ✗ | ✗ | ✗ | — |

¹ A felelős-váltás nem státuszváltás (az állapot marad) — a mátrixban az
"önmagába" cella jelzi, hogy `Assigned`/`InProgress` állapotban a felelős-váltás
megengedett akció.

**Olvasat a Claude Code-nak.** Minden ✗ cella egy **tiltott átmenet** — ha egy
API-hívás ilyet kísérel, a válasz `409 Conflict` (`01_kozos_mintak` 6.3). A ✓
cellák a megengedett átmenetek, a 3. szakasz részletezi a feltételeiket. A
`Rejected` sora teljesen ✗ — kemény végállapot (Ü-4).

---

## 8. Nyitott kérdések

| # | Kérdés | Hatáskör |
|---|---|---|
| **NY-Á1** | A "Munka megkezdése" (`Assigned → InProgress`) — a pilotra **kézi** akció (a terepi dolgozó vagy a diszpécser kattint). Lehet-e későbbi igény, hogy a terepi mobilapp automatikusan váltson `InProgress`-be (pl. helyszínre érkezéskor)? | A terepi mobilapp későbbi tervezése — hatókörön kívül; jelzés szintjén |
| **NY-Á2** | A téves duplikáció-összevonás korrekciója (Ü-4) — a pilotra "új ügy nyitása" a megoldás. A dedikált "összevonás visszavonása" mint **valódi** állapotgép-átmenet (`Rejected → előző állapot`) a 6-12. hó kérdése. Ha bekerül, az állapotgép bővül egy feltételes átmenettel | 6-12. hó — a `20_duplikacio` jövő-iterációja |
| **NY-Á3** | Van-e a pilotnak igénye **tömeges státuszváltásra** (pl. több `New` ügy egyszerre elutasítása)? A `00_architektura` bulk-műveleteket említ a lista-oldalakon. Ha igen, a bulk-átmenet ugyanezt az állapotgépet alkalmazza ügyenként — de az UX és az API a `Ticket` feature-spec dolga | `Ticket` feature-spec — az állapotgép logikája változatlan, csak a kiváltás csoportos |

---

## 9. A `ContentStatus` állapotgép — a tartalmi entitások életciklusa

Ez a szakasz a polgári app "puha" tartalmának (`News`, `Event`, `CityInfo`)
életciklusát formalizálja. A három entitás **azonos állapotgépet** használ
a `ContentStatus` enumon (`00_domain_model.md` 4.1, SD-22). Forrás:
`60_tartalom_v2.md` 3.5 (funkcionális) + `60_tartalom.md` v1.0 2.9 és 3.
(specifikációs), SD-70.

### 9.1 Az állapotok

| # | Állapot (`enum`) | Felületi név | Jelleg | Jelentés |
|---|---|---|---|---|
| 1 | `Draft` | Vázlat | kezdő | A tartalomkezelő dolgozik rajta; a polgári appban **nem** látszik |
| 2 | `Published` | Publikált | aktív | Élesben — a polgári appban látszik |
| 3 | `Archived` | Archivált | passzív | Volt publikálva, de már nem aktuális; a polgári appban nem látszik, de megmarad |

**Kezdőállapot.** Minden új `News`/`Event`/`CityInfo` `Draft`-ban születik.
A `POST /v1/news` (és társai) szerver-oldalon hardkódoltan `Draft`-ra
állítja a `contentStatus`-t; a kérésben szereplő `contentStatus` mező
`400 field_not_editable`-lel zár.

**Végállapot nincs.** A három állapot között minden irány bejárható (a
tiltott átmenetek kivételével, lásd 9.2) — a tartalmi életciklus
**nem-monoton**, eltérően a `Ticket`-étől.

### 9.2 Az állapotgép — diagram

```mermaid
stateDiagram-v2
    [*] --> Draft : létrehozás (POST)
    Draft --> Published : T1 — Publikálás
    Published --> Draft : T2 — Visszavonás vázlatba
    Published --> Archived : T3 — Archiválás
    Archived --> Published : T4 — Újrapublikálás
    
    note right of Draft
      publishedAt = null
    end note
    note right of Published
      publishedAt = now()
      (jelenlegi publikálási idő)
    end note
    note right of Archived
      publishedAt változatlan
      (legutóbbi publikálás emléke)
    end note
```

> **A diagram olvasása.** Négy megengedett átmenet (T1-T4); két tiltott
> átmenet (`Draft → Archived`, `Archived → Draft`) — nincsenek nyilak,
> mert a hívás `409 invalid_transition`-szel zár. A `Draft` és az
> `Archived` szemantikailag tisztán elválasztandó (SD-22): a `Draft` a
> "soha nem publikált", az `Archived` a "volt publikálva, már nem
> aktuális" — ezért nem lehet egyikből a másikba közvetlenül átmenni.

### 9.3 Az átmenetek — formális tábla

| # | Átmenet | Kiváltó akció | Jogosultság | Előfeltétel | Mellékhatás |
|---|---|---|---|---|---|
| T1 | `Draft → Published` | "Publikálás" gomb | `content_manager`, `manager` | A mezőszintű validáció (cím, body, stb.) sikeres | `contentStatus := Published`; `publishedAt := now()` (szerver UTC); `updatedAt`, `updatedBy` frissül |
| T2 | `Published → Draft` | "Visszavonás vázlatba" akció | ua. | — | `contentStatus := Draft`; `publishedAt := null` (a jelenlegi publikálási idő törlődik); `updatedAt`, `updatedBy` frissül |
| T3 | `Published → Archived` | "Archiválás" akció | ua. | — | `contentStatus := Archived`; `publishedAt` **változatlan** (megőrzi a legutóbbi publikálás idejét); `updatedAt`, `updatedBy` frissül |
| T4 | `Archived → Published` | "Újrapublikálás" akció | ua. | A mezőszintű validáció sikeres | `contentStatus := Published`; `publishedAt := now()` — **új publikálási időpont**, nem a régi visszahozása; `updatedAt`, `updatedBy` frissül |

### 9.4 Tiltott átmenetek

**`Draft → Archived`:** nincs ilyen út. Indok: archiválni csak azt lehet,
ami valaha publikálva volt. Egy soha-nem-publikált vázlat törlése a
`DELETE` (lásd 9.6), nem archiválás. A `POST .../transition { targetStatus:
"Archived" }` egy `Draft` rekordon `409 invalid_transition`-szel zár,
`details.from = "Draft"`, `details.to = "Archived"`.

**`Archived → Draft`:** szintén nincs. Ha egy archív tartalmat újra
szerkeszteni-és-publikálni akarsz, az út: `Archived → Published` (T4),
majd ha kell, `Published → Draft` (T2). Indok: a `Draft` szemantikája a
"soha nem publikált" — az archiváltból visszadrafftolás összemosná ezt.

**Önmagába-átmenet (`Draft → Draft`, `Published → Published`, stb.):**
nincs ilyen út — a négy átmenet egyike sem. Ugyanúgy `409
invalid_transition`-szel zár.

### 9.5 A `transition`-végpont szerződése (összefoglaló)

A három tartalmi entitás mindegyikén dedikált `POST
/v1/{entity}/{id}/transition` végpont van (T-9, T-18, T-25 a
`60_tartalom`-spec 3.1.1-ben). A request body:

```json
{
  "targetStatus": "Published",   // enum: Draft / Published / Archived
  "expectedUpdatedAt": "..."     // optimista konkurrencia token (SD-32)
}
```

A workflow:
1. Jelenlegi `contentStatus` lekérve.
2. A `(currentStatus, targetStatus)` páros ellenőrzése a 9.3 átmenet-tábla
   ellen.
3. Ha nem T1-T4 között → `409 invalid_transition`.
4. `expectedUpdatedAt`-ellenőrzés; ha eltér → `409 stale`.
5. Mellékhatások a 9.3 szerint, egy tranzakcióban.

**Validáció kérdéses pontjai:**
- A mezőszintű validáció (cím, body, stb.) érvényessége elsősorban a
  `PUT`-on érvényesül, nem a `transition`-on; egy érvénytelen `body`-val
  publikálni nem lehet, de az elutasítás a `PUT`-on történik (az
  állapot-átmenet csak a státuszt vált). A `60_tartalom`-spec 3.2.5/3.3.4
  ezt részletezi.

### 9.6 A `DELETE` és az állapot — guard

A `ContentStatus` állapotgép a *megjelenítési* életciklust modellezi. A
**fizikai törlés** ettől független:

| Állapot | `DELETE` engedélyezett? |
|---|---|
| `Draft` | **igen** — egy vázlat törlése értelmes (rontott kezdés, megszakított munka) |
| `Published` | **nem** — `409 cannot_delete_published`, `details.currentStatus = "Published"` |
| `Archived` | **igen** — az archív lerakodás után törölhető |

A standard `BaseController` `DELETE` végpont egy `BeforeDeleteAsync`-guard
ellenőrzi az állapotot. Indok a `Published`-tiltás mögött: a publikált
tartalmat *láthatták* polgárok; közvetlen törlés audit-szempontból
problémás. A megfelelő út: `Published → Archived → DELETE`.

### 9.7 A polgári oldal "látja" mátrix

| Állapot | A polgári app látja? |
|---|---|
| `Draft` | ✗ — nem listázza, nem kéri le |
| `Published` | ✓ — látható |
| `Archived` | ✗ — nem listázza, nem kéri le |

A polgári olvasói végpont (a polgári adatigény-spec hatásköre) `WHERE
contentStatus = 'Published'` szűrőt alkalmaz. Ez nem új szabály, csak
rögzítve. A polgári `News`/`Event`/`CityInfo`-DTO **nem tartalmazza** a
`contentStatus` mezőt (a polgár csak publikáltakat lát; az állapot
értelmetlen vetület).

---

## 10. Hivatkozott dokumentumok

- `00_domain_model.md` — a `TicketStatus`, `RejectionReason`,
  `ActivityEventType`, `ContentStatus` enumok; az `assignedAt`/`resolvedAt`/
  `publishedAt` mezők
- `10_triage_flow_v1.md` — a `Ticket`-státusz-modell és az átmenetek
  funkcionális forrása; a triage-folyamat
- `60_tartalom_v2.md` — a tartalmi állapotgép funkcionális forrása (3.5)
- `60_tartalom.md` v1.0 — a tartalmi állapotgép specifikációs forrása
  (2.9, 3., SD-70)
- `20_duplikacio_v1.md` — a duplikáció-összevonás (3.7)
- `05_jogosultsagok_v2.md` — az átmenetek szerepkör-jogosultsága
- `99_donesnaplo.md` — Ü-3 (a "Triage kész" gomb modellje), Ü-4 (a
  `Rejected` kemény végállapot), TD-5, TD-6, TD-7, SD-22, SD-70
- `01_kozos_mintak.md` — a hibakód-keret (6.3): `409` tiltott átmenetre,
  `422` üzleti szabály sérülésre, `409 stale` optimistic-concurrency
  ütközésre

---

## Verziónapló

- **v0.1 (2026.05.18)** — Első verzió. A `Ticket` öt állapota; az állapotgép
  diagramja; a tíz átmenet formális táblája (kiváltó akció, jogosultság,
  előfeltétel, mellékhatás); a két belépési pont; az átmenet-előfeltételek
  részletei (triage-teljesség, lezárás-dokumentáció); az értesítés-mátrix; a
  teljes "miből mibe" átmenet-mátrix; három nyitott kérdés (NY-Á1 — NY-Á3).
- **v0.2 (2026.05.20)** — A `60_tartalom` feature-spec (v1.0) átvezetése. **Új
  9. szakasz: a `ContentStatus` állapotgép** a három tartalmi entitásra
  (`News`/`Event`/`CityInfo`). Három állapot (`Draft`/`Published`/
  `Archived`), négy megengedett átmenet (T1 Publikálás, T2 Visszavonás
  vázlatba, T3 Archiválás, T4 Újrapublikálás), két tiltott átmenet
  (`Draft → Archived`, `Archived → Draft`); a `transition`-végpont
  szerződésének összefoglalója; a `DELETE` állapot-érzékenységének
  formális rögzítése (`Draft` és `Archived` törölhető, `Published` guarddal
  blokkolt); a polgári oldal "látja" mátrix (csak `Published`). A bevezető
  megjegyzés frissült: a `ContentStatus` immár ide tartozik (a `TenantStatus`
  még mindig külön Urbino-admin hatáskör). Forrás: `60_tartalom_v2.md` 3.5
  (funkcionális), `60_tartalom.md` v1.0 2.9 + 3. (specifikációs), SD-70.
