# OTEP — Open Tracking Event Protocol

*Překlad pro pohodlí — anglická verze je závazná.*

> Stav: **Draft v0.1** · Otevřená, na dodavatelích nezávislá specifikace · Licence: otevřená (viz §10)

OTEP je **otevřený, na dodavatelích nezávislý protokol** pro životní cyklus libovolného sledovatelného předmětu.
Definuje jeden model událostí a jeden slovník stavů, takže sledovací události z libovolného
zdroje — doručení vlastní flotilou, kurýři třetích stran, přepravní štítky a další — lze
vyměňovat, chápat a promítat do mezinárodních standardů bez nutnosti znovu integrovat každou
stranu zvlášť.

Tento dokument je specifikace: struktura, pole, tabulky stavů, stavový
automat, mapování na externí standardy a pravidla shody pro vytvoření
kompatibilní implementace. Je nezávislá na implementaci — popisuje protokol, nikoli
vnitřní mechanismy konkrétního dodavatele. Klíčová slova RFC-2119 (MUST / SHOULD / MAY) jsou normativní.

## 1. Co OTEP řeší

Sledování je roztříštěné: každý dopravce pojmenovává pole a stavové kódy odlišně, každý
doručovací kanál hlásí ve svém vlastním tvaru a propojení s globálními standardy znamená
opakovanou integraci. OTEP dává producentům a konzumentům jeden společný
jazyk: producent vydá události OTEP jednou a každý konzument OTEP jim porozumí a
dokáže je promítnout do standardu, který potřebuje.

## 2. Návrhové principy

1. **Event-sourced.** Časová osa událostí je zdrojem pravdy; „aktuální stav“ je vždy
   projekce — stav nejnovější události.
2. **Co / Kdy / Kde / Proč.** Každá událost je formována kolem těchto čtyř dimenzí — tytéž
   dimenze sdílejí GS1 EPCIS, IATA ONE Record a UN/CEFACT — takže tyto standardy
   jsou výstupní projekce události OTEP, nikoli paralelní modely.
3. **Zdroje bez seznamu nejsou překážkou.** Zdroj, který vystavuje pouze aktuální stav (bez
   historie), je zpracován syntetizováním jedné události pro každou pozorovanou změnu (§5).
4. **Aditivní.** OTEP je vystaven vedle libovolného existujícího sledovacího API; jeho přijetí nikdy
   nevyžaduje zpětně nekompatibilní změnu toho, co konzumenti již používají.

## 3. Časová osa OTEP

Časová osa je obálka nesoucí sledovaný předmět a uspořádaný seznam událostí.

```jsonc
{
  "otep_version": "0.1",
  "profile": "parcel",                  // domain profile (§8)
  "subject": {
    "tracking_number": "SR123...",      // ≥1 identifier required
    "order_id": 12345,                  // optional
    "package_id": 67890,                // optional
    "external_tracking_number": "1Z...",// optional
    "gs1_sscc": "00...",                // optional — enables EPCIS epcList
    "piece_id": "..."                   // optional — enables ONE Record linkage
  },
  "current_status": "delivered",        // projection of the latest event
  "delivered": true,
  "events": [ /* OTEP events, §4 */ ]
}
```

### Událost OTEP

```jsonc
{
  // WHAT — carried once at the timeline level (subject above)

  // WHEN
  "occurred_at": "2026-06-10T09:30:00-04:00",  // event instant, ISO-8601 with offset
  "recorded_at": "2026-06-10T09:45:23-04:00",  // ingestion instant (optional)
  "time_type": "actual",                        // actual | estimated | scheduled

  // WHERE
  "location": {
    "name": "Toronto Hub",
    "code": "YYZ-2",
    "gln": "0614141000005",                     // GS1 GLN → EPCIS SGLN
    "lat": 43.6777, "lng": -79.6248,
    "country": "CA"                             // ISO 3166-1 alpha-2
  },

  // WHY / WHAT HAPPENED
  "status_code": "out_for_delivery",            // an OTEP status code (§4)
  "phase": "out_for_delivery",                  // derived from status_code
  "incident_reason": null,                       // an OTEP incident reason (§4) when exception

  // WHO
  "actor": { "type": "driver", "name": "Jane D.", "phone": "+1..." },

  // PROVENANCE
  "source": {
    "type": "self_delivery",                    // self_delivery | third_party_delivery | carrier_label
    "provider_id": null,
    "carrier_code": null,
    "external_event_code": null,                 // your raw status code (preserve it)
    "raw": { /* original payload */ }
  },

  // PROOF
  "pod": {
    "photos": [ { "url": "...", "content_base64": null } ],
    "signature": [ { "url": "...", "content_base64": null } ],
    "recipient": "John Smith"
  }
}
```

