# OTEP — Open Tracking Event Protocol

*Prevod radi praktičnosti — merodavna je verzija na engleskom jeziku.*

> Status: **Nacrt v0.1** · Otvorena, vendor-neutralna specifikacija · Licenca: otvorena (vidi §10)

OTEP je **otvoren, vendor-neutralan protokol** za životni ciklus bilo kog predmeta koji se može pratiti.
Definiše jedan model događaja i jedan rečnik statusa tako da se događaji praćenja iz bilo kog
izvora — dostava sopstvenom flotom, kuriri trećih strana, oznake prevoznika i šire — mogu
razmenjivati, razumeti i projektovati na međunarodne standarde bez ponovne integracije za
svaku stranu.

Ovaj dokument je specifikacija: struktura, polja, tabele statusa, automat stanja,
mapiranja na spoljne standarde i pravila usaglašenosti za izgradnju
usaglašene implementacije. Nezavisan je od implementacije — opisuje protokol, a ne
interne mehanizme bilo kog konkretnog vendora. Ključne reči RFC-2119 (MUST / SHOULD / MAY) su normativne.

## 1. Šta OTEP rešava

Praćenje je fragmentisano: svaki prevoznik drugačije imenuje polja i statusne kodove, svaki
kanal dostave izveštava u sopstvenom obliku, a povezivanje sa globalnim standardima znači
ponovnu integraciju iznova i iznova. OTEP daje proizvođačima i potrošačima jedan zajednički
jezik: proizvođač jednom emituje OTEP događaje, a svaki OTEP potrošač ih razume i
može ih projektovati na standard koji mu je potreban.

## 2. Principi dizajna

1. **Zasnovano na događajima.** Vremenska linija događaja je izvor istine; „trenutni status“ je uvek
   projekcija — status poslednjeg događaja.
2. **Šta / Kada / Gde / Zašto.** Svaki događaj je oblikovan oko ove četiri dimenzije — iste
   dimenzije koje dele GS1 EPCIS, IATA ONE Record i UN/CEFACT — tako da su ti standardi
   izlazne projekcije OTEP događaja, a ne paralelni modeli.
3. **Izvori bez liste nisu prepreka.** Izvor koji izlaže samo trenutni status (bez
   istorije) obrađuje se sintetizovanjem jednog događaja po posmatranoj promeni (§5).
4. **Aditivno.** OTEP se izlaže uporedo sa bilo kojim postojećim API-jem za praćenje; njegovo usvajanje nikada ne
   zahteva promenu koja narušava ono što potrošači već koriste.

## 3. OTEP vremenska linija

Vremenska linija je omotač koji nosi praćeni predmet i uređenu listu događaja.

```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 */ ]
}
```

### OTEP događaj

```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"
  }
}
```

Svako polje osim `subject`, `occurred_at`, `status_code` i `source` je opciono —
delimični izvori popunjavaju ono što imaju. Skeniranja na čvorovima/habovima postavljaju `location` na objekat skeniranja.
Visokofrekventna GPS telemetrija NIJE OTEP događaj; događaj se emituje pri promeni statusa ili
skeniranju na čvoru.

## 4. Rečnik

### 4.1 Statusni kodovi

20 kanonskih kodova životnog ciklusa. `phase` se uvek može izvesti iz koda.

| code | phase | terminalno | POD | značenje |
|---|---|:--:|:--:|---|
| `information_submitted` | pre_shipment | | | informacije o porudžbini primljene |
| `booking_confirmed` | pre_shipment | | | prevoznik/rezervacija potvrđen |
| `awaiting_pickup` | pre_shipment | | | spremno za preuzimanje |
| `out_for_pickup` | pickup | | | na putu radi preuzimanja |
| `picked_up` | pickup | | ✓ | preuzeto od pošiljaoca |
| `pickup_failed` | exception | | | pokušaj preuzimanja neuspešan |
| `pickup_rescheduled` | exception | | | preuzimanje će se ponoviti |
| `received` | inbound | | | primljeno u objektu |
| `arrival_scan` | inbound | | | skeniranje dolaska na čvoru |
| `in_transit` | transit | | | u kretanju |
| `package_outbound` | transit | | | napustilo objekat |
| `removed_from_route` | exception | | | uklonjeno sa rute |
| `route_cancelled` | exception | | | ruta otkazana |
| `out_for_delivery` | out_for_delivery | | | u vozilu |
| `delivered` | delivered | ✓ | ✓ | isporučeno primaocu |
| `delivery_failed` | exception | | | pokušaj isporuke neuspešan |
| `delivery_rescheduled` | exception | | | ponoviće se / ponovna isporuka |
| `return_to_sender` | return | ✓ | | vraćanje na poreklo |
| `rejected_by_recipient` | return | ✓ | | primalac odbio |
| `cancelled` | return | ✓ | | porudžbina otkazana |

