# Duplikáció-szűrés és bejelentés-összevonás

**Dokumentum-típus:** feature-specifikáció (fejlesztői, Claude Code-barát)
**Modul:** Admin felület → triage → duplikáció-szűrés
**Feature-verzió:** v1.0
**Dátum:** 2026.05.19
**Cél olvasó:** senior fejlesztő + Claude Code
**Végpontok:** `GET /v1/tickets/{id}/similar`, `POST /v1/tickets/{id}/merge`

> **Mire való ez a fájl.** A duplikáció-szűrés determinisztikus heurisztikájának
> és a bejelentés-összevonás folyamatának fejlesztői specifikációja: a
> `similar`-végpont szerződése, az összevonás-akció a tranzakciós kétoldali
> mellékhatással, a "Hasonló bejelentések" doboz tartalmi szerződése, az
> összevonás-megerősítő párbeszéd, acceptance criteria.
>
> **Mit NEM tartalmaz.** Nem ismétli a `20_duplikacio_v1.md` funkcionális
> tervét, a `02_globalis_allapotgep.md` 3.7 átmenet-tábláját, a
> `00_domain_model.md` entitás-mezőit, sem a `01_kozos_mintak.md`
> platform-mintáit — ezekre **hivatkozik**. A "Hasonló bejelentések" doboz
> *helyét* az adatlapon és a redukált duplikátum-adatlap UI-ját a
> `10_bejelentes_lista_es_adatlap.md` (SD-29) adja — ez a feature a doboz
> *tartalmi szerződését* fedi, lezárva a `10` `NY-bej-6` nyitott kérdését. A
> polgári oldali megjelenítés modulközi átadás.

---

## 0. Funkcionális alap

### 0.1 A feature-t fedő dokumentumok

| Dokumentum | Mit ad ehhez a feature-höz |
|---|---|
| `20_duplikacio_v1.md` 1.–8. | **Az elsődleges bemenet.** A heurisztika (három jel, küszöbök), a "Hasonló bejelentések" doboz viselkedése, az összevonás-folyamat (eredeti vs. duplikátum, megerősítő lépés, bejelentő-átkötés), a pilot-scope, a GPS nélküli eset |
| `02_globalis_allapotgep.md` 3.7 | Az összevonás mint formális `→ Rejected` átmenet — kiváltó akció, jogosultság, előfeltétel, kétoldali mellékhatás |
| `10_triage_flow_v1.md` 3.2, 3.5 | A duplikáció-doboz helye a triage-képernyőn; a 30 mp-anatómia |
| `05_jogosultsagok_v2.md` 2.5 | A duplikáció-akciók szerepkör-mátrixa |
| `00_domain_model.md` 1.2.3, 1.2.6, 1.4, 2.1 | A `Ticket.originalTicketId` self-FK, a `rejectionReasonCode = Duplicate` szabály (SD-40), az `ActivityLog` + `Merged` esemény, a `Category`-fa gyökér-feloldása |
| `01_kozos_mintak.md` 2.1, 5.3, 6.1–6.3 | API-konvenciók: `/v1/` route, tenant-transzparens `DbContext`, hibakód-keret, i18n "szerver kódot ad" elv |
| `10_bejelentes_lista_es_adatlap.md` v1.2 | Átfedés-elhatárolás — lásd 0.4 |
| `90_sitemap_v3.md` 2.3 | A doboz az adatlap döntéstámogatás-zónájában |

### 0.2 Érintett kanonikus döntések

- **K-032** — a duplikáció-szűrés a pilotra deterministic három-jeles heurisztika (≤50 m, azonos gyökér-kategória, ≤14 nap, **együtt**), nem ML; a rendszer javasol, a diszpécser dönt.
- **K-033** — a duplikáció-ellenőrzés **nem kötelező lépés, nem kapu**; az üres eset csendes.
- **K-034** — összevonáskor a régebbi bejelentés az eredeti; a duplikátum `Rejected` lesz "Duplikáció" indoklással és eredeti-ügy hivatkozással; a duplikátum bejelentője átkötődik az eredeti ügy értesítendői közé.
- **K-016** — szigorú pilot-scope (a konfigurálható küszöbök ezért 6–12. hó).

### 0.3 Mit döntött már el a funkcionális/alap-réteg

Röviden, hivatkozással — a specifikáció ezeket **nem nyitja újra**:

- A heurisztika tartalma — három jel és a konkrét küszöbök (`20` 2.2–2.3, K-032).
- A GPS nélküli eset — koordináta hiányában a térbeli jel nem értékelhető (`20` 2.4).
- A találati halmaz nyitott-állapotra korlátozása (`20` 3.3).
- A doboz háromállapotú viselkedése (`20` 3.1, 3.4), és hogy semmit sem blokkol (K-033).
- Az összevonás folyamata: eredeti/duplikátum szerepek, megerősítő párbeszéd (`20` 4.1–4.3, K-034).
- Az összevonás állapotgép-vetülete (`02_globalis_allapotgep.md` 3.7).
- A `rejectionReasonText` `Duplicate`-esete: üres, a hivatkozást az `originalTicketId` hordozza (`00_domain_model.md` 1.2.3, SD-40).
- A téves összevonás korrekciója: pilotra kézi újranyitás (Ü-4).
- A jogosultság: doboz-megtekintés és összevonás `dispatcher`/`manager`, terepi nem (`05` 2.5).

### 0.4 Mit tölt ki EZ a specifikáció — és az elhatárolás a `10`-től

A `10_bejelentes_lista_es_adatlap.md` v1.2 már megírta — **ez a feature NEM ismétli:**

- A doboz **helye** az adatlap jobb oszlopában és **betöltési viselkedése** mint adatlap-elem (`10` 4.3).
- A **redukált duplikátum-adatlap** (SD-29): ha `originalTicketId` kitöltött, redukált csak-olvasható nézet, duplikáció-doboz nélkül.
- A detail-DTO `originalTicketId`/`originalTicketDisplayId` mezeje (`10` 3.2.2).
- A `Merged` `ActivityLog`-esemény adatlap-megjelenítésének kerete (`10` 4.6).

Amit **ez a feature ad** (a `10` `NY-bej-6` lezárása):

- A `GET /v1/tickets/{id}/similar` teljes szerződése — a heurisztika fejlesztői modellje.
- A `POST /v1/tickets/{id}/merge` szerződése — a tranzakciós összevonás.
- A "Hasonló bejelentések" doboz **tartalmi szerződése** — komponens-vázlat, találat-sor, az összevonás-megerősítő párbeszéd.
- A duplikáció-specifikus i18n-kulcsok és acceptance criteria.

---

## 1. Cél és hatókör

### 1.1 Cél

A duplikáció-szűrés a triage-be ágyazott döntéstámogatás: a rendszer
determinisztikus heurisztikával felismeri a lehetséges duplikációt, és a
bejelentés-adatlapon megmutatja a "Hasonló bejelentések" dobozban. A
diszpécser egy kattintással összevonhat két ügyet — a duplikátum lezárul, a
bejelentője az eredeti ügy értesítendői közé kerül. Az üzleti érték a
koncepció Wow #4 első fele: a diszpécsernek nem kell külön ellenőriznie a régi
adatokat.

### 1.2 Hatókörön kívül (ennél a feature-nél)

