# Zamanlanmış görev ve kuyruk

"Her pazartesi sabah hatırlat", "süresi dolan kayıtları gece kapat", "bu raporu arka planda hazırla" gibi işler için.
Uygulama işi **beyan eder ve işleyicisini yazar**; işi Devoplus One tetikler. Uygulamada kendi zamanlayıcınızı ya da
arka plan döngünüzü yazmayın; uygulama yalnız bir istek geldiğinde çalışır.

| İhtiyaç | Kullanın |
|---|---|
| Belli zamanlarda tekrarlanan iş | Zamanlama: manifest `jobs.schedules[]` + `defineJobs({ schedules })` |
| Kullanıcının başlattığı, arka planda bitecek iş | Kuyruk: manifest `jobs.queues[]` + `enqueue(...)` + `defineJobs({ queues })` |
| İşi bir süre sonra yapmak (en çok 24 saat) | `enqueue(ad, gövde, { delaySeconds })` |

## 1. Manifest

```json
"jobs": {
  "schedules": [{ "name": "weekly-reminder", "cron": "0 9 * * 1", "timeZone": "Europe/Istanbul", "description": "Pazartesi 09:00 hatırlatma" }],
  "queues":    [{ "name": "send-report", "maxRetries": 3, "description": "Raporu hazırlayıp kaydeder" }]
}
```

- Ad: küçük harfle başlar; küçük harf, rakam ve tire; 2-40 karakter. En çok 10 zamanlama ve 10 kuyruk.
- `cron`: 5 alan; dakika saat ayın-günü ay haftanın-günü (`0 9 * * 1` = pazartesi 09:00, `*/15 8-18 * * mon-fri` = hafta
  içi mesai saatlerinde 15 dakikada bir). En sık aralık dakikada birdir. `?`, `L`, `W`, `#` desteklenmez.
- `timeZone`: verilmezse `Europe/Istanbul`. Kullanıcının saatiyle düşünün; platform çevirir.
- `maxRetries` (yalnız kuyruk): 0-10, varsayılan 3.
- `services` (isteğe bağlı): işin Devoplus'ta kullanacağı servisler (bkz. §4). Örnek: `"services": ["ai.use", "mail.execute"]`.
- Kurallar ve hata yolları: [manifest](/docs/manifest.md) içinde `jobs` bölümü.

## 2. İşleyiciler

```ts
import { OneError, defineJobs, enqueue, handleJobRequest, readContext, requireUser, type OneApp } from '@devoplus/one-sdk'

const jobs = defineJobs<Env>({
  schedules: {
    'weekly-reminder': async (job, env) => {
      // Kullanıcısız iş: ctx.user null. Devoplus servisi gerekiyorsa jobs.services + kurulum onayı (§4).
      await env.DB.prepare('INSERT OR IGNORE INTO reminders (job_id, created_at) VALUES (?, ?)').bind(job.id, job.scheduledTime).run()
    },
  },
  queues: {
    'send-report': async (job, env, ctx) => {
      const { reportId } = job.body as { reportId: number }
      if (!reportId) throw new OneError(422, 'APP-E-001', 'Rapor kimliği yok.') // 4xx: kalıcı hata, yeniden denenmez
      // ctx.user = iletiyi bırakan kullanıcı
      await env.DB.prepare('INSERT OR IGNORE INTO reports (job_id, report_id, owner) VALUES (?, ?, ?)').bind(job.id, reportId, ctx.user?.sub ?? null).run()
    },
  },
})

export default {
  async fetch(request, env, execution) {
    const job = handleJobRequest(request, env, execution, jobs) // platformun iş çağrısı; değilse null
    if (job) return job
    const ctx = readContext(request.headers)
    if (new URL(request.url).pathname === '/api/reports' && request.method === 'POST') {
      requireUser(ctx)
      const { id } = await enqueue('send-report', { reportId: 7 }) // 202 ve { id } döner
      return Response.json({ queued: true, id }, { status: 202 })
    }
    // ...
  },
} satisfies OneApp<Env>
```

## 3. Sonuç ve yeniden deneme