### 4.2 Faze

`pre_shipment` · `preparing` · `pickup` · `inbound` · `in_custody` · `transit` ·
`out_for_delivery` · `delivered` · `exception` · `return`. (`preparing` i `in_custody`
rezervisani su za neparcel profile — §8.)

### 4.3 Tipovi izvora i tipovi vremena

`source.type`: `self_delivery` · `third_party_delivery` · `carrier_label`.
`time_type`: `actual` · `estimated` · `scheduled` (podrazumevano `actual`).

### 4.4 Razlozi incidenata

Kada je događaj u fazi `exception`, SHOULD nositi `incident_reason` iz ovog
normalizovanog rečnika:

- **Prevoznik:** `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`
- **Trgovac/pošiljalac:** `retailer_cancelled`, `retailer_incorrect_data`, `retailer_not_ready`,
  `retailer_incorrect_parcel`, `retailer_incorrect_dimensions`, `retailer_packaging_issue`
- **Primalac:** `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`
- **Carina:** `customs_delay`, `customs_documentation`, `customs_duties_unpaid`,
  `customs_prohibited`, `customs_inspection`
- **Viša sila:** `weather_delay`, `natural_disaster`, `force_majeure`
- **Ostalo:** `parcel_being_researched`, `security_issue`, `regulatory_hold`, `unknown`

## 5. Automat stanja i izvori samo sa statusom

Faze napreduju unapred; izuzeci ih prekidaju i vraćaju nazad. Terminalna stanja (`delivered`,
`return_to_sender`, `rejected_by_recipient`, `cancelled`) zatvaraju predmet.

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

Pravila:
- Potrošači MUST sortirati događaje po `occurred_at` i MUST tolerisati dolazak van redosleda.
- Prelazi u terminalno stanje su idempotentni; ponavljanja se deduplikuju.
- Nakon terminalnog statusa, proizvođač MUST NOT emitovati dalje događaje osim dokumentovanog
  RMA / re-open toka.
- Izvor koji izlaže samo trenutni status MUST sintetizovati jedan događaj po posmatranoj promeni
  (sa stabilnim ključem za deduplikaciju) umesto da izostavi istoriju. Vremenom se snimci nagomilavaju
  u vremensku liniju.

## 6. Mapiranja na spoljne standarde

OTEP-ove četiri dimenzije poklapaju se polje-za-polje sa glavnim standardima, tako da je svaki
izlazna projekcija OTEP događaja.