Každé pole kromě `subject`, `occurred_at`, `status_code` a `source` je volitelné —
částečné zdroje vyplní, co mají. Skeny v uzlu/hubu nastaví `location` na skenující zařízení.
Vysokofrekvenční GPS telemetrie NENÍ událostí OTEP; událost je vydána při změně stavu nebo
při skenu v uzlu.

## 4. Slovník

### 4.1 Stavové kódy

20 kanonických kódů životního cyklu. `phase` je vždy odvoditelná z kódu.

| code | phase | terminální | POD | význam |
|---|---|:--:|:--:|---|
| `information_submitted` | pre_shipment | | | informace o objednávce přijaty |
| `booking_confirmed` | pre_shipment | | | dopravce/rezervace potvrzena |
| `awaiting_pickup` | pre_shipment | | | připraveno k vyzvednutí |
| `out_for_pickup` | pickup | | | na cestě k vyzvednutí |
| `picked_up` | pickup | | ✓ | vyzvednuto od odesílatele |
| `pickup_failed` | exception | | | pokus o vyzvednutí selhal |
| `pickup_rescheduled` | exception | | | vyzvednutí bude opakováno |
| `received` | inbound | | | přijato v zařízení |
| `arrival_scan` | inbound | | | sken při příjezdu do uzlu |
| `in_transit` | transit | | | v pohybu |
| `package_outbound` | transit | | | opustilo zařízení |
| `removed_from_route` | exception | | | staženo z trasy |
| `route_cancelled` | exception | | | trasa zrušena |
| `out_for_delivery` | out_for_delivery | | | ve vozidle |
| `delivered` | delivered | ✓ | ✓ | doručeno příjemci |
| `delivery_failed` | exception | | | pokus o doručení selhal |
| `delivery_rescheduled` | exception | | | bude opakováno / znovu doručeno |
| `return_to_sender` | return | ✓ | | vrací se k odesílateli |
| `rejected_by_recipient` | return | ✓ | | příjemce odmítl |
| `cancelled` | return | ✓ | | objednávka zrušena |

### 4.2 Fáze

`pre_shipment` · `preparing` · `pickup` · `inbound` · `in_custody` · `transit` ·
`out_for_delivery` · `delivered` · `exception` · `return`. (`preparing` a `in_custody`
jsou vyhrazeny pro neparcelní profily — §8.)

### 4.3 Typy zdrojů a typy času

`source.type`: `self_delivery` · `third_party_delivery` · `carrier_label`.
`time_type`: `actual` · `estimated` · `scheduled` (výchozí `actual`).

### 4.4 Důvody incidentů

Když je událost ve fázi `exception`, SHOULD nést `incident_reason` z tohoto
normalizovaného slovníku:

- **Carrier:** `carrier_damaged_parcel`, `carrier_sorting_error`, `carrier_address_not_found`,
  `carrier_parcel_lost`, `carrier_not_enough_time`, `carrier_vehicle_issue`,
  `carrier_capacity_exceeded`, `carrier_mechanical_delay`
- **Retailer/shipper:** `retailer_cancelled`, `retailer_incorrect_data`, `retailer_not_ready`,
  `retailer_incorrect_parcel`, `retailer_incorrect_dimensions`, `retailer_packaging_issue`
- **Consignee:** `consignee_refused`, `consignee_business_closed`, `consignee_not_available`,
  `consignee_not_home`, `consignee_cancelled`, `consignee_verification_failed`,
  `consignee_incorrect_address`, `consignee_access_restricted`, `consignee_safe_place_unavailable`
- **Customs:** `customs_delay`, `customs_documentation`, `customs_duties_unpaid`,
  `customs_prohibited`, `customs_inspection`
- **Force majeure:** `weather_delay`, `natural_disaster`, `force_majeure`
- **Other:** `parcel_being_researched`, `security_issue`, `regulatory_hold`, `unknown`

## 5. Stavový automat a zdroje pouze se stavem

