# Aksiyonlar (Actions)

> Panel yerleşimleri, iframe ve API aksiyonları, imzalı istek/yanıt biçimi ve aksiyonları yerelde test etme.
>
> Kaynak: https://tecof-app-dev.vercel.app/app-actions · Bölüm: Uygulama geliştirme · Son güncelleme: 2018-10-20

Aksiyonlar, uygulamanızın Tecof panelindeki sipariş, ürün ve müşteri ekranlarına kendi işlemlerini eklemesini sağlar. Örneğin bir muhasebe uygulaması sipariş detayına "Fatura kes" düğmesi, sipariş listesine "Seçilenleri faturala" toplu işlemi ekleyebilir.

İki tür aksiyon vardır:

| Tür | Ne olur | Ne zaman |
| :--- | :--- | :--- |
| `iframe` | Panel, uygulamanızın bir sayfasını modal ya da çekmece içinde açar | Kullanıcıdan girdi almak, önizleme göstermek gerekiyorsa |
| `api` | Panel Tecof'a çalıştırma isteği gönderir; Tecof uygulamanıza imzalı bir `POST` atar ve yanıtınızı kullanıcıya bildirim olarak gösterir | Tek tıkla biten işlemler, toplu işlemler |

---

## Yerleşimler

| `placement` | Ekran | Bağlam (`ids`) | Gerekli kapsam |
| :--- | :--- | :--- | :--- |
| `order.detail` | Sipariş detayı, başlıktaki "Uygulamalar" menüsü | `[orderId]` | `orders:read` |
| `order.list` | Sipariş listesi, toplu işlem menüsü | Seçili sipariş kimlikleri (≤ 50) | `orders:read` |
| `product.detail` | Ürün detayı | `[productId]` | `products:read` |
| `product.list` | Ürün listesi, toplu işlem menüsü | Seçili ürün kimlikleri (≤ 50) | `products:read` |
| `customer.detail` | Müşteri detayı | `[customerId]` | `customers:read` |
| `customer.list` | Müşteri listesi, toplu işlem menüsü | Seçili müşteri kimlikleri (≤ 50) | `customers:read` |

Aksiyon panelde yalnız **aktif** ve yerleşimin gerektirdiği kapsamı **verilmiş** kurulumlarda listelenir.

---

## Tanımlama

Aksiyonlar `tecof.app.json` içindeki `actions` dizisinde tanımlanır (en fazla 12):

```json
{
  "actions": [
    {
      "key": "create-invoice",
      "type": "iframe",
      "placement": "order.detail",
      "title": { "tr": "İşbaşı'nda fatura kes", "en": "Create İşbaşı invoice" },
      "path": "/actions/create-invoice",
      "presentation": "modal"
    },
    {
      "key": "bulk-invoice",
      "type": "api",
      "placement": "order.list",
      "title": { "tr": "Seçilenleri faturala", "en": "Invoice selected" },
      "path": "/api/actions/bulk-invoice",
      "confirm": {
        "tr": "Seçili siparişler için fatura kesilsin mi?",
        "en": "Create invoices for selected orders?"
      }
    }
  ]
}
```

| Alan | Kural |
| :--- | :--- |
| `key` | `^[a-z][a-z0-9-]{1,39}$`; uygulama içinde tekil |
| `type` | `iframe` ya da `api` |
| `placement` | Yukarıdaki tablodan biri |
| `title` | `{ tr, en }`; panel diline göre gösterilir |
| `path` | `appUrl`'e göre, `/` ile başlar |
| `presentation` | Yalnız `iframe`: `modal` (varsayılan) ya da `drawer` |
| `confirm` | Yalnız `api`, isteğe bağlı: `{ tr, en }` metniyle panel çalıştırmadan önce onay sorar |

Değişikliği `tecof app config push` ile gönderin.

---

## Panel Tarafında Akış

```mermaid
sequenceDiagram
    participant U as Kullanıcı (panel)
    participant P as Tecof Paneli / API
    participant A as Uygulamanız

    P->>P: GET /api/merchant/apps/actions?placement=order.list
    U->>P: Aksiyonu seçer (ve onaylar)
    P->>P: POST /api/merchant/apps/actions/run { installationId, actionKey, placement, ids }
    alt type = iframe
        P->>A: iframe: launch URL + actionRunId, actionKey, placement, ids
        A->>P: App Bridge close({ refresh, message, kind })
    else type = api
        P->>A: İmzalı POST (15 sn zaman aşımı)
        A-->>P: { ok, message, kind, redirectUrl, refresh }
        P->>U: Bildirim / yenileme / yönlendirme
    end
```

Her çalıştırma bir kayıt (`runId`) üretir. Geliştirici API'sinden geçmişi izleyebilirsiniz: `GET /api/developer/apps/:id/action-runs`.

---

## iframe Aksiyonları

Panel aksiyon penceresini, uygulamanızın açılış (launch) adresine şu parametreleri ekleyerek açar:

```text
https://isbasi.apps.tecof.site/actions/create-invoice?installationId=…&merchantId=…&locale=tr&embedded=1&host=…&session=…&actionRunId=…&actionKey=create-invoice&placement=order.detail&ids=6720bb00cc11dd22ee33ff44
```

`ids` virgülle ayrılmış kimliklerdir. Aynı bilgi App Bridge bağlamında `context.action = { runId, key, placement, ids }` olarak da gelir. İşiniz bitince `close` ile pencereyi kapatın; `refresh: true` paneldeki ekranı yeniler.

```tsx
// app/actions/create-invoice/page.tsx
"use client";

import { useState } from "react";
import { useAppBridge, useAppContext, useSessionFetch } from "@tecof/app-bridge/react";

export default function CreateInvoiceAction() {
  const bridge = useAppBridge();
  const context = useAppContext();
  const sessionFetch = useSessionFetch();
  const [busy, setBusy] = useState(false);

  const orderId = context.action?.ids[0];

  async function createInvoice() {
    if (!orderId) return;
    setBusy(true);
    bridge.setLoading(true);
    try {
      const res = await sessionFetch("/api/invoices", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ orderId }),
      });
      if (!res.ok) throw new Error("invoice-failed");
      bridge.close({ refresh: true, message: "Fatura oluşturuldu", kind: "success" });
    } catch {
      bridge.toast("Fatura oluşturulamadı", { kind: "error" });
    } finally {
      bridge.setLoading(false);
      setBusy(false);
    }
  }

  return (
    <main style={{ padding: 24 }}>
      <h1>Fatura kes</h1>
      <p>Sipariş: {orderId}</p>
      <button onClick={createInvoice} disabled={busy}>Faturayı oluştur</button>
      <button onClick={() => bridge.close()}>Vazgeç</button>
    </main>
  );
}
```

Sayfanın `AppBridgeProvider` içinde olması gerekir; bkz. [App Bridge](/app-bridge). `/api/invoices` sizin uçunuzdur ve isteği oturum token'ıyla doğrular (`tecof.sessionFromRequest(req)`).

---

## API Aksiyonları

### İstek

Tecof, `appUrl + path` adresine imzalı bir `POST` gönderir (test mağazası kurulumunda `tecof app dev` açıksa tünel adresine). Zaman aşımı **15 saniyedir**; aksiyonlar **yeniden denenmez**.

```http
POST /api/actions/bulk-invoice HTTP/1.1
Content-Type: application/json
X-Tecof-Timestamp: 1760087770
X-Tecof-Signature: t=1760087770,v1=5f2c…e91a
X-Tecof-Action: bulk-invoice
X-Tecof-Installation-Id: 6720aa00bb11cc22dd33ee55
```

```json
{
  "actionRunId": "6720cc00dd11ee22ff33aa55",
  "actionKey": "bulk-invoice",
  "placement": "order.list",
  "ids": ["6720bb00cc11dd22ee33ff44", "6720bb00cc11dd22ee33ff45"],
  "merchantId": "66f0c0ffee0000000000abcd",
  "installationId": "6720aa00bb11cc22dd33ee55",
  "userId": "66f0aa00bb11cc22dd33ee66",
  "locale": "tr",
  "timestamp": "2026-10-10T09:16:10.000Z"
}
```

### Yanıt

Yanıt JSON ve **en fazla 16 KB** olmalıdır:

```json
{
  "ok": true,
  "message": "2 fatura oluşturuldu",
  "kind": "success",
  "redirectUrl": "https://isbasi.apps.tecof.site/invoices",
  "refresh": true
}
```

| Alan | Anlamı |
| :--- | :--- |
| `ok` | İşlem sonucu |
| `message` | Kullanıcıya gösterilecek mesaj |
| `kind` | `success`, `error`, `info`, `warning` |
| `redirectUrl` | İsteğe bağlı yönlendirme adresi |
| `refresh` | `true` ise panel ekranı yeniler |

Panel tarafına `{ runId, type: "api", ok, message, kind, redirectUrl, refresh }` döner. `2xx` olmayan yanıt ya da zaman aşımı `ok: false`, `kind: "error"` ve genel `app-action-failed` mesajına dönüşür. Kullanıcıya **kendi** hata mesajınızı göstermek istiyorsanız `200` ile `{ "ok": false, "kind": "error", "message": "…" }` dönün.

### İşleyici

```ts
// app/api/actions/bulk-invoice/route.ts
import { tecof } from "@/lib/tecof";

export async function POST(req: Request) {
  const rawBody = await req.text();

  let action;
  try {
    action = tecof.verifyActionRequest(rawBody, req.headers);
  } catch {
    return Response.json({ ok: false, kind: "error", message: "Geçersiz imza" }, { status: 401 });
  }

  const api = tecof.client(action.installationId);
  let created = 0;
  const failed: string[] = [];

  for (const orderId of action.ids) {
    try {
      const order = await api.orders.get(orderId);
      // … faturayı oluştur, siparişe metafield yaz
      created++;
    } catch {
      failed.push(orderId);
    }
  }

  if (failed.length > 0) {
    return Response.json({
      ok: created > 0,
      kind: created > 0 ? "warning" : "error",
      message: `${created} fatura oluşturuldu, ${failed.length} sipariş atlandı`,
      refresh: true,
    });
  }

  return Response.json({ ok: true, kind: "success", message: `${created} fatura oluşturuldu`, refresh: true });
}
```