| 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 | — |
| subjekat (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 |

Vrednosti po kodu (EPCIS CBV `bizStep`/`disposition`, ONE Record `eventCode`, UN/CEFACT kod)
objavljene su u mašinski čitljivom kodeksu. Pored ovih međunarodnih standarda, OTEP
vremenska linija se takođe može projektovati na OpenTelemetry tragove, OGC SensorThings opservacije i
uobičajene komercijalne platforme (AfterShip, Shopify, Amazon, Walmart, BigCommerce, Magento,
WooCommerce, Etsy).

> Pouzdanost: EPCIS CBV vrednosti su stabilni standardni URN-ovi. ONE Record i UN/CEFACT kodne vrednosti
> najbolje su prilagođene poslednjoj milji i treba ih proveriti u odnosu na zvanične kodne liste pre
> spoljne upotrebe. „Isporučeno primaocu“ nema tačan CBV bizStep — koristi se najbliže rešenje
> (`receiving` + `received`), ili URN ekstenzije korisničkog rečnika.

## 7. OTEP API

OTEP se konzumira preko male HTTP površine samo za čitanje; sve krajnje tačke su javne.

| Verb | Path | Vraća |
|---|---|---|
| GET | `/api/v1/otep/trackings/{tracking_number}` | vremensku liniju za broj za praćenje |
| GET | `/api/v1/otep/trackings/{tracking_number}/events` | samo događaje |
| POST | `/api/v1/otep/trackings/batch` | mnogo brojeva za praćenje u jednom pozivu |
| POST | `/api/v1/otep/validate` | proveru usaglašenosti za poslatu vremensku liniju (§9) |

Dostupan je i GraphQL upit koji izlaže istu vremensku liniju.

### Pregovaranje o sadržaju

Ista vremenska linija se serijalizuje u bilo koju reprezentaciju koju zatražite, preko `?format=`
upitnog parametra ili `Accept` profila:

| Zahtev | Reprezentacija |
|---|---|
| `?format=otep` (podrazumevano) | izvorna OTEP vremenska linija |
| `?format=epcis` | GS1 EPCIS 2.0 (JSON-LD `ObjectEvent`s) |
| `?format=onerecord` | IATA ONE Record (`LogisticsEvent`s) |
| `?format=uncefact` | UN/CEFACT transportni status |
| `?format=otlp` | OpenTelemetry tragovi |
| `?format=sensorthings` | OGC SensorThings opservacije |
| `?format=aftership` \| `shopify` \| `amazon` \| `walmart` \| `bigcommerce` \| `magento` \| `woocommerce` \| `etsy` | oblik praćenja/ispunjenja platforme |

Događaji kojima se ne može dodeliti kod u projekciji se preskaču i broje (nikada se tiho ne
odbacuju).

## 8. Domenski profili

OTEP je opšti protokol, a ne protokol za pakete. Sloj protokola (omotač događaja, kičma faza,
automat stanja) je univerzalan; konkretni statusni kodovi pripadaju **profilu** deklarisanom na
vremenskoj liniji preko `profile`. Kodovi u §4 su profil **`parcel`**. Drugi domeni —
selidbe, dostava hrane, skladištenje i šire — dodaju svoje skupove kodova pod svojim profilom,
sa prostorom imena `otep:<profile>:<code>`, svaki mapiran na istu kičmu faza. Dodavanje profila
je ekstenzija, a ne promena protokola.

## 9. Specifikacija usaglašenosti (normativno)

Proizvođač ili potrošač je **OTEP-usaglašen** kada svaki događaj koji emituje ili prihvata zadovoljava
ove tabele i pravila.

### 9.1 Omotač vremenske linije

| Polje | Tip | Obav. | Ograničenja |
|---|---|:--:|---|
| `otep_version` | string | MUST | semver, npr. `0.1` |
| `profile` | string | MUST | registrovan profil |
| `subject` | object | MUST | §9.2 |
| `current_status` | string\|null | SHOULD | statusni kod (§4) |
| `current_phase` | string\|null | SHOULD | MUST biti jednako fazi koda `current_status` ako su oba prisutna |
| `delivered` | boolean | SHOULD | `true` ako i samo ako je `current_status` = `delivered` |
| `events` | array | MUST | objekti događaja (§9.3), sortirljivi po `occurred_at` |

### 9.2 `subject`

Najmanje JEDNO od `tracking_number` / `order_id` / `package_id` MUST biti prisutno.

| Polje | Tip | Ograničenja |
|---|---|---|
| `tracking_number` | string | nije prazno |
| `order_id` / `package_id` | integer\|null | |
| `external_tracking_number` | string\|null | |
| `gs1_sscc` | string\|null | 18 cifara |
| `piece_id` | string\|null | |

### 9.3 Objekat događaja

| # | Polje | Tip | Obav. | Ograničenja |
|---|---|---|:--:|---|
| 1 | `occurred_at` | string | MUST | ISO-8601 sa ofsetom |
| 2 | `recorded_at` | string\|null | SHOULD | ISO-8601 |
| 3 | `time_type` | string | MAY (podrazumevano `actual`) | `actual` \| `estimated` \| `scheduled` |
| 4 | `status_code` | string\|null | MUST¹ | kod iz §4.1 |
| 5 | `phase` | string\|null | SHOULD | MUST biti jednako fazi koda `status_code` |
| 6 | `incident_reason` | string\|null | SHOULD² | razlog iz §4.4 |
| 7 | `description` | string\|null | MAY | čitljivo za čoveka |
| 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? }` |

¹ Kodirani događaj MUST nositi statusni kod iz §4.1. Sirovo skeniranje koje još ne možete klasifikovati MAY postaviti
`status_code = null`, ali MUST sačuvati izvorni kod u `source.external_event_code` i MUST
biti izbrojano, nikada odbačeno.
² Događaj u fazi `exception` SHOULD nositi `incident_reason`.
³ Događaji sa `status_code` ∈ {`delivered`, `picked_up`} SHOULD nositi `pod`.

### 9.4 `location`

`name` (string) · `code` (string) · `gln` (GS1 GLN, 13 cifara) · `lat` / `lng` (WGS-84) ·
`country` (ISO 3166-1 alpha-2). Sve opciono.

### 9.5 `source`

| Polje | Tip | Obav. | Ograničenja |
|---|---|:--:|---|
| `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⁴ | vaš sirovi statusni kod |
| `raw` | object\|null | MAY | originalni payload |