Fáze postupují vpřed; výjimky je přerušují a vracejí se zpět. Terminální stavy (`delivered`,
`return_to_sender`, `rejected_by_recipient`, `cancelled`) uzavírají předmět.

```
pre_shipment → preparing → pickup → inbound → in_custody → transit → out_for_delivery → delivered ✓
                                       └──────────── exception ──────────┘
                                       └──────────── return / cancelled ✓
```

Pravidla:
- Konzumenti MUST uspořádat události podle `occurred_at` a MUST tolerovat příchod mimo pořadí.
- Přechody do terminálního stavu jsou idempotentní; opakování jsou deduplikována.
- Po terminálním stavu producent MUST NOT vydat další události s výjimkou zdokumentovaného
  toku RMA / znovuotevření.
- Zdroj vystavující pouze aktuální stav MUST syntetizovat jednu událost pro každou pozorovanou změnu
  (se stabilním deduplikačním klíčem) místo vynechání historie. Postupem času se snímky kumulují
  do časové osy.

## 6. Mapování na externí standardy

Čtyři dimenze OTEP odpovídají pole po poli hlavním standardům, takže každý je
výstupní projekcí události OTEP.

| OTEP | GS1 EPCIS 2.0 | IATA ONE Record | UN/CEFACT |
|---|---|---|---|
| `occurred_at` (+offset) | `eventTime` + `eventTimeZoneOffset` | `eventDate` + `eventTimeType=Actual` | Event Date/Time |
| `recorded_at` | `recordTime` | recordedAt | — |
| subject (sscc/piece) | `epcList` (SSCC URN) | `linkedObject` → Piece/Shipment | Consignment |
| `location` (gln/lat/lng) | `readPoint` / `bizLocation` (SGLN) | `recordedAtLocation` → Location | Location |
| `status_code` | `bizStep` + `disposition` | `eventCode` | Transport status code |
| `actor` | sourceList / extension | Actor / Party | Party |
| `incident_reason` | disposition / ErrorDeclaration | event remark | Status reason code |

Hodnoty pro jednotlivé kódy (EPCIS CBV `bizStep`/`disposition`, ONE Record `eventCode`, kód UN/CEFACT)
jsou publikovány ve strojově čitelném číselníku. Kromě těchto mezinárodních standardů lze časovou osu
OTEP promítnout také do OpenTelemetry trasování, OGC SensorThings pozorování a
běžných obchodních platforem (AfterShip, Shopify, Amazon, Walmart, BigCommerce, Magento,
WooCommerce, Etsy).

> Spolehlivost: Hodnoty EPCIS CBV jsou stabilní standardní URN. Hodnoty kódů ONE Record a UN/CEFACT
> jsou nejlepší možnou volbou pro poslední míli a před externím použitím by měly být ověřeny vůči
> oficiálním seznamům kódů. „Doručeno příjemci“ nemá přesný CBV bizStep — používá se nejbližší
> odpovídající (`receiving` + `received`) nebo rozšiřující URN z uživatelského slovníku.

## 7. API OTEP

OTEP je konzumováno přes malé read-only HTTP rozhraní; všechny koncové body jsou veřejné.

| Sloveso | Cesta | Vrací |
|---|---|---|
| GET | `/api/v1/otep/trackings/{tracking_number}` | časovou osu pro sledovací číslo |
| GET | `/api/v1/otep/trackings/{tracking_number}/events` | pouze události |
| POST | `/api/v1/otep/trackings/batch` | mnoho sledovacích čísel v jednom volání |
| POST | `/api/v1/otep/validate` | kontrolu shody pro odeslanou časovou osu (§9) |

K dispozici je také GraphQL dotaz vystavující tutéž časovou osu.

### Vyjednávání obsahu

Tatáž časová osa je serializována do té reprezentace, kterou si vyžádáte, prostřednictvím
dotazového parametru `?format=` nebo profilu `Accept`:

| Požadavek | Reprezentace |
|---|---|
| `?format=otep` (výchozí) | nativní časová osa OTEP |
| `?format=epcis` | GS1 EPCIS 2.0 (JSON-LD `ObjectEvent`s) |
| `?format=onerecord` | IATA ONE Record (`LogisticsEvent`s) |
| `?format=uncefact` | UN/CEFACT transport status |
| `?format=otlp` | OpenTelemetry trasování |
| `?format=sensorthings` | OGC SensorThings pozorování |
| `?format=aftership` \| `shopify` \| `amazon` \| `walmart` \| `bigcommerce` \| `magento` \| `woocommerce` \| `etsy` | tvar sledování/plnění dané platformy |