- A doboz *helye* az adatlapon és a redukált duplikátum-adatlap UI-ja — `10` (SD-29).
- A polgári oldali duplikáció-megjelenítés — modulközi átadás (`20` 7.2, lásd 8.4).
- Az ML / kép-hasonlóság alapú felismerés — `20` 5.2, 18–24. hó.
- A konfigurálható heurisztika-küszöbök felülete — K-016, 6–12. hó.
- Az összevonás formális visszavonása — Ü-4, megtartott hiány (8.4).

---

## 2. Domain-modell

### 2.1 Új/módosított entitások

**Új entitás: nincs. Új enum: nincs. Új tárolt mező: nincs. Új kapcsolat: nincs.**

A duplikáció adatmodellje a `00_domain_model.md`-ben **már kész**. A feature
három meglévő elemre épít, és **két** magyarázó megjegyzés-sorral pontosítja a
domain-modellt — egyik sem új struktúra.

| Elem | Hol él már | Szerep |
|---|---|---|
| `Ticket.originalTicketId` (self-FK) | `00_domain_model.md` 1.2.6 | A duplikáció-kapcsolat egyetlen tárolt hordozója |
| `RejectionReason.Duplicate` | `00_domain_model.md` 1.2.3, 7. blokk | A duplikáció-elutasítás strukturált oka |
| `ActivityEventType.Merged` | `00_domain_model.md` 1.4 | Az összevonás napló-eseménye |

### 2.2 Domain-modell-pontosítás 1 — az értesítendő-halmaz (TD-D1)

> **Beszúrandó a `00_domain_model.md` 1.2.6-ba, a "duplikáció-kapcsolat
> iránya" megjegyzés után:**
>
> **Az eredeti ügy értesítendőinek halmaza.** K-034 előírja, hogy a duplikátum
> bejelentője az eredeti ügy értesítendői közé kerül. Ezt **nem külön entitás
> vagy mező** hordozza: az eredeti ügy értesítendőinek halmaza lekérdezésből
> áll elő — `{eredeti.reporterId} ∪ {d.reporterId | d.originalTicketId =
> eredeti.id}`. Az `originalTicketId` self-FK beírása az összevonáskor
> **önmagában** átköti a duplikátum bejelentőjét; nincs `TicketSubscriber`
> entitás. Az értesítés tényleges kézbesítése nem a domain-modell tárgya.

A `reporterId` cél-típusa továbbra is nyitott (`NY-2`) — TD-D1 ezzel nem
ütközik, csak a `reporterId` azonosító-szerepét használja.

### 2.3 Domain-modell-pontosítás 2 — az `originalTicketDisplayId` (TD-D7)

> **Beszúrandó a `00_domain_model.md` 1.2.6-ba:**
>
> **Az `originalTicketDisplayId` nem tárolt mező.** A detail-DTO-ban, a
> `similar`- és a merge-válaszban megjelenő `originalTicketDisplayId` a
> DTO-réteg **származtatott** értéke: az eredeti `Ticket` `displayId`-ja
> (`Tenant.displayPrefix` + `-` + az eredeti `ticketNumber`, SD-16). A
> `Ticket` egyetlen tárolt duplikáció-mezeje az `originalTicketId` self-FK.

### 2.4 Állapotgép

Az összevonás állapotgép-vetületét a `02_globalis_allapotgep.md` **3.7**
**már formálisan rögzítette** — kiváltó akció, jogosultság, előfeltétel,
kétoldali mellékhatás. A feature-spec ezt **hivatkozza, nem ismétli**.

A feature **hozzátesz** az állapotgéphez: a 3.7 előfeltételét a 3.3
versenyhelyzet-elemzés szerint két ponttal szigorítja (az eredeti nem maga is
duplikátum; az eredeti nem végállapotú). Ez nem ütközés — visszacsorgó jelzés,
lásd 8.6 VF-D1.

A `similar`-végpont **nem** állapotgép-átmenet — read-only.

### 2.5 A heurisztika domain-szintű olvasata

| Jel | Domain-forrás | Szabály |
|---|---|---|
| Térbeli közelség | `Ticket.latitude` + `longitude` | A két ügy távolsága ≤ 50 m. Ha bármelyiknek nincs koordinátája → a jel nem értékelhető (`20` 2.4) |
| Kategória-egyezés | `Ticket.categoryId`, fallback `citizenSuggestedCategoryId` | A gyökérre feloldott kategória azonos. Forrás-sorrend: `categoryId` → `citizenSuggestedCategoryId` (TD-D2). Ha egyik sincs → a jel nem értékelhető |
| Idő-közelség | `Ticket.createdAt` | `|Δ createdAt|` ≤ 14 nap |
| Nyitott-szűrés | `Ticket.status` | A találati halmaz csak `New`/`Assigned`/`InProgress` (`20` 3.3) |
| Már-duplikátum kizárása | `Ticket.originalTicketId` | A találatból kizárt minden ügy, amelynek `originalTicketId`-ja kitöltött |

Ha egy jel **nem értékelhető**, a `20` 2.4 elve szerint a rendszer **hallgat**
— az ügypár nem duplikáció-gyanú. Ez K-032 *alkalmazási részlete* (TD-D2), nem
felülírása: a három jelből egy sem hagyható el, csak a forrásuk oldódik fel
rugalmasan.

### 2.6 Lookup-ok

**Nem releváns, mert** a feature nem vezet be referencia-adatot. A `Category`-fa
meglévő lookup-forrás; a `RejectionReason`/`ActivityEventType` enum már
tartalmazza a szükséges értéket (`Duplicate`, `Merged`).

---

## 3. Szerver — API és logika

A `Ticket` standard CRUD-ja a `10` 3. szakaszában lefedett. **Ez a feature két
nem-standard végpontot ad** — egyik sem `BaseController`-művelet.

> **Eltérés a mintától.** A `GET .../similar` heurisztika-számítás (nem
> `BaseController` `ListAsync`); a `POST .../merge` workflow-akció (nem
> `BaseController` update). A `TicketController` a `10` 3.4 mintája szerint
> veszi fel őket; a `merge` logikája a `TicketWorkflowService`-be illeszkedik,
> a `reject` szomszédjaként. A konkrét osztály-struktúra fejlesztői döntés.

### 3.1 `GET /v1/tickets/{id}/similar` — hasonló bejelentések

**Eltérés a mintától:** read-only, mellékhatás nélküli heurisztika-végpont.

#### 3.1.1 Request

| Elem | Érték |
|---|---|
| Method / route | `GET /v1/tickets/{id}/similar` |
| Path-paraméter | `{id}` — a vizsgált `Ticket` belső `id`-ja |
| Query-paraméter | nincs (a küszöbök kódba égetve — K-016) |
| Request body | nincs |

#### 3.1.2 Response — `200 OK`

A válasz a `SimilarTicketDto` rendezett listája. "Nincs találat" → **üres
lista**, nem `404`.

```jsonc
{
  "items": [
    {
      "id": 198,
      "displayId": "ALM-198",
      "title": "Kátyú a Petőfi utcán",
      "status": "InProgress",
      "categoryLabel": "Utak és járdák > kátyú",
      "distanceMeters": 35,
      "ageDifferenceDays": 3,
      "createdAt": "2026-05-15T07:20:00Z",
      "updatedAt": "2026-05-16T11:00:00Z",
      "thumbnailRef": "..."
    }
  ]
}
```

A `SimilarTicketDto` mezői:

| Mező | Típus | Forrás / jelentés |
|---|---|---|
| `id` | `long` | A találat `Ticket.id`-ja |
| `displayId` | `string` | `Tenant.displayPrefix` + `-` + `ticketNumber` (feloldva, SD-16) |
| `title` | `string` | `Ticket.title` |
| `status` | `enum TicketStatus` | `New`/`Assigned`/`InProgress` |
| `categoryLabel` | `string` | A találat feloldott kategória-neve |
| `distanceMeters` | `int` | A térbeli jel számszerű értéke (méterre kerekítve) |
| `ageDifferenceDays` | `int` | Az idő-jel számszerű értéke (napra kerekítve) |
| `createdAt` | `DateTime` | A találat beérkezési ideje (relatív megjelenítéshez) |
| `updatedAt` | `DateTime` | A találat concurrency-tokene — ha a felhasználó az összevonás-párbeszédben megfordítja a szerepeket, a `merge` ezt küldi `expectedUpdatedAt`-ként (lásd 4.3.2) |
| `thumbnailRef` | `string?` | Az első `ReportPhoto` bélyegkép-hivatkozása |

A `distanceMeters`/`ageDifferenceDays` a determinisztikus heurisztika
átláthatóságát szolgálja (`20` 3.1).

#### 3.1.3 Sorrend és limit

- **Sorrend** (`20` 3.4): növekvő `distanceMeters`; azonos távolságnál csökkenő `createdAt`.
- **Limit:** legfeljebb 20 találat. A kliens 3-4-et mutat közvetlenül (5. szakasz); a 20 fölötti elvi eset nem ad érdemi triage-információt.

#### 3.1.4 A heurisztika logikája

A `{id}` ügyhöz hasonló az a `Ticket`, amelyre **mindhárom értékelhető jel
teljesül** (a 2.5 domain-olvasat szerint):

1. **Nyitott-szűrés:** `status` ∈ {`New`,`Assigned`,`InProgress`}; `originalTicketId` üres; `id` ≠ `{id}` (önkizárás).
2. **Térbeli jel:** mindkét ügynek van koordinátája, és a távolság ≤ 50 m. Ha bármelyiknek nincs → a pár nem találat.
3. **Kategória-jel:** a gyökérre feloldott kategória azonos. Forrás-sorrend ügyenként: `categoryId` → `citizenSuggestedCategoryId`. Ha bármelyiknek egyik kategória-mezeje sincs → a pár nem találat.
4. **Idő-jel:** `|Δ createdAt|` ≤ 14 nap.

> **Eltérés a mintától — a `{id}` ügy maga lehet duplikátum.** Ha a `{id}`
> ügynek kitöltött `originalTicketId`-ja van, a végpont **üres listát** ad
> (`200`, `items: []`), nem hibát. Egy duplikátumnak nincs értelmes "hasonló
> nyitott" halmaza.

#### 3.1.5 Implementációs döntések — naplózva (`99_donesnaplo.md`)

| Döntés | Tartalom | Indok |
|---|---|---|
| Távolság-számítás | Bounding-box előszűrés (±50 m lat/lon-ablak, `decimal`-összehasonlítás) → pontos Haversine a szűkebb halmazon. Nem PostGIS | A `latitude`/`longitude` sima `decimal`; pilot-méreten arányos, a PostGIS scope-feszítés |
| Gyökér-feloldás | Alkalmazás-szintű, egylépéses `Category.parentId`-felmenet (a fa pontosan kétszintű) | Kétszintű fánál a felmenet max. egy lépés — rekurzív CTE túlzás |
| Index | `Ticket(status, createdAt)` index | A heurisztika fő szűrője; pilot-méreten elég |
| Küszöb-tárolás | A három küszöb névvel ellátott konfig-konstans-blokkban a duplikáció-logikában | K-016: pilotra kódból módosítható; a 90. napi finomítást megkönnyíti |

A **tenant-határ** automatikus a befecskendezett `DbContext` révén
(`01_kozos_mintak.md` 2.1) — **nem eltérés a mintától**.

#### 3.1.6 Hibakódok

| Kód | Mikor |
|---|---|
| `200` | A `{id}` létezik és a kérő jogosult — beleértve az üres listát |
| `404` | A `{id}` nem létezik, vagy másik tenant ügye |
| `403` | A kérő szerepköre nem jogosult (3.5) |

### 3.2 `POST /v1/tickets/{id}/merge` — összevonás

**Eltérés a mintától:** workflow-akció a `10` 3.4 mintája szerint. A
`02_globalis_allapotgep.md` 3.7 átmenetének HTTP-szerződése.

#### 3.2.1 A route szemantikája

A `{id}` path-paraméter **a duplikátum** — az az ügy, amelyik `Rejected` lesz.
Az eredeti ügyet a request body adja. Konzisztens a `10` 3.4 akció-végpontjaival,
ahol a `{id}` mindig az átmenetet elszenvedő ügy.

#### 3.2.2 Request

```jsonc
POST /v1/tickets/234/merge
{
  "originalTicketId": 198,
  "expectedUpdatedAt": "2026-05-18T09:12:33Z"
}
```

| Mező | Típus | Köt. | Jelentés |
|---|---|---|---|
| `originalTicketId` | `long` | K | A megmaradó eredeti ügy `id`-ja |
| `expectedUpdatedAt` | `DateTime` | K | A duplikátum (`{id}`) `updatedAt`-ja optimistic-concurrency tokenként (SD-32) |

A megerősítő párbeszéd eredeti/duplikátum választását a kliens fordítja
végponthívásra: a duplikátummá váló ügy `id`-ja a route `{id}`-ja, a megmaradóé
az `originalTicketId` (lásd 4.3.2).

#### 3.2.3 Előfeltételek — a teljes, formalizált lista (TD-D5)

| # | Előfeltétel | Sérülés esetén |
|---|---|---|
| 1 | A `{id}` (duplikátum) létezik, a kérő tenantjában | `404` |
| 2 | Az `originalTicketId` ügy létezik, a kérő tenantjában | `404` |
| 3 | `originalTicketId` ≠ `{id}` | `422` `reason: "self_merge"` |
| 4 | A `{id}` nem-végállapotú: `status` ∈ {`New`,`Assigned`,`InProgress`} | `409` `reason: "invalid_transition"` |
| 5 | Az eredeti nem-végállapotú: `status` ∈ {`New`,`Assigned`,`InProgress`} | `422` `reason: "original_not_open"` |
| 6 | Az eredeti `originalTicketId`-ja üres (nem maga is duplikátum) | `422` `reason: "original_is_duplicate"` |
| 7 | `expectedUpdatedAt` egyezik a `{id}` aktuális `updatedAt`-jával | `409` `reason: "stale"` |

> **A `409` vs. `422` határa (`01_kozos_mintak.md` 6.3).** A 4. és 7. **`409`**
> — a `{id}` ügy állapota/verziója konfliktusban (tiltott átmenet /
> optimistic-ütközés), a `10` 3.4 `reason`-konvenciója szerint. A 3., 5., 6.
> **`422`** — a kérés formailag jó, de a megnevezett eredeti ügy nem alkalmas
> összevonás-célnak (üzleti-előfeltétel).

#### 3.2.4 Mellékhatás — tranzakciós, kétoldali (TD-D4)

Siker esetén **egyetlen adatbázis-tranzakcióban**:

**A duplikátumon (`{id}`):**
- `status := Rejected`
- `rejectionReasonCode := Duplicate`
- `rejectionReasonText := null` (SD-40 — a hivatkozást az `originalTicketId` hordozza)
- `originalTicketId := <request.originalTicketId>`
- a triage-adatok (`categoryId`, `priority`, `dueDate`, `assignedGroupId`/`assignedUserId`, `assignedAt`) **változatlanok** (a `Rejected`-be lépés nem nullázza őket)
- egy `Merged` `ActivityLog`-sor (3.2.5)

