# Pätevyyspiste API v1

Rajapinta koulutusten, moduulien, käyttäjien ja pätevyyksien hallintaan.
Suunniteltu automaatiolle ja tekoälyagenteille.

Perusosoite: `/api/v1`

---

## 1. Tunnistautuminen

Jokainen pyyntö tarvitsee API-avaimen otsakkeessa:

```
Authorization: Bearer wtk_xxxxxxxxxxxxxxxxxxxx
```

Vaihtoehtoisesti `X-Api-Key: wtk_...`.

**Ympäristö tulee avaimesta.** Et anna etkä voi antaa yritystunnusta missään pyynnössä.
Avain on sidottu yhteen ympäristöön, ja kaikki luku ja kirjoitus kohdistuu vain siihen.

Avaimen luo yrityksen ylläpitäjä hallintapaneelista: **Asetukset → Integraatiot → Rajapinta (API)**.
Rajapinta on oletuksena pois käytöstä, ja se aktivoidaan samasta paikasta.

### Oikeudet

Avaimella on yksi tai useampi oikeus:

| Oikeus | Mitä sallii |
|---|---|
| `read` | Kaiken ympäristön sisällön lukeminen. Aina mukana. |
| `content:write` | Koulutusten, moduulien, diojen ja kysymysten luonti ja muokkaus. |
| `users:write` | Käyttäjien ja ryhmien luonti ja muokkaus. |

Jos avaimelta puuttuu tarvittava oikeus, vastaus on **403** ja siinä kerrotaan mikä
oikeus puuttuu. Anna agentille vain `read` + `content:write`, ellei sen ole
tarkoitus hallita käyttäjätilejä.

---

## 2. Aloita tästä

Kutsu ensin:

```
GET /api/v1/me
```

Vastaus kertoo mihin ympäristöön avain osoittaa, mitä oikeuksia sillä on ja paljonko
sisältöä ympäristössä on:

```json
{
  "tenant": { "slug": "yritys", "name": "Yritys Oy" },
  "key": { "name": "Agentti", "prefix": "wtk_a1b2c3", "scopes": ["read", "content:write"] },
  "counts": { "users": 42, "courses": 3, "modules": 18, "groups": 4 },
  "docs": "/api/v1/docs"
}
```

---

## 3. Koulutuksen luonti yhdellä kutsulla

**Tämä on tavallisin tapa luoda koulutus.** Koko rakenne — koulutus, moduulit, diat ja
tietovisan kysymykset — syntyy yhdestä pyynnöstä.

```
POST /api/v1/courses/full
```

Runko:

```json
{
  "title": "Tietosuojan perusteet",
  "description": "Henkilötietojen käsittely arjessa.",
  "published": false,
  "completeWithinWeeks": 4,
  "modules": [
    {
      "title": "Mitä henkilötieto on",
      "icon": "ti-shield-lock",
      "passThreshold": 80,
      "slides": [
        {
          "heading": "Henkilötieto arjessa",
          "content": "<p>Henkilötieto on mikä tahansa tieto, josta henkilö voidaan tunnistaa.</p><ul><li>Nimi ja sähköposti</li><li>Sijaintitieto</li></ul>"
        },
        {
          "heading": "Milloin tietoa saa käsitellä",
          "content": "<p>Käsittelylle tarvitaan aina peruste.</p>"
        }
      ],
      "questions": [
        {
          "question": "Onko työsähköpostiosoite henkilötietoa?",
          "type": "boolean",
          "correctIndex": 0,
          "feedbackOk": "Kyllä — osoitteesta voi tunnistaa henkilön."
        },
        {
          "question": "Mitkä näistä ovat henkilötietoa?",
          "type": "multi",
          "options": ["Nimi", "Y-tunnus", "Kotiosoite", "Sään lämpötila"],
          "correctIndexes": [0, 2],
          "feedbackOk": "Nimi ja kotiosoite tunnistavat henkilön."
        }
      ]
    }
  ]
}
```

### Säännöt

- `title` on pakollinen. Jokainen moduuli tarvitsee `title`-kentän ja **vähintään yhden dian**.
- Kysymykset ovat vapaaehtoisia. Moduuli ilman kysymyksiä on pelkkää luettavaa.
- Enintään 50 moduulia yhdessä pyynnössä.
- `minScore` on **pistemäärä, ei prosentti**: se on todistukseen vaadittu yhteispistemäärä,
  ja koulutuksen enimmäispisteet ovat sen tietovisojen kysymysten määrä (kysymyksettömästä
  moduulista tulee 1 piste). `0` tarkoittaa, ettei pistevaatimusta ole.