| İşleyici | Platform |
|---|---|
| Döner | İş tamam (200) |
| 4xx `OneError` fırlatır (ör. 422) | Kalıcı hata; **yeniden denenmez** (ör. biçimsiz ileti, artık geçersiz kayıt) |
| Başka hata fırlatır, zaman aşımı, sınır aşımı | 500; geri çekilmeyle yeniden denenir (30 sn'den başlayıp ikiye katlanır, en çok 1 saat): kuyrukta `maxRetries`, zamanlamada 3 kez |

⛔ **Aynı iş birden çok kez gelebilir** (yeniden deneme; nadiren aynı dakikanın tekrarı). `job.id` her denemede
**aynıdır**: kalıcı bir şey yazıyorsanız tabloda bu kimliği birincil anahtar yapın ve `INSERT OR IGNORE` kullanın.
E-posta gibi geri alınamaz bir işi iş kimliğiyle işaretlemeden göndermeyin.

## 4. Kimlik

- **Kuyruk işi** iletiyi bırakan kullanıcının kimliğini (`ctx.user.sub`) **bilgi olarak** taşır. Uygulama izinleri
  ve yönetici bilgisi işe **taşınmaz** (`hasPermission` her zaman `false`): izin kararını iletiyi bırakırken verin.
- **Zamanlanmış iş kullanıcısızdır** (`ctx.user` null).
- **Devoplus servisleri işte uygulamanın kimliğiyle çağrılır** (`ctx.mode === 'job'`), kişi adına değil. Kullanılabilen
  servisler yalnız şunlardır ve her biri manifest `jobs.services` **ve** `scopes` içinde beyan edilir: `mail.execute`
  (e-posta), `sms.execute` (SMS), `push.execute` (mobil bildirim), `dexa.read` (DEXA okuma) ve `ai.use` (yapay zeka).
  ⛔ **Beyan yetmez: arka plan işi servisleri kurulumda portal yöneticisinin onayıyla açılır.** Onaylanmamış servise
  çağrı 403 alır; geliştiriciye "bu servis kurulumda onaylanmamış, portal yöneticisinden onay isteyin" deyin.
- ⛔ **DEXA'ya yazma (ERP işlemi) arka plan işinde açılmaz.** Organizasyon şeması ve diğer servisler de işte yoktur.
- **DEXA okuma sorgusu kişiye bağlı bir kod istiyorsa** (`UserCode`, `CompanyCode`, `CostCenterCode`): işte oturum
  olmadığı için kodu **uygulama gönderir**. Bunun için kurulumda portal yöneticisinin **"kodları uygulama belirler"**
  onayı gerekir; onay yoksa gönderilen kod reddedilir. Kod gönderilmezse sorgu reddedilir (oturumdan çözülecek kişi
  yok). Kişi ve şirket kodu kurulumun portalına karşı doğrulanır; boş değer ERP'ye gitmez. `LangCode` gönderilmez.
- İşte "kendime gönder" yoktur: e-posta/SMS/bildirim alıcısını açıkça verin. Denetim kaydında aktör uygulamadır.
- Servis istemcisini belirteçsiz kurun: `new BackendClient({ baseUrl: env.API_BASE, authorization: null })`. Uygulamanın
  yazdığı `Authorization` başlığı işte Devoplus'a iletilmez.
- Kimliği iletinin gövdesine yazmayın; platform kimliği istekten kendisi alır. Başka kullanıcı adına ileti bırakılamaz.
- Vekalet ya da Yerine Görüntüle oturumunda, uygulama bu oturumlarda yazmaya açık değilse (`actingSessions: "read"`)
  kuyruğa ileti bırakılamaz (403).

## 5. Sınırlar

- Gövde JSON'a çevrilebilir olmalı ve en çok 64 KiB'dir; gecikme 0-86.400 saniye (24 saat).
- Beyan edilmemiş kuyruk adına ileti bırakılamaz (404). Manifest'ten çıkarılan iş yeni sürüm yayımlanınca durur.
- `/__one/` ile başlayan yollar platforma ayrılmıştır: bu yolda kendi ucunuzu yazmayın; internetten bu yola gelen
  istek uygulamaya ulaşmaz.
- Kaçırılan bir zamanlama sonradan telafi edilmez. Yaz saati geçişinde atlanan yerel saatteki zamanlama o gün çalışmaz.
- Yerelde (`one dev`) zamanlama tetiklenmez ve `enqueue` çalışmaz (503). Uygulamanın yerel adresi (`8788`) `/__one/`
  yolunu platformdaki gibi reddeder; işleyiciyi denemek için yerel çalışma ortamının iç adresine
  (`http://127.0.0.1:8789`, yani `--port` artı iki) `X-One-Auth: job` başlığıyla ve `X-One-App`, `X-One-Portal-Key`,
  `X-One-Portal` başlıklarıyla `POST /__one/jobs/<schedule|queue>/<ad>` gönderin, gövde `{ "id": "deneme-1", "body": ... }`.
  İşin Devoplus servis çağrıları yerelde taklit edilmez. Bu iç adres ad (Host) denetimi yapmaz ve başlıkları doğrulamaz;
  yalnız komut satırından deneme içindir, tarayıcıda açılmaz.
- Hata kodları: `ONE-SDK-E-070` (ad), `-071` (gövde JSON değil), `-072` (gövde büyük), `-073` (gecikme), `-074`
  (bırakılamadı), `-075` (iş bulunamadı), `-076` (çağrı gövdesi bozuk); platform tarafı `ONE-OUT-E-010` ile `-017` arası.