**Az eredetin (`originalTicketId`):**
- `status` **változatlan** (`02_allapotgep` 3.7)
- egy `Merged` `ActivityLog`-sor (3.2.5)
- a bejelentő-átkötés **nem külön írási lépés** — a duplikátum `originalTicketId`-jának beírása maga az átkötés (TD-D1; 2.2)

Az **értesítés** (a duplikátum bejelentőjének) az `AfterSaveAsync`-ben, "best
effort" (`10` 3.4 minta): hibája nem görgeti vissza az összevonást. A polgári
megjelenítés nem ez a feature (`20` 7.2, lásd 8.4).

> A tranzakció a két `Ticket`-et és a két `ActivityLog`-sort együtt menti vagy
> együtt görgeti vissza. Részleges siker tilos.

#### 3.2.5 A két `Merged` `ActivityLog`-sor (TD-D6)

| Oldal | `ticketId` | `eventType` | `actorId` | `fromValue` | `toValue` | `note` |
|---|---|---|---|---|---|---|
| duplikátum | `{id}` | `Merged` | a kérő `TenantUser` | a duplikátum korábbi `status`-a (enum-név) | `"ticket:<originalTicketId>"` | `null` |
| eredeti | `originalTicketId` | `Merged` | a kérő `TenantUser` | `null` | `"ticket:<{id}>"` | `null` |

A `toValue` stabil, nyelvfüggetlen `ticket:<id>` referencia (SD-30, a
`Reassigned` `"group:3"` mintája). A kliens ezt megjelenítő `displayId`-ra
oldja fel.

#### 3.2.6 Response

| Kód | Body | Mikor |
|---|---|---|
| `200` | `TicketDto` — a duplikátum (`{id}`) frissített adatlapja | Siker |
| `404` | hibakód-keret | Előfeltétel 1 vagy 2 |
| `409` | `{ "reason": "invalid_transition" \| "stale" }` | Előfeltétel 4 vagy 7 |
| `422` | `{ "reason": "self_merge" \| "original_not_open" \| "original_is_duplicate" }` | Előfeltétel 3, 5 vagy 6 |
| `403` | hibakód-keret | A kérő szerepköre nem jogosult (3.5) |

A `200` a duplikátum `TicketDto`-ját adja — a kliens innen rajzolja át az
adatlapot a redukált duplikátum-nézetre (`10` SD-29).

### 3.3 Validáció — FluentValidation

| Mező | Szabály |
|---|---|
| `originalTicketId` | Kötelező; pozitív `long`; létező `Ticket`-re mutasson (tenanton belül) — egyébként `404` |
| `expectedUpdatedAt` | Kötelező; érvényes `timestamptz` |

A 3.2.3 előfeltételei (3–6) **nem mező-szintű FluentValidation**, hanem
workflow-előfeltételek — a `TicketWorkflowService`-ben ellenőrződnek, `409`/`422`
`reason`-kódot adnak, nem `fieldErrors`-t (a `10` 3.4 mintája). A
`similar`-végpontnak nincs request body-ja — nincs FluentValidation-szabálya.

A feature nem vezet be új mezőt — nincs ismételni való méret. A `merge` nem
fogad `rejectionReasonText`-et (a duplikáció-indok strukturált).

### 3.4 Workflow- és business-szabályok

- Az összevonás a `02_globalis_allapotgep.md` 3.7 `→ Rejected` átmenete a duplikátumon; az eredeti állapota változatlan.
- A `similar` read-only, státuszt nem mozdít.
- A kétoldali mellékhatás atomi (3.2.4).

### 3.5 Jogosultság — `authorization.json`

```jsonc
{ "method": "GET",  "path": "/v1/tickets/{id}/similar",
  "roles": ["dispatcher", "manager"] }
{ "method": "POST", "path": "/v1/tickets/{id}/merge",
  "roles": ["dispatcher", "manager"] }
```

A `field_worker` egyik végpontot sem éri el (`05` 2.5); a `content_manager` a
`/v1/tickets/*` route-okhoz nincs jogosultsága (`10` 4.4). A route-szintű
engedély a durva szűrő; a rekord-szintű tenant-szűrés a `01_kozos_mintak.md` 4.
kétrétegű mintája szerint automatikus.

### 3.6 Multi-tenancy érvényesítés

A `Ticket` a Tenant DB-ben él; a befecskendezett `DbContext` a helyes tenant
DB-re mutat (`01_kozos_mintak.md` 2.1) — a `similar`-heurisztika és a
`merge`-tranzakció **automatikusan tenant-izolált**. **Nem eltérés a mintától.**

### 3.7 Audit és naplózás

A `merge` az `AuditableEntity` `UpdatedAt/By`-ját mindkét `Ticket`-en
automatikusan frissíti. A két `Merged` `ActivityLog`-sor (3.2.5) a
feature-specifikus naplózás. A `similar` read-only, nem naplóz.

### 3.8 i18n — szerver-vetület

A szerver **kódot/`reason`-t ad, nem kész szöveget** (`01_kozos_mintak.md` 5.3).
A `reason`-kódok kliens-oldali leképezése a 4.6-ban.

---

## 4. Admin felület

### 4.1 Érintett képernyők

A duplikáció-feature **egyetlen képernyőhöz** ad UI-t: a bejelentés-adatlaphoz
(`/bejelentesek/details/<id>`). Nincs önálló nézete (`20` 1.).

A `10` 4.3 már megírta — **ez a feature NEM ismétli:** a doboz helye, betöltési
viselkedése, szerepkör-láthatósága, és a redukált duplikátum-adatlap.

### 4.2 A "Hasonló bejelentések" doboz — komponens-vázlat

**Eltérés a mintától:** a doboz **nem `TableStateConfig` lista-oldal**, hanem
adatlap-widget (munkanév: `SimilarTicketsBox`). Egy konkrét `Ticket`-hez
relatív, a `GET .../similar` egyszeri hívásából rajzolt, nem lapozott, nem
URL-állapotú — a `TableStateConfig` apparátusa itt értelmetlen volna. A konkrét
komponens-struktúra fejlesztői döntés.

#### 4.2.1 A doboz négy állapota

| Állapot | Mikor | Megjelenés |
|---|---|---|
| Betöltés | A `similar`-hívás folyamatban | Skeleton/spinner a doboz keretén belül; az adatlap többi része nem vár rá (`10` 4.3) |
| Üres — csendes | `200`, `items: []` | Egyetlen halvány sor: `ticket.similar.empty`. Figyelmet nem kér (`20` 3.1) |
| Találat — figyelemkérő | `200`, `items` nem üres | Keret-fejléc figyelemjelzővel + találatszám; a találat-sorok; max. 3-4 közvetlenül, a többi "+ további N" alatt (`20` 3.4) |
| Hiba | `4xx`/`5xx` (a `404` kivételével) | A doboz saját hibasávja "Újrapróbálás" linkkel; az adatlap többi része használható, a triage befejezhető (K-033) |

A `field_worker`-nek a doboz meg sem jelenik (`10` 4.3 elemtérkép).

#### 4.2.2 A találat-sor — mezősablon

Egy találat-sor a `SimilarTicketDto` (3.1.2) egy elemét jeleníti meg.

**Fej-elemek**
- Megjelenő tartalom: `displayId` + `title` (pl. "ALM-198 · Kátyú a Petőfi utcán")
- Adatmodell: `SimilarTicketDto.displayId`, `.title`
- Interakció: a fej-elem a "Megnyitás"-akcióra kattintható