Události, kterým nelze v projekci přiřadit kód, jsou přeskočeny a započítány (nikdy nejsou tiše
zahozeny).

## 8. Doménové profily

OTEP je obecný protokol, nikoli parcelní. Protokolová vrstva (obálka událostí, páteř fází,
stavový automat) je univerzální; konkrétní stavové kódy patří do **profilu** deklarovaného na
časové ose pomocí `profile`. Kódy v §4 jsou profil **`parcel`**. Další domény —
stěhování, doručování jídla, skladování a další — přidávají vlastní sady kódů pod svým profilem,
s jmenným prostorem `otep:<profile>:<code>`, přičemž každý se mapuje na tutéž páteř fází. Přidání profilu
je rozšíření, nikoli změna protokolu.

## 9. Specifikace shody (normativní)

Producent nebo konzument je **OTEP-konformní**, když každá událost, kterou vydá nebo přijme, splňuje
tyto tabulky a pravidla.

### 9.1 Obálka časové osy

| Pole | Typ | Pož. | Omezení |
|---|---|:--:|---|
| `otep_version` | string | MUST | semver, např. `0.1` |
| `profile` | string | MUST | registrovaný profil |
| `subject` | object | MUST | §9.2 |
| `current_status` | string\|null | SHOULD | stavový kód (§4) |
| `current_phase` | string\|null | SHOULD | MUST se rovnat fázi `current_status`, pokud jsou přítomny obě |
| `delivered` | boolean | SHOULD | `true` právě když `current_status` = `delivered` |
| `events` | array | MUST | objekty událostí (§9.3), seřaditelné podle `occurred_at` |

### 9.2 `subject`

Alespoň JEDEN z `tracking_number` / `order_id` / `package_id` MUST být přítomen.

| Pole | Typ | Omezení |
|---|---|---|
| `tracking_number` | string | neprázdné |
| `order_id` / `package_id` | integer\|null | |
| `external_tracking_number` | string\|null | |
| `gs1_sscc` | string\|null | 18 číslic |
| `piece_id` | string\|null | |

### 9.3 Objekt události

| # | Pole | Typ | Pož. | Omezení |
|---|---|---|:--:|---|
| 1 | `occurred_at` | string | MUST | ISO-8601 s offsetem |
| 2 | `recorded_at` | string\|null | SHOULD | ISO-8601 |
| 3 | `time_type` | string | MAY (výchozí `actual`) | `actual` \| `estimated` \| `scheduled` |
| 4 | `status_code` | string\|null | MUST¹ | kód v §4.1 |
| 5 | `phase` | string\|null | SHOULD | MUST se rovnat fázi `status_code` |
| 6 | `incident_reason` | string\|null | SHOULD² | důvod v §4.4 |
| 7 | `description` | string\|null | MAY | čitelné člověkem |
| 8 | `location` | object\|null | MAY | §9.4 |
| 9 | `actor` | object\|null | MAY | `{ type, name?, phone? }`; type ∈ {driver, operator, carrier, system} |
| 10 | `source` | object | MUST | §9.5 |
| 11 | `pod` | object\|null | MAY³ | `{ photos[], signature[], recipient? }` |

¹ Kódovaná událost MUST nést stavový kód z §4.1. Surový sken, který zatím nemůžete klasifikovat, MAY nastavit
`status_code = null`, ale MUST zachovat nativní kód v `source.external_event_code` a MUST
být započítán, nikdy nezahozen.
² Událost ve fázi `exception` SHOULD nést `incident_reason`.
³ Události se `status_code` ∈ {`delivered`, `picked_up`} SHOULD nést `pod`.

### 9.4 `location`

`name` (string) · `code` (string) · `gln` (GS1 GLN, 13 číslic) · `lat` / `lng` (WGS-84) ·
`country` (ISO 3166-1 alpha-2). Vše volitelné.

### 9.5 `source`

| Pole | Typ | Pož. | Omezení |
|---|---|:--:|---|
| `type` | string | MUST | `self_delivery` \| `third_party_delivery` \| `carrier_label` |
| `provider_id` | integer\|null | MAY | |
| `carrier_code` | string\|null | MAY | |
| `external_event_code` | string\|null | SHOULD⁴ | váš surový stavový kód |
| `raw` | object\|null | MAY | původní payload |