15 saniyelik sınırı aşabilecek toplu işlerde işi küçük parçalara bölün; aksiyon yeniden denenmediği için işleyicinin yarıda kalması durumunda kullanıcının aksiyonu tekrar çalıştırması güvenli olmalıdır (idempotent tasarım: işlenmiş siparişleri metafield ile atlayın).

---

## Yerelde Test

**Uçtan uca (önerilen):** `tecof app dev` çalışırken uygulamanız test mağazasında kuruluysa, panelden çalıştırdığınız hem iframe hem API aksiyonları tünel üzerinden yerel sunucunuza gelir (`devUrl`, test mağazası kurulumlarında `appUrl`'in yerine geçer). Bkz. [Hızlı Başlangıç](/app-quickstart).

**Yalnız imza ve yanıt biçimi:** API aksiyonunu yerel sunucunuza imzalı `curl` ile de gönderebilirsiniz:

```bash
SECRET="$TECOF_CLIENT_SECRET"
T=$(date +%s)
BODY='{"actionRunId":"local-run-1","actionKey":"bulk-invoice","placement":"order.list","ids":["6720bb00cc11dd22ee33ff44"],"merchantId":"66f0c0ffee0000000000abcd","installationId":"6720aa00bb11cc22dd33ee55","userId":"66f0aa00bb11cc22dd33ee66","locale":"tr","timestamp":"2026-10-10T09:16:10.000Z"}'
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')

curl -X POST http://localhost:3000/api/actions/bulk-invoice \
  -H "Content-Type: application/json" \
  -H "X-Tecof-Timestamp: $T" \
  -H "X-Tecof-Signature: t=$T,v1=$SIG" \
  -H "X-Tecof-Action: bulk-invoice" \
  -H "X-Tecof-Installation-Id: 6720aa00bb11cc22dd33ee55" \
  --data-binary "$BODY"
```

- `printf '%s.%s'` ile imzalanan metin ile `--data-binary` ile gönderilen gövde **bayt bayt aynı** olmalıdır.
- `installationId` gerçek bir test kurulumuna ait değilse işleyicideki API çağrıları başarısız olur; uçtan uca akışı test mağazasında tünelle doğrulayın.
- iframe aksiyonları için test mağazasında aksiyonu panelden çalıştırın; App Bridge yalnız panel içinde çalışır.

---

## Sık Karşılaşılan Hatalar

| Belirti | Olası neden | Çözüm |
| :--- | :--- | :--- |
| Aksiyon panelde görünmüyor | Yapılandırma gönderilmedi; kurulum aktif değil; yerleşimin gerektirdiği kapsam verilmemiş; onaysız uygulama test mağazası dışında | `tecof app config push`; kapsamı ekleyip mağazaya onaylatın; `testStores`'u kontrol edin |
| Test mağazasında aksiyon yerel sunucuya ulaşmıyor | `tecof app dev` kapalı ya da `devUrl`'in 8 saatlik süresi doldu | `tecof app dev`'i yeniden başlatın |
| Panelde `app-action-failed` | Uç `2xx` dışı yanıt döndü ya da 15 saniyeyi aştı | Kendi hata mesajınız için `200` + `ok: false` dönün; işi bölün |
| İşleyici `401` dönüyor | Gövde JSON olarak okunup yeniden serileştirildi; yanlış sır; saat farkı 300 sn'yi aşıyor | `await req.text()` kullanın; `TECOF_CLIENT_SECRET`'ı kontrol edin; sunucu saatini eşitleyin |
| Sır döndürmeden sonra imza hatası | Uygulama eski sırla çalışıyor ve yalnız tek `v1` kontrol ediliyor | Başlıktaki tüm `v1` değerlerini deneyin (SDK yapar); yeni sırrı ortama yazın |
| Panelde beklediğiniz mesaj görünmüyor | Yanıt JSON değil ya da 16 KB sınırını aşıyor | Yanıtı en fazla 16 KB'lık JSON olarak dönün |
| iframe aksiyonu boş açılıyor | Sayfa `X-Frame-Options: DENY` ya da panel origin'ini içermeyen `frame-ancestors` gönderiyor | Panel origin'inin uygulamanızı çerçevelemesine izin verin |
| `close()` çalışmıyor | App Bridge hazır değil ya da sayfa panel dışında açıldı | `AppBridgeProvider` / `bridge.ready()` sonrasında çağırın |
| Toplu işlemde 50'den fazla kayıt | Panel seçim sınırı | Yerleşim başına en fazla 50 kimlik gelir |