**Jelek (a heurisztika átláthatósága)**
- Megjelenő tartalom: státusz-címke · távolság · idő-különbség · kategória (pl. "Folyamatban · 35 m · 3 napja · Utak és járdák")
- i18n kulcsok: státusz `ticket.status.*` (meglévő); távolság `ticket.similar.distance` (paraméter: `meters`); idő `ticket.similar.age` / `.age.today` / `.age.yesterday` (paraméter: `days`); kategória adat
- Adatmodell: `SimilarTicketDto.status` / `.distanceMeters` / `.ageDifferenceDays` / `.categoryLabel`

**Bélyegkép (opcionális)**
- Megjelenő tartalom: a `thumbnailRef`-ből rajzolt kis kép, ha van
- Adatmodell: `SimilarTicketDto.thumbnailRef` — presigned-URL-feloldás az `Attachment`-feature mintája, itt fogyasztva
- Eredet: új javaslat — a fotó-összevetést (`20` 3.2) kattintás nélkül támogatja

**Akciók** (`20` 3.2 — egyik sem kötelező)

| Akció | Címke (i18n) | Mit tesz |
|---|---|---|
| Megnyitás | `ticket.similar.action.open` | Navigál `/bejelentesek/details/<SimilarTicketDto.id>`-re |
| Ez ugyanaz → összevonás | `ticket.similar.action.merge` | Megnyitja az összevonás-megerősítő párbeszédet (4.3) |
| *(Nem csinál semmit)* | — | A doboz nem kapu (K-033); nincs UI-elem |

### 4.3 Az összevonás-megerősítő párbeszéd

A "Ez ugyanaz → összevonás" gomb egy kis modális párbeszédet nyit (`20` 4.3) —
az ügy `Rejected`-be tétele következménnyel jár.

#### 4.3.1 A párbeszéd tartalma

- **Választógomb-pár:** "Melyik bejelentés maradjon az élő ügy?" — a két ügy `displayId` + `title` + relatív kor. **Az alapértelmezett kijelölés a régebbi `createdAt`-ú** (`20` 4.1, K-034); a felhasználó felülírhatja.
- **Magyarázó szöveg:** `ticket.merge.dialog.consequence`.
- **Gombok:** "Mégsem" / "Összevonás".

#### 4.3.2 A választás leképezése a `merge`-hívásra

- a **nem kijelölt** ügy → a route `{id}`-ja (a duplikátum);
- a **kijelölt** ügy → a request `originalTicketId`-ja;
- `expectedUpdatedAt` → a **duplikátummá váló** ügy `updatedAt`-ja. Ha a duplikátum a jelenlegi adatlap ügye, az `updatedAt` az adatlap-betöltésből van; ha a duplikátum a *találat*, a `SimilarTicketDto.updatedAt` mezeje adja (3.1.2 — ezért hordozza a DTO az `updatedAt`-ot).

### 4.4 A redukált duplikátum-adatlap

A `10` SD-29 eldöntötte: ha `originalTicketId` kitöltött, redukált
csak-olvasható adatlap **duplikáció-doboz nélkül**. Ez a feature ezt
**hivatkozza** — a doboz egy duplikátum saját adatlapján meg sem jelenik, és a
kliens a `similar`-végpontot sem hívja. Az eredeti ügy adatlapján a "1
duplikátum összevonva" jelzés a self-FK fordított oldalából jön
(`00_domain_model.md` 1.2.6); megjelenítési helye a `10` hatásköre.

### 4.5 Üres / betöltési / hibaállapot

A doboz saját négy állapotát a 4.2.1 adja. Kulcs elv (K-033): a doboz bármely
állapota **nem blokkolja** az adatlap többi részét és a triage befejezését.

### 4.6 Jogosultság

A `SimilarTicketsBox` és az összevonás-párbeszéd `dispatcher`/`manager`-nek
renderelődik, `field_worker`-nek nem (`10` 4.3, `05` 2.5). **Kétrétegű
védelem** (`01_kozos_mintak.md` 4.): a kliens elrejti, a szerver `403`-mal
utasít el (3.5).

### 4.7 i18n kulcsok (`hu.json`)

A felület **tegez**; a szerver kódot ad, a kliens fordítja (`01_kozos_mintak.md`
5.3).

**Doboz — keret és állapotok**

| Kulcs | Magyar szöveg |
|---|---|
| `ticket.similar.title` | Hasonló bejelentések |
| `ticket.similar.empty` | Nincs hasonló nyitott bejelentés. |
| `ticket.similar.countBadge` | {count} |
| `ticket.similar.showMore` | + további {count} |
| `ticket.similar.loadError` | A hasonló bejelentések nem tölthetők be. |
| `ticket.similar.retry` | Újrapróbálás |

**Találat-sor — jelek és akciók**

| Kulcs | Magyar szöveg |
|---|---|
| `ticket.similar.distance` | {meters} m |
| `ticket.similar.age` | {days} napja |
| `ticket.similar.age.today` | ma |
| `ticket.similar.age.yesterday` | tegnap |
| `ticket.similar.action.open` | Megnyitás |
| `ticket.similar.action.merge` | Ez ugyanaz → összevonás |

**Összevonás-megerősítő párbeszéd**

| Kulcs | Magyar szöveg |
|---|---|
| `ticket.merge.dialog.title` | Összevonás megerősítése |
| `ticket.merge.dialog.question` | Melyik bejelentés maradjon az élő ügy? |
| `ticket.merge.dialog.consequence` | A másik bejelentés »Duplikáció« indoklással lezárásra kerül, és a bejelentője értesítést kap. |
| `ticket.merge.dialog.cancel` | Mégsem |
| `ticket.merge.dialog.confirm` | Összevonás |
| `ticket.merge.success` | A bejelentések összevonva. |

**Hibaüzenet- és konfliktus-kulcsok** (a szerver `reason`-kódjához — 3.8)

| Kulcs | Magyar szöveg | Szerver-`reason` (HTTP) |
|---|---|---|
| `ticket.merge.error.selfMerge` | Egy bejelentés nem vonható össze önmagával. | `self_merge` (`422`) |
| `ticket.merge.error.originalNotOpen` | A megjelölt eredeti bejelentés már lezárt — nem lehet összevonás célja. | `original_not_open` (`422`) |
| `ticket.merge.error.originalIsDuplicate` | A megjelölt eredeti bejelentés maga is duplikátum. Válaszd az ő eredeti ügyét. | `original_is_duplicate` (`422`) |
| `ticket.merge.error.invalidTransition` | Ez a bejelentés a jelenlegi állapotában nem vonható össze. | `invalid_transition` (`409`) |
| `ticket.merge.error.stale` | A bejelentés időközben módosult. Töltsd be újra, és próbáld újra. | `stale` (`409`) |

**Esemény-napló-kulcsok** — a `Merged` esemény magyar mondatsablonja. A
duplikátum- és az eredeti-oldali bejegyzés eltérő mondatot kíván; a `10` 4.6
egyetlen `ticket.activity.merged` kulcsát ez **kettébontja** (visszacsorgó
jelzés — 8.6 VF-D2). A kliens az oldalt a `Merged` log `fromValue`-jából tudja
(duplikátum-oldal: `TicketStatus`; eredeti-oldal: `null`).

| Kulcs | Magyar szöveg | Mikor |
|---|---|---|
| `ticket.activity.merged.asDuplicate` | {actor} összevonta ezt a bejelentést — eredeti: {ticket} | a duplikátum naplójában |
| `ticket.activity.merged.asOriginal` | {actor} ide vont össze egy bejelentést: {ticket} | az eredeti naplójában |