⁴ MUST být přítomen, když je `status_code` null, aby se nativní kód nikdy neztratil.

### 9.6 Pravidla

1. **Čas.** `occurred_at` MUST být zpracovatelný jako ISO-8601. Při příjmu normalizujte číselné epochy a `.NET /Date(ms)/`;
   tyto formy nevydávejte.
2. **Uspořádání / deduplikace.** Konzumenti MUST řadit podle `occurred_at`, tolerovat příchod mimo pořadí
   a deduplikovat podle (`subject`, `status_code`, `occurred_at`).
3. **Stavový automat.** Po terminálním stavu nevydávejte další události s výjimkou zdokumentovaného
   RMA / znovuotevření.
4. **Žádná tichá ztráta.** Události, které nelze kódovat, MUST být započítány, nikdy zahozeny.
5. **Odvození.** `phase`, `current_status`, `current_phase`, `delivered` jsou projekce — pokud
   jsou přítomny, MUST být konzistentní s časovou osou událostí.

### 9.7 Úrovně shody a validace

- **Level 1** — vydává obálku (§9.1), události s požadovanými poli (§9.3), platné stavové
  kódy (§4.1), platné fáze (§4.2) a respektuje stavový automat (§5).
- **Level 2** — navíc vydává alespoň jednu projekci do externího standardu (§6) a, kde
  je to relevantní, kódy specifické pro profil (§8).

**Ověřte svůj výstup** odesláním časové osy na `POST /api/v1/otep/validate`. Jakékoli
`errors` považujte za blokující; řešte `warnings`. Pro offline validaci jsou publikovány strojově
čitelné JSON Schema a kompletní číselník (každý stavový kód, fáze a externí mapování).

## 10. Otevřenost a správa

OTEP je **otevřená specifikace**, kterou může implementovat kdokoli zdarma.

- **Normativní vs informativní.** Normativní: obálka událostí, páteř fází, slovník stavů,
  stavový automat a mapování polí na externí standardy. To, jak implementátor naváže OTEP na své vlastní
  interní systémy, je jeho věc a je mimo rozsah tohoto dokumentu.
- **Stabilní identifikátory.** Kódy jsou adresovány jako `otep:<profile>:<code>`, fáze jako
  `otep:phase:<name>`. Jakmile je identifikátor publikován ve vydané verzi, jeho význam je neměnný.
- **Verzování.** Sémantické verzování. Přidání kódů/profilů je MINOR (zpětně kompatibilní)
  změna; změna významu existujícího kódu je MAJOR změna a SHOULD jí být vyhnuto. Verze
  protokolu putuje s každou časovou osou (`otep_version`).
- **Rozšíření.** Nové profily a kódy jsou navrhovány vůči této specifikaci, nikoli forkovány, takže
  nezávislí implementátoři konvergují. Experimentální kódy MAY používat prefix `x-`
  (`otep:parcel:x-my_code`), dokud nejsou registrovány.
- **Licence.** Specifikace má být vydána pod otevřenou licencí — bude doplněno, čeká
  na schválení.

## 11. Interoperabilita dodavatelů — přineste si vlastní standard

OTEP vítá další dodavatele, aby přinesli svůj vlastní standard sledovacích událostí, aby s ním OTEP mohl
interoperovat, v obou směrech:

- **Projekce OTEP → váš standard.** Definujte mapování z časové osy OTEP do vašeho formátu,
  s opětovným využitím slovníku stavů OTEP. Je to čistá transformace — vstup časová osa, výstup vaše
  struktura — takže mapování se napíše jednou a každý producent OTEP může vydávat váš formát.
- **Mapování váš standard → OTEP.** Poskytněte převodní tabulku z vašeho slovníku stavů na kódy OTEP
  (§4) plus, pokud je třeba, profil (§8). Vaše surové kódy jsou zachovány v
  `source.external_event_code`; nenamapované kódy jsou započítány, nikdy zahozeny.

I když nemůžete přijmout OTEP přímo, jste vítáni přidat jediný normalizovaný `otep_status`
do svých vlastních API odpovědí a sdílet své kódy sledovacích událostí pro převodní mapování. Navrhujte
mapování vůči této specifikaci (místo forkování), aby implementátoři konvergovali; nové formáty se zapojí do
stejného vyjednávání obsahu `?format=` a nikdy nenaruší existujícího konzumenta.
