# Örnek Uygulama: Logo İşbaşı

> Uçtan uca örnek uygulama: Logo İşbaşı ile otomatik fatura — Vault, metafield, aksiyon ve olay yayını.
>
> Kaynak: https://tecof-app-dev.vercel.app/app-example-isbasi · Bölüm: Uygulama geliştirme · Son güncelleme: 2018-10-20

Logo İşbaşı uygulaması, platformun yeteneklerini uçtan uca gösteren örnek bir fatura uygulamasıdır: ödenen siparişler için İşbaşı'nda otomatik fatura oluşturur, İşbaşı kimlik bilgilerini Vault'ta saklar, fatura numarasını siparişe metafield olarak yazıp panelde gösterir, sipariş ekranlarına aksiyon ekler ve başka uygulamaların dinleyebileceği `invoice.created` olayını yayınlar.

> Bu sayfa uygulamanın mimarisini ve platformla sözleşmesini anlatır. İşbaşı API'sinin kendi ayrıntıları (uç adları, alanlar) uygulamaya özgüdür ve burada `createIsbasiInvoice` gibi yardımcılarla soyutlanmıştır. Ayar alanları örnektir; uygulamanın son sürümünde farklılık gösterebilir.

---

## Ne Yapar

| Özellik | Platform yeteneği |
| :--- | :--- |
| `order.paid` geldiğinde faturayı **taslak** ya da **kesilmiş** olarak oluşturur (mağaza ayarı) | [Webhook'lar](/app-webhooks), [API](/app-api) |
| İşbaşı API anahtarı ve parolasını panelin ayar formundan alır, şifreli saklar | [Yerel ayar formu + Vault](/app-data) |
| Fatura numarasını siparişe yazar; sipariş detayındaki "Uygulamalar" kartında görünür | [Metafield'lar](/app-data) |
| Sipariş detayına "İşbaşı'nda fatura kes", sipariş listesine "Seçilenleri faturala" ekler | [Aksiyonlar](/app-actions) |
| Panelde "Faturalar" ekranı | [App Bridge](/app-bridge), `navigation` |
| Fatura kesilince `app.isbasi.invoice.created` olayını yayınlar | [Uygulamalar Arası İletişim](/app-interop) |
| Aylık plan ve 14 günlük deneme | [Ücretlendirme](/app-billing) |

---

## Yapılandırma

```json
{
  "name": "Logo İşbaşı",
  "slug": "isbasi",
  "hosting": "tecof",
  "embedded": true,
  "scopes": ["orders:read", "customers:read", "products:read"],
  "webhooks": {
    "path": "/api/webhooks",
    "events": ["order.paid", "order.cancelled", "order.refunded"]
  },
  "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?" }
    }
  ],
  "navigation": [
    { "label": { "tr": "Faturalar", "en": "Invoices" }, "path": "/invoices" }
  ],
  "settings": [
    { "key": "username", "label": { "tr": "İşbaşı kullanıcı adı", "en": "İşbaşı username" }, "type": "text", "required": true },
    { "key": "password", "label": { "tr": "İşbaşı parolası", "en": "İşbaşı password" }, "type": "secret", "required": true },
    { "key": "apiKey", "label": { "tr": "İşbaşı API anahtarı", "en": "İşbaşı API key" }, "type": "secret", "required": true },
    { "key": "autoInvoice", "label": { "tr": "Ödenen siparişleri otomatik faturala", "en": "Auto-invoice paid orders" }, "type": "boolean" },
    { "key": "issueInvoices", "label": { "tr": "Faturayı taslak değil kesilmiş olarak oluştur", "en": "Issue invoices instead of drafts" }, "type": "boolean" },
    { "key": "defaultVatRate", "label": { "tr": "Varsayılan KDV oranı (%)", "en": "Default VAT rate (%)" }, "type": "number" }
  ],
  "appEvents": {
    "publishes": [
      { "name": "invoice.created", "description": { "tr": "Fatura oluşturuldu", "en": "Invoice created" } }
    ],
    "subscribes": ["app.kargo.shipment.created"]
  },
  "plans": [
    {
      "key": "standart", "name": { "tr": "Standart", "en": "Standard" },
      "description": { "tr": "Sınırsız otomatik fatura", "en": "Unlimited automatic invoices" },
      "price": 199, "currency": "TRY", "interval": "month", "trialDays": 14,
      "features": [{ "tr": "Sınırsız fatura", "en": "Unlimited invoices" }]
    }
  ],
  "listing": {
    "shortDescription": { "tr": "Siparişlerinizi Logo İşbaşı'nda otomatik faturalayın", "en": "Automatically invoice orders in Logo İşbaşı" },
    "description": { "tr": "…", "en": "…" },
    "category": "accounting",
    "supportEmail": "destek@ornek.com",
    "privacyUrl": "https://ornek.com/gizlilik"
  }
}
```

Kapsam gerekçeleri: `orders:read` sipariş ve `order.*` olayları, sipariş aksiyonları ve sipariş metafield'ları için; `customers:read` fatura alıcısı için; `products:read` ürün bilgisi için. Uygulama Tecof'a ürün ya da sipariş **yazmaz**; tek yazdığı şey kendi metafield'ları, ayarları ve olaylarıdır.

---

## Mimari

```mermaid
sequenceDiagram
    participant M as Mağaza paneli
    participant T as Tecof API
    participant A as İşbaşı uygulaması
    participant L as Logo İşbaşı API

    Note over M,A: Kurulum ve ayar
    M->>T: Kurulum (kapsam onayı, deneme planı)
    T->>A: webhook app.installed
    M->>T: Ayarlar sekmesi: kullanıcı adı, parola, API anahtarı
    T->>T: Sır alanları Vault'a (AES-256-GCM)
    T->>A: webhook app.settings.updated { keys, secretKeys }

    Note over T,L: Otomatik fatura
    T->>A: webhook order.paid (imzalı)
    A->>T: POST /api/apps/oauth/token (client_credentials)
    A->>T: GET /api/v1/metafields?ownerType=order&namespace=isbasi
    A->>T: GET /api/v1/orders/:id
    A->>T: GET /api/v1/app/settings + /api/v1/app/secrets/:key
    A->>L: Fatura oluştur (taslak / kesilmiş)
    L-->>A: Fatura numarası
    A->>T: PUT /api/v1/metafields (invoiceNumber, shared, panelde göster)
    A->>T: POST /api/v1/app/events { name: "invoice.created" }
    A-->>T: 204
```

---

## Fatura Oluşturma

Webhook işleyicisi, iframe aksiyonu ve toplu API aksiyonu aynı fonksiyonu çağırır:

```ts
// lib/invoice.ts
import { tecof } from "@/lib/tecof";
import { createIsbasiInvoice } from "@/lib/isbasi"; // uygulamaya özgü İşbaşı istemcisi

export async function invoiceOrder(installationId: string, orderId: string) {
  const api = tecof.client(installationId);

  // 1) Idempotency: siparişte fatura numarası varsa tekrar kesme
  const existing = await api.metafields.list({ ownerType: "order", ownerId: orderId, namespace: "isbasi" });
  if (existing.some((m: { key: string }) => m.key === "invoiceNumber")) {
    return { status: "skipped" as const };
  }

  // 2) Siparişin güncel hâli
  const order = await api.orders.get(orderId);
  if (order.currency !== "TRY") {
    return { status: "unsupported-currency" as const }; // v1: yalnız TRY
  }

  // 3) Ayarlar ve Vault
  const { settings } = await api.settings.get();
  const [{ value: password }, { value: apiKey }] = await Promise.all([
    api.secrets.get("password"),
    api.secrets.get("apiKey"),
  ]);
  const defaultVatRate = Number(settings.defaultVatRate ?? 20);

  // 4) İşbaşı'nda fatura
  const invoice = await createIsbasiInvoice(
    { username: String(settings.username), password, apiKey },
    {
      issue: settings.issueInvoices === true, // false → taslak
      buyer: order.billingAddress,            // identityNumber: TCKN/VKN
      lines: order.lineItems.map((li) => ({
        name: li.variantTitle ? `${li.name} (${li.variantTitle})` : li.name,
        sku: li.sku,
        quantity: li.quantity,
        unitPrice: li.unitPrice,
        vatRate: li.taxRate ?? defaultVatRate, // null = eski sipariş → mağaza varsayılanı
        taxIncluded: li.taxIncluded,
      })),
      shipping: order.shippingLines,
      discounts: order.discountLines,
    },
  );

  // 5) Fatura numarasını siparişe yaz (panelde görünür, diğer uygulamalarla paylaşılır)
  await api.metafields.set({
    ownerType: "order",
    ownerId: order.id,
    key: "invoiceNumber",
    type: "string",
    value: invoice.number,
    visibility: "shared",
    panel: { label: { tr: "İşbaşı fatura no", en: "İşbaşı invoice no" }, show: true },
  });

  // 6) Olayı yayınla
  await api.events.publish("invoice.created", {
    orderId: order.id,
    orderNumber: order.orderNumber,
    invoiceNumber: invoice.number,
    issued: settings.issueInvoices === true,
  });

  return { status: "created" as const, invoiceNumber: invoice.number };
}
```

Webhook işleyicisi:

```ts
// app/api/webhooks/route.ts
import { createWebhookHandler } from "@tecof/app-sdk";
import { tecof } from "@/lib/tecof";
import { invoiceOrder } from "@/lib/invoice";

export const POST = createWebhookHandler({
  app: tecof,
  handlers: {
    "order.paid": async (event) => {
      const { settings } = await tecof.client(event.installationId).settings.get();
      if (settings.autoInvoice === true) {
        await invoiceOrder(event.installationId, String(event.data.orderId));
      }
    },
    // v1: fatura otomatik iptal edilmez — iptal/iade belgesi İşbaşı'nda yönetilir
    "order.cancelled": async () => {},
    "order.refunded": async () => {},
  },
});
```

`invoiceOrder` kalıcı hatalarda (eksik ayar, desteklenmeyen para birimi) hata fırlatmaz, bir durum döner; böylece işleyici `2xx` yanıt verir ve Tecof aynı olayı boşuna yeniden denemez. Geçici hatalarda (İşbaşı'na ulaşılamadı) hata fırlatılır; `createWebhookHandler` `2xx` dışı yanıt döner ve teslimat +2 dk ile +15 dk sonra yeniden denenir. Yeniden denemede `invoiceNumber` metafield kontrolü çift faturayı önler.

---

## Panelde Görünüm

| Yer | Ne görünür |
| :--- | :--- |
| Uygulamalar → İşbaşı → Ayarlar | Yerel ayar formu; sır alanları yalnız "ayarlı / ayarlı değil" gösterir |
| Uygulamalar → İşbaşı → Faturalar | Gömülü `/invoices` ekranı (App Bridge) |
| Sipariş detayı → "Uygulamalar" kartı | `İşbaşı fatura no: ABC2026000000123` (metafield) |
| Sipariş detayı → "Uygulamalar" menüsü | "İşbaşı'nda fatura kes" (iframe aksiyonu, modal) |
| Sipariş listesi → toplu işlemler | "Seçilenleri faturala" (API aksiyonu, onaylı) |

---

## Sınırlamalar (v1)

| Konu | Durum |
| :--- | :--- |
| Para birimi | Yalnız **TRY** siparişler faturalanır; diğer para birimleri atlanır |
| İptal / iade | `order.cancelled` ve `order.refunded` alınır ancak fatura **otomatik iptal edilmez**; iptal ve iade belgeleri İşbaşı'nda elle yönetilir |
| KDV oranı bilinmeyen eski siparişler | Kalemde `taxRate: null` ise uygulama ayarındaki varsayılan oran kullanılır |
| Abonelik | Otomatik yenileme yok; mağaza dönem sonunda planı yeniler |