---

## 5. Polgári mobilapp adatigénye

> Adat-szintű specifikáció, UX-tervezés nincs (project instructions — Hatókör).

### 5.1 A duplikáció a polgári oldalon

A duplikáció-szűrés és az összevonás **tisztán admin-felületi**. A polgári
mobilappnak nincs duplikáció-funkciója — a polgár csak az összevonás
**következményét** éli meg a `screen_bejelentes_adatlap` képernyőn.

| Vetület | Hol dől el |
|---|---|
| Az összevonás adat-rögzítése (`→ Rejected`, `originalTicketId`, bejelentő-átkötés) | **Ez a feature**, 3.2 |
| Az összevonás polgári megjelenítése ("már jelezték, dolgozunk rajta"; az eredeti ügy státuszai) | Polgári mobilapp dokumentuma — modulközi átadás (`20` 7.2) |

### 5.2 API-hívások — adatáramlás

A feature **nem definiál polgári API-hívást.** Az érintettség közvetett:

| Adatfolyam | Irány | Mit jelent |
|---|---|---|
| A duplikátum-ügy adatlapjának polgári lekérdezése | olvasás | A meglévő polgári bejelentés-adatlap-végpont (`NY-bej-1`); a duplikáció nem ad új végpontot |
| Az eredeti ügy státuszfrissítéseinek eljutása a duplikátum bejelentőjéhez | olvasás (közvetett) | A manager-oldal az `originalTicketId`-val rögzíti az átkötést; a polgári lekérdezés/értesítés módja a polgári adatigény-spec dolga |

A `merge`-végpont `200` válasza a manager-kliensé — a polgári app nem fogyasztja.

### 5.3 Adat-szintű nyitott kérdések

Mindegyik **már átadott** modulközi kérdés — lásd 8.4.

---

## 6. Acceptance criteria

### 6.1 AC-S — a `similar`-heurisztika

**AC-S1.** *Given* két `New` bejelentés 35 m-re, azonos gyökér-kategóriával, 3 nap különbséggel; *When* `GET .../similar` az egyikre; *Then* `200`, az `items` tartalmazza a másikat.

**AC-S2.** *Given* két bejelentés 80 m-re (a többi jel egyezik); *When* `similar`-hívás; *Then* a másik nem szerepel az `items`-ben.

**AC-S3.** *Given* két bejelentés azonos helyen/kategóriában, 20 nap különbséggel; *When* `similar`-hívás; *Then* a másik nem szerepel az `items`-ben.

**AC-S4.** *Given* két bejelentés azonos helyen/időben, eltérő gyökér-kategóriával; *When* `similar`-hívás; *Then* a másik nem szerepel az `items`-ben.

**AC-S5.** *Given* a vizsgált bejelentésnek nincs koordinátája; *When* `similar`-hívás rá; *Then* `200`, `items: []`.

**AC-S6.** *Given* egy kandidátusnak nincs koordinátája (a többi jel egyezne); *When* `similar`-hívás; *Then* a kandidátus nem szerepel az `items`-ben.

**AC-S7.** *Given* a vizsgált ügynek nincs `categoryId`-ja, de van `citizenSuggestedCategoryId`-ja, amelynek gyökere egyezik egy kandidátuséval; *When* `similar`-hívás; *Then* a kandidátus szerepel az `items`-ben (TD-D2 fallback).

**AC-S8.** *Given* a vizsgált ügynek sem `categoryId`-ja, sem `citizenSuggestedCategoryId`-ja; *When* `similar`-hívás; *Then* `200`, `items: []`.

**AC-S9.** *Given* egy `Resolved` és egy `Rejected` ügy, amely minden jelben egyezne; *When* `similar`-hívás; *Then* egyik sem szerepel az `items`-ben.

**AC-S10.** *Given* egy ügy kitöltött `originalTicketId`-val, amely minden jelben egyezne; *When* `similar`-hívás; *Then* nem szerepel az `items`-ben.

**AC-S11.** *Given* egy `{id}` bejelentés; *When* `similar`-hívás rá; *Then* az `items` nem tartalmazza magát a `{id}` ügyet.

**AC-S12.** *Given* három találat 12 m / 40 m / 40 m távolságra, az utóbbi kettő eltérő `createdAt`-tal; *When* `similar`-hívás; *Then* a sorrend: 12 m, majd a két 40 m közül a frissebb, végül a régebbi.

**AC-S13.** *Given* egy találat 35 m-re és 3 nap különbséggel; *When* `similar`-hívás; *Then* a `SimilarTicketDto.distanceMeters` = 35, `.ageDifferenceDays` = 3.

**AC-S14.** *Given* a `{id}` ügy maga duplikátum; *When* `similar`-hívás rá; *Then* `200`, `items: []` (nem `404`/`409`).

**AC-S15.** *Given* egy nem létező `{id}`; *When* `similar`-hívás; *Then* `404`.

### 6.2 AC-M — az összevonás-végpont

**AC-M1.** *Given* a `{id}` (`New`) duplikátum és egy `originalTicketId` (`InProgress`) eredeti, mindkettő nem-duplikátum; *When* `POST .../merge` érvényes `expectedUpdatedAt`-tal; *Then* `200`, a `{id}` `status`-a `Rejected`, `rejectionReasonCode`-ja `Duplicate`, `originalTicketId`-ja az eredetire mutat.

**AC-M2.** *Given* sikeres összevonás; *When* a `{id}` adatait lekérdezzük; *Then* a `rejectionReasonText` `null`.

**AC-M3.** *Given* sikeres összevonás; *When* az eredeti ügyet lekérdezzük; *Then* az eredeti `status`-a változatlan.

**AC-M4.** *Given* sikeres összevonás; *When* a `{id}` `ActivityLog`-ját lekérdezzük; *Then* tartalmaz egy `Merged` eseményt, `toValue` = `"ticket:<originalTicketId>"`, `fromValue` = a duplikátum összevonás előtti `status`-a.

**AC-M5.** *Given* sikeres összevonás; *When* az eredeti `ActivityLog`-ját lekérdezzük; *Then* tartalmaz egy `Merged` eseményt, `toValue` = `"ticket:<{id}>"`, `fromValue` = `null`.

**AC-M6.** *Given* egy `Assigned`, kitöltött `assignedUserId`-jú duplikátum-jelölt; *When* sikeres összevonás; *Then* a duplikátummá vált ügy `assignedUserId`-ja és `assignedAt`-ja változatlan.

**AC-M7.** *Given* egy `merge`-kérés, ahol `originalTicketId` = a route `{id}`; *When* feldolgozás; *Then* `422`, `reason: "self_merge"`.

**AC-M8.** *Given* egy `merge`-kérés, ahol az `originalTicketId` ügy `Resolved`; *When* feldolgozás; *Then* `422`, `reason: "original_not_open"`.

**AC-M9.** *Given* egy `merge`-kérés, ahol az `originalTicketId` ügynek kitöltött `originalTicketId`-ja van; *When* feldolgozás; *Then* `422`, `reason: "original_is_duplicate"`.

**AC-M10.** *Given* egy `merge`-kérés, ahol a `{id}` már `Rejected`; *When* feldolgozás; *Then* `409`, `reason: "invalid_transition"`.

**AC-M11.** *Given* egy `merge`-kérés elavult `expectedUpdatedAt`-tal; *When* feldolgozás; *Then* `409`, `reason: "stale"`, és sem a `{id}`, sem az eredeti nem módosul.