⁴ MUST biti prisutno kada je `status_code` null, tako da se izvorni kod nikada ne izgubi.

### 9.6 Pravila

1. **Vreme.** `occurred_at` MUST se parsirati kao ISO-8601. Normalizujte numeričke epohe i `.NET /Date(ms)/`
   pri unosu; nemojte emitovati te oblike.
2. **Redosled / dedup.** Potrošači MUST sortirati po `occurred_at`, tolerisati dolazak van redosleda
   i deduplikovati po (`subject`, `status_code`, `occurred_at`).
3. **Automat stanja.** Nakon terminalnog statusa, nemojte emitovati dalje događaje osim dokumentovanog
   RMA / re-open.
4. **Bez tihog gubitka.** Događaji koji se ne mogu kodirati MUST biti izbrojani, nikada odbačeni.
5. **Izvođenje.** `phase`, `current_status`, `current_phase`, `delivered` su projekcije — ako su
   prisutni MUST biti konzistentni sa vremenskom linijom događaja.

### 9.7 Nivoi usaglašenosti i validacija

- **Nivo 1** — emituje omotač (§9.1), događaje sa obaveznim poljima (§9.3), važeće statusne
  kodove (§4.1), važeće faze (§4.2) i poštuje automat stanja (§5).
- **Nivo 2** — dodatno emituje najmanje jednu projekciju na spoljni standard (§6) i, gde je
  primenljivo, kodove specifične za profil (§8).

**Proverite svoj izlaz** slanjem vremenske linije na `POST /api/v1/otep/validate`. Tretirajte svaku
grešku (`errors`) kao blokirajuću; rešite upozorenja (`warnings`). Mašinski čitljiva JSON Schema i kompletan
kodeks (svaki statusni kod, faza i spoljno mapiranje) objavljeni su za oflajn validaciju.

## 10. Otvorenost i upravljanje

OTEP je **otvorena specifikacija**, besplatna za implementaciju bilo kojoj strani.

- **Normativno vs informativno.** Normativno: omotač događaja, kičma faza, rečnik statusa,
  automat stanja i mapiranja polja na spoljne standarde. Način na koji implementator povezuje OTEP sa sopstvenim
  internim sistemima njegova je stvar i van obima je ovog dokumenta.
- **Stabilni identifikatori.** Kodovi se adresiraju kao `otep:<profile>:<code>`, faze kao
  `otep:phase:<name>`. Jednom objavljeno u izdatoj verziji, značenje identifikatora je nepromenljivo.
- **Verzionisanje.** Semantičko verzionisanje. Dodavanje kodova/profila je MINOR (kompatibilna unazad)
  promena; menjanje značenja postojećeg koda je MAJOR promena i SHOULD se izbegavati. Verzija
  protokola putuje sa svakom vremenskom linijom (`otep_version`).
- **Ekstenzija.** Novi profili i kodovi predlažu se u odnosu na ovu specifikaciju umesto forkovanja, tako da se
  nezavisni implementatori približavaju. Eksperimentalni kodovi MAY koristiti prefiks `x-`
  (`otep:parcel:x-my_code`) dok se ne registruju.
- **Licenca.** Specifikacija je namenjena izdavanju pod otvorenom licencom — TBD,
  do potvrde.

## 11. Interoperabilnost vendora — donesite svoj standard

OTEP poziva druge vendore da donesu sopstveni standard događaja praćenja kako bi OTEP mogao da interoperira
sa njim, u oba smera:

- **Projektujte OTEP → vaš standard.** Definišite mapiranje sa OTEP vremenske linije na vaš format,
  ponovo koristeći OTEP rečnik statusa. To je čista transformacija — vremenska linija ulazi, vaša struktura
  izlazi — pa se mapiranje piše jednom i svaki OTEP proizvođač može emitovati vaš format.
- **Mapirajte vaš standard → OTEP.** Obezbedite ukrštanje sa vašeg rečnika statusa na OTEP
  kodove (§4) plus, ako je potrebno, profil (§8). Vaši sirovi kodovi se čuvaju u
  `source.external_event_code`; nemapirani kodovi se broje, nikada ne odbacuju.

Čak i ako ne možete direktno usvojiti OTEP, dobrodošli ste da dodate jedan normalizovani `otep_status`
sopstvenim API odgovorima i da podelite svoje kodove događaja praćenja za ukršteno mapiranje. Predložite
mapiranja u odnosu na ovu specifikaciju (umesto forkovanja) kako bi se implementatori približili; novi formati se priključuju
istom `?format=` pregovaranju o sadržaju i nikada ne narušavaju postojećeg potrošača.