- **Koulutus luodaan julkaisemattomana**, ellei annat `"published": true`. Julkaisematon
  koulutus ei näy henkilöstölle. Tämä on tarkoituksellista: automaatin tekemä koulutus
  kannattaa katsoa läpi ennen jakelua.
- **Koko pyyntö validoidaan ennen kirjoittamista.** Jos jokin kohta on kelvoton, mitään ei
  luoda ja virheviesti osoittaa tarkan kohdan, esim.
  `"modules[1].questions[0]: \"correctIndex\" on kokonaisluku väliltä 0–3"`.

Vastaus (201) sisältää luodun koulutuksen tunnisteineen sekä kentän `next`, joka kertoo
seuraavan järkevän toimenpiteen.

---

## 4. Diojen sisältö

Kenttä `content` on HTML:ää. Käytä yksinkertaisia elementtejä:

`<p>` `<ul>` `<ol>` `<li>` `<strong>` `<em>` `<h3>` `<blockquote>` `<a href>` `<img src>` `<table>`

Hyvän dian mitta on 40–120 sanaa. Pitkä sisältö kannattaa jakaa useaksi diaksi: oppija
etenee dia kerrallaan, ja moduulin tavoitepituus on 5–10 minuuttia.

---

## 5. Kysymystyypit

| `type` | Kentät | Huomiot |
|---|---|---|
| `single` (oletus) | `options` (≥2), `correctIndex` | Yksi oikea vastaus. |
| `multi` | `options` (≥2), `correctIndexes` (taulukko) | Useita oikeita. Kaikki eivät voi olla oikein. |
| `boolean` | `correctIndex` (0 = Tosi, 1 = Epätosi) | `options` täytetään automaattisesti. |

`feedbackOk` on palaute, joka näytetään oikean vastauksen jälkeen. Se on vapaaehtoinen
mutta suositeltava: se on paras kohta perustella, miksi vastaus on oikea.

`options` on tavallisesti merkkijonotaulukko. Jos haluat kertoa myös, miksi jokin
vaihtoehto on väärä, anna vaihtoehto objektina:
`{ "text": "Kotiosoite", "wrongFeedback": "Kotiosoite ei ole julkista tietoa." }`.
Luettaessa vaihtoehdot palautetaan aina merkkijonoina, ja selitteet omassa
`optionFeedback`-taulukossaan, jos niitä on annettu.

---

## 6. Reitit

### Orientaatio
| Metodi | Polku | Oikeus |
|---|---|---|
| GET | `/me` | read |
| GET | `/docs` | — (avoin) |
| GET | `/openapi.json` | — (avoin) |

### Koulutukset
| Metodi | Polku | Oikeus |
|---|---|---|
| GET | `/courses` | read |
| GET | `/courses/:id` | read |
| POST | `/courses` | content:write |
| POST | `/courses/full` | content:write |
| PATCH | `/courses/:id` | content:write |
| DELETE | `/courses/:id` | content:write |
| POST | `/courses/:id/groups` | content:write |

`GET /courses/:id` palauttaa koulutuksen kaikkine moduuleineen, dioineen ja
kysymyksineen — sama rakenne, jonka `POST /courses/full` ottaa vastaan.

`DELETE /courses/:id` poistaa koulutuksen mutta **jättää moduulit ympäristöön**, koska
sama moduuli voi olla käytössä toisessa koulutuksessa.

### Moduulit, diat ja kysymykset
| Metodi | Polku | Oikeus |
|---|---|---|
| GET | `/modules` | read |
| GET | `/modules/:id` | read |
| POST | `/courses/:id/modules` | content:write |
| PATCH | `/modules/:id` | content:write |
| DELETE | `/modules/:id` | content:write |
| POST | `/modules/:id/slides` | content:write |
| PATCH | `/slides/:id` | content:write |
| DELETE | `/slides/:id` | content:write |
| POST | `/modules/:id/questions` | content:write |
| DELETE | `/questions/:id` | content:write |

`DELETE /modules/:id` **arkistoi** moduulin eikä poista sitä. Suoritushistoria on
todiste koulutuksen läpikäynnistä, eikä sitä saa hävitä moduulin mukana.

### Käyttäjät ja ryhmät
| Metodi | Polku | Oikeus |
|---|---|---|
| GET | `/users` | read |
| POST | `/users` | users:write |
| PATCH | `/users/:id` | users:write |
| GET | `/groups` | read |
| POST | `/groups` | users:write |

`GET /users` tukee suodattimia `?status=active`, `?role=user`, `?email=...`.