**AC-M12.** *Given* egy `merge`-kérés nem létező `originalTicketId`-vel; *When* feldolgozás; *Then* `404`.

**AC-M13.** *Given* egy állapot, ahol az eredeti ügy `ActivityLog`-írása meghiúsulna; *When* a `merge`-tranzakció fut; *Then* a `{id}` `status`-a sem változik `Rejected`-re (atomi mellékhatás).

**AC-M14.** *Given* sikeres összevonás `{id}` = 234, `originalTicketId` = 198; *When* az eredeti (198) értesítendőit lekérdezzük (`reporterId` ∪ a rá mutató duplikátumok `reporterId`-jai); *Then* a halmaz tartalmazza a 234-es ügy `reporterId`-ját.

**AC-M15.** *Given* sikeres összevonás; *When* a `200` body-t vizsgáljuk; *Then* a body a `{id}` `TicketDto`-ja, kitöltött `originalTicketId`- és `originalTicketDisplayId`-mezővel.

### 6.3 AC-B — a "Hasonló bejelentések" doboz

**AC-B1.** *Given* a `similar`-hívás folyamatban; *When* az adatlap renderelődik; *Then* a doboz betöltési állapotot mutat, az adatlap többi blokkja kész és használható.

**AC-B2.** *Given* a `similar`-hívás `200`, `items: []`; *When* a doboz renderelődik; *Then* a doboz az `ticket.similar.empty` szöveget mutatja, figyelemjelző nélkül.

**AC-B3.** *Given* a `similar`-hívás `200`, két találattal; *When* a doboz renderelődik; *Then* a fejléc figyelemjelzőt és a `2` számot mutatja, mindkét sor látható.

**AC-B4.** *Given* a `similar`-hívás hibát ad (a `404` kivételével); *When* a doboz renderelődik; *Then* a doboz saját hibasávot mutat "Újrapróbálás" linkkel, és a "Triage kész — Kiosztás" gomb kattintható marad.

**AC-B5.** *Given* öt találat; *When* a doboz renderelődik; *Then* max. 3-4 sor jelenik meg közvetlenül, a többi "+ további N" mögött.

**AC-B6.** *Given* egy találat-sor; *When* a felhasználó a "Megnyitás"-ra kattint; *Then* a kliens a `/bejelentesek/details/<találat-id>`-re navigál.

**AC-B7.** *Given* egy találat-sor; *When* a felhasználó a "Ez ugyanaz → összevonás"-ra kattint; *Then* megnyílik a párbeszéd, a régebbi `createdAt`-ú ügy kijelölve.

**AC-B8.** *Given* a párbeszéd a régebbi ügyet jelöli ki alapból; *When* a felhasználó nem változtat, és "Összevonás"-t nyom; *Then* a `merge`-hívás route-`{id}`-ja az újabb, `originalTicketId`-ja a régebbi.

**AC-B9.** *Given* a párbeszéd; *When* a felhasználó megfordítja a kijelölést (az újabb az élő ügy), és "Összevonás"-t nyom; *Then* a `merge`-hívás route-`{id}`-ja a régebbi, `originalTicketId`-ja az újabb, `expectedUpdatedAt`-ja a régebbi (most duplikátummá váló) ügy `updatedAt`-ja.

**AC-B10.** *Given* egy bejelentés kitöltött `originalTicketId`-val; *When* az adatlapja megnyílik; *Then* a doboz meg sem jelenik, és a kliens nem hív `similar`-t.

**AC-B11.** *Given* egy `merge`-hívás `409 stale` választ ad; *When* a kliens feldolgozza; *Then* a felhasználó az `ticket.merge.error.stale` üzenetet látja.

**AC-B12.** *Given* egy `merge`-hívás `422 original_is_duplicate` választ ad; *When* a kliens feldolgozza; *Then* a felhasználó az `ticket.merge.error.originalIsDuplicate` üzenetet látja.

### 6.4 AC-J — jogosultság

**AC-J1.** *Given* `dispatcher` szerepkör; *When* `GET .../similar`-t hív; *Then* a válasz nem `403`.

**AC-J2.** *Given* `field_worker` szerepkör; *When* `GET .../similar`-t hív; *Then* `403`.

**AC-J3.** *Given* `field_worker` szerepkör; *When* `POST .../merge`-t hív; *Then* `403` — minden egyéb előfeltétel teljesülése esetén is (kétrétegű védelem).

**AC-J4.** *Given* `field_worker` szerepkör; *When* egy adatlap megnyílik; *Then* a "Hasonló bejelentések" doboz nem renderelődik.

**AC-J5.** *Given* `manager` szerepkör; *When* `POST .../merge`-t hív; *Then* a válasz nem `403`.

**AC-J6.** *Given* egy `dispatcher` az A tenantban; *When* `GET .../similar`-t hív egy B tenant `Ticket`-id-jával; *Then* `404`.

---

## 7. Keresztmetszeti

**i18n.** Érdemi — a 4.7 kulcskészlete a `hu.json`-ba kerül; a felület tegez, a
szerver `reason`-kódot ad. A `Merged`-kulcs kettébontása a `10` 4.6-tal
egyeztetendő (8.6 VF-D2).

**Időzóna.** Az idő-jel (`createdAt`-különbség) időzóna-független; a doboz
relatív megjelenítése a meglévő minta szerint (alap Europe/Budapest). Nincs új
teendő.

**Teljesítmény.** Érdemi — a 3.1.5 technikai döntései fedik (bounding-box +
Haversine, `Ticket(status, createdAt)` index, 20-as limit). Pilot-méreten
arányos.

**Biztonság.** Nem érdemi a standard fölött — a kétrétegű jogosultság (3.5,
AC-J3) és a tenant-izoláció (3.6) a meglévő mintát követi.

---

## 8. Lezárás

### 8.1 Első kiadás (MVP) vs. következő iterációk

**Első kiadás — T0/pilot:**

- `GET /v1/tickets/{id}/similar` — a három-jeles heurisztika, a nem-értékelhető esetek, a nyitott-szűrés, az önkizárás, a rendezés és a 20-as limit.
- `POST /v1/tickets/{id}/merge` — a tranzakciós kétoldali összevonás, a hét előfeltétel `409`/`422`-besorolással, a concurrency-token, a kétoldali `Merged` `ActivityLog`.
- A "Hasonló bejelentések" doboz mint adatlap-widget — négyállapotú megjelenés, találat-sor, "Megnyitás"/"összevonás" akciók.
- Az összevonás-megerősítő párbeszéd, eredeti/duplikátum választással.
- A bejelentő-átkötés az `originalTicketId`-on keresztül.
- A duplikáció-specifikus i18n-kulcsok.

**Következő iterációk (listázva, nem specifikálva):**

- ML / kép-hasonlóság alapú felismerés — `20` 5.2, 18–24. hó.
- Konfigurálható heurisztika-küszöbök admin felületen — K-016, 6–12. hó.
- Az összevonás formális visszavonása mint állapotgép-átmenet — `02_globalis_allapotgep.md` NY-Á2 / Ü-4, 6–12. hó.
- Tömeges (batch) összevonás — `20` 5.2.

### 8.2 Feltételezések