`POST /users` ei ota salasanaa vastaan: se kulkisi pyynnön rungossa ja päätyisi
lokeihin. Käyttäjälle luodaan satunnainen salasana ja pakotetaan vaihto.

Anna `"sendInvite": true`, niin käyttäjälle lähtee kutsuviesti, jossa on linkki
oman salasanan asettamiseen (voimassa 7 vrk). Vastauksen `invited` kertoo,
lähtikö viesti. Ilman kutsua käyttäjä pääsee sisään vasta kirjautumissivun
"Unohditko salasanasi?" -linkin kautta.

Käyttäjämäärän rajat koskevat myös rajapintaa. Jos luonti ylittäisi tilatun paketin
käyttäjämäärän, vastaus on `402` ja mukana on `planUpgrade`-tarjous: nykyinen
käyttäjämäärä, ylittyvä raja, seuraava paketti ja sen hinta. Hinnan hyväksyy ihminen
hallintapaneelista (Asetukset → Tilaus), minkä jälkeen sama pyyntö menee läpi.

### Suoritukset ja pätevyydet
| Metodi | Polku | Oikeus |
|---|---|---|
| GET | `/completions` | read |
| GET | `/competencies` | read |
| GET | `/competencies/:id/records` | read |

`/completions` palauttaa moduulisuoritukset koulutuskohtaisesti: mukana ovat
`courseId` ja `courseTitle` (tyhjä, jos moduuli ei kuulu koulutukseen). Suodattimet
`userId`, `moduleId` ja `courseId`.

`/competencies?kind=qualification` = pätevyydet (kortit, luvat).
`/competencies?kind=training` = muualla suoritetut koulutukset.

Kirjauksen tila on `valid`, `expired` tai `valid_indefinitely`.

---

## 7. Sivutus

Listareitit tukevat parametreja `limit` (oletus 50, enintään 200) ja `offset`.
Vastauksessa on `data`, `limit`, `offset` ja listasta riippuen `total`.

---

## 8. Virheet

Kaikki virheet ovat JSONia ja sisältävät kentän `error` sekä linkin `docs`.

| Koodi | Tarkoittaa | Mitä tehdä |
|---|---|---|
| 400 | Pyyntö on virheellinen | Lue `error` — se osoittaa tarkan kentän. Korjaa ja yritä uudelleen. |
| 401 | Avain puuttuu, on väärä, mitätöity tai vanhentunut | Tarkista otsake. Pyydä uusi avain ylläpitäjältä. |
| 403 | Oikeus puuttuu tai rajapinta ei ole käytössä | Katso `keyScopes`. Ylläpitäjä luo avaimen laajemmin oikeuksin. |
| 404 | Kohdetta ei ole tässä ympäristössä | Tarkista tunniste `GET`-listauksella. |
| 409 | Ristiriita, esim. sähköposti on jo käytössä | Vastaus kertoo olemassa olevan tunnisteen. Käytä sitä. |
| 429 | Liikaa pyyntöjä | Odota `retryAfterSeconds` ja jatka. |
| 500 | Palvelinvirhe | Yritä uudelleen. Jos toistuu, ota yhteys ylläpitoon. |

Pyyntöraja on 300 pyyntöä minuutissa avainta kohden.

---

## 9. Jäljitettävyys

Jokainen rajapinnan kautta tehty muutos kirjautuu ympäristön tapahtumalokiin tekijänä
`api:<avaimen alkuosa>`. Ylläpitäjä näkee lokista, mikä avain muutti mitäkin ja milloin.

---

## 10. Esimerkki: agentin työnkulku

Toimeksianto: *"Tee 3 moduulin perehdytyskoulutus etätyön pelisäännöistä ja kohdista se Toimisto-ryhmälle."*

1. `GET /api/v1/me` — varmista ympäristö ja oikeudet.
2. `GET /api/v1/groups` — etsi ryhmän "Toimisto" tunniste.
3. `POST /api/v1/courses/full` — luo koulutus kolmella moduulilla, kussakin 2–4 diaa ja
   1–3 kysymystä. Jätä `published` pois, jotta ylläpitäjä katsoo sisällön ensin.
4. `POST /api/v1/courses/:id/groups` rungolla `{"groupIds": [12]}` — kohdista ryhmälle.
5. Kerro käyttäjälle koulutuksen tunniste ja se, että koulutus odottaa julkaisua:
   `PATCH /api/v1/courses/:id` rungolla `{"published": true}`.

Älä julkaise koulutusta oma-aloitteisesti, ellei sitä erikseen pyydetä.