1. A multi-tenancy modell zárt — SD-1 érvényes; a `Ticket` a Tenant DB-ben él, a `DbContext` tenant-izolált.
2. A `02_globalis_allapotgep.md` 3.7 a kész összevonás-átmenet; ez a feature a HTTP-szerződését és az előfeltétel-szigorítást adja, az átmenetet nem nyitja újra.
3. Az `Attachment`-feltöltés/-kiszolgálás külön feature — a `thumbnailRef` presigned-URL-feloldása az `Attachment`-feature mintája, itt fogyasztva.
4. A `Category`-fa pontosan kétszintű (K-036) — a gyökér-feloldás egylépéses `parentId`-felmenet.
5. A polgári bejelentés-adatlap-végpont a polgári adatigény-spec hatásköre (`NY-bej-1`); a duplikáció nem ad új polgári API-t.
6. TD-D2 — a kategória-jel `categoryId` → `citizenSuggestedCategoryId` forrás-feloldása K-032 *alkalmazási részlete*, nem felülírása. Ha egyik kategória-mező sincs, a jel nem értékelhető, és nincs gyanú.

### 8.3 Nyitott kérdések és megtartott hiányok

| # | Kérdés / hiány | Típus | Hatáskör |
|---|---|---|---|
| **NY-D1** | Az eredeti ügy státuszfrissítéseinek kézbesítése a duplikátum bejelentőjéhez (csatorna, időzítés). A TD-D1 az *adatot* garantálja; a kézbesítés-mechanika nem itt dől el | **Megtartott hiány** — nem blokkolja a feature gerincét (az adat-rögzítés teljes) | Polgári adatigény-spec / `Ticket`-spec |
| **NY-D2** | A duplikátum bejelentőjének polgári megjelenítése ("már jelezték, dolgozunk rajta"; az eredeti ügy státuszai) | Modulközi — *már átadott* (`20` 7.2) | Polgári mobilapp dokumentuma |
| **NY-D3** | A státusz-badge-leképezés (`Rejected`/`Duplicate` → polgári badge) | Modulközi — *már átadott* (`10` `NY-bej-4`) | Polgári mobilapp dokumentuma |
| **NY-D4** | Az összevonás formális visszavonása mint `Rejected → előző` állapotgép-átmenet — a pilotra Ü-4 kézi újranyitást rögzített | **Megtartott hiány** — tudatosan 6–12. hóra halasztva (Ü-4); a feature gerincét nem érinti | `20` jövő-iterációja / `02_globalis_allapotgep.md` NY-Á2 |

Egyik nyitott pont sem blokkolja a feature manager-oldali implementálását.

### 8.4 Hivatkozott dokumentumok

- Funkcionális: `20_duplikacio_v1.md`, `10_triage_flow_v1.md`, `05_jogosultsagok_v2.md`, `90_sitemap_v3.md`, `manager_felulet_atadas.md`
- Alapdokumentumok: `00_domain_model.md`, `02_globalis_allapotgep.md`, `01_kozos_mintak.md`, `00_terminologia.md`, `99_donesnaplo.md`
- Feature-spec (átfedés-elhatárolás): `10_bejelentes_lista_es_adatlap.md` v1.2
- Kanonikus: `kanonikus_donek.md` — K-016, K-032, K-033, K-034
- Meglévő minták: `project_backend` CLAUDE.md, `project_backend_client_angular` CLAUDE.md
- Polgári adatigény forrása: Flutter `screen_bejelentes_adatlap_1` / `_2` / `_2b`

### 8.5 Specifikációs döntések (a `99_donesnaplo.md`-be)

| Döntés | Tartalom |
|---|---|
| **TD-D1** | A bejelentő-átkötés hordozója: nincs `TicketSubscriber` entitás; az eredeti ügy értesítendői = `reporterId` ∪ a rá mutató duplikátumok `reporterId`-jai. Az `originalTicketId` beírása maga az átkötés |
| **TD-D2** | A heurisztika kategória-jele `categoryId` → `citizenSuggestedCategoryId` sorrendben oldódik fel; ha egyik sincs, a jel nem értékelhető. K-032 alkalmazási részlete |
| **TD-D3** | A `GET /v1/tickets/{id}/similar` nem-standard read-only heurisztika-végpont, nem `BaseController`-művelet |
| **TD-D4** | A `POST /v1/tickets/{id}/merge` nem-standard write-végpont; a kétoldali mellékhatás egyetlen tranzakcióban, atomi |
| **TD-D5** | A `merge` hét formalizált előfeltétele `409`/`422`-besorolással (a `02_allapotgep` 3.7-et a lánc-tiltással és a végállapot-tiltással szigorítva) |
| **TD-D6** | A `Merged` `ActivityLog` két sora; a `toValue` stabil `ticket:<id>` referencia (SD-30); az i18n-kulcs kettébontva (`asDuplicate`/`asOriginal`) |
| **TD-D7** | Az `originalTicketDisplayId` származtatott DTO-mező, nem tárolt domain-mező |
| Technikai | Távolság-számítás bounding-box + Haversine; gyökér-feloldás egylépéses `parentId`; `Ticket(status, createdAt)` index; küszöbök kódba égetve |

### 8.6 Visszacsorgó jelzések

| # | Jelzés | Cél-dokumentum |
|---|---|---|
| **VF-D1** | A `02_globalis_allapotgep.md` 3.7 előfeltétele bővítendő: az eredeti ügy nem lehet maga is duplikátum (lánc-tiltás) és nem lehet végállapotú (TD-D5) | `02_globalis_allapotgep.md` 3.7 |
| **VF-D2** | A `10` 4.6 egyetlen `ticket.activity.merged` kulcsa kettébontandó (`asDuplicate`/`asOriginal`) — a duplikátum- és az eredeti-oldali napló-bejegyzés eltérő mondatot kíván | `10_bejelentes_lista_es_adatlap.md` 4.6 |
| **VF-D3** | A `10` `NY-bej-6` ("a Hasonló bejelentések doboz tartalmi szerződése") lezárva — ez a feature-spec adja | `10_bejelentes_lista_es_adatlap.md` 8.4 |

### 8.7 Kísérő-dokumentumok frissítése

- `00_domain_model.md` 1.2.6 — két magyarázó megjegyzés-sor (2.2, 2.3). Új tárolt mező nincs.
- `99_donesnaplo.md` — a 8.5 döntései.
- `00_terminologia.md` — ellenőrzés (az "összevonás" / "duplikátum" / "eredeti ügy" terminusok a `20`-ból már bevezetettek).
- `CLAUDE.md` — nincs frissítés (a feature nem vezet be a `CLAUDE.md` általános szabályait érintő új mintát).

---

## Verziónapló

- **v1.0 (2026.05.19)** — Első kiadás. A duplikáció-szűrés és a
  bejelentés-összevonás fejlesztői specifikációja: a `GET .../similar`
  heurisztika-végpont és a `POST .../merge` összevonás-végpont teljes
  szerződése, a tranzakciós kétoldali mellékhatás, a hét formalizált
  `merge`-előfeltétel `409`/`422`-besorolással, a "Hasonló bejelentések" doboz
  mint adatlap-widget tartalmi szerződése, az összevonás-megerősítő párbeszéd,
  a duplikáció-specifikus i18n-kulcskészlet, 48 acceptance criterion. A feature
  hét specifikációs döntésre épül (TD-D1 — TD-D7) és a `02_globalis_allapotgep.md`
  3.7 kész átmenetére. Két megtartott hiány (NY-D1 kézbesítés-mechanika, NY-D4
  összevonás-visszavonás), négy nyitott kérdés, három visszacsorgó jelzés. A
  `10_bejelentes_lista_es_adatlap.md` `NY-bej-6` nyitott kérdése ezzel lezárva.
