Next.js ile Sage Bağlantısı Nasıl Yapılır?

By Codefacture6 dk okuma

Next.js ile Sage Bağlantısı Nasıl Yapılır?

 

Sage Accounting; Birleşik Krallık, İrlanda, Amerika Birleşik Devletleri ve Kanada gibi pazarlarda küçük ve orta ölçekli işletmelerin kullandığı bulut tabanlı bir muhasebe platformudur. Next.js ile geliştirilen uygulamalar için Sage bağlantısı; faturaların otomatik oluşturulması, müşteri kayıtlarının uyumlu kalması ve finansal verilere manuel dışa aktarım olmadan erişilmesi demektir. Sage Accounting API v3.1 temiz bir REST API'dir, ancak geliştiricileri hazırlıksız yakalayan birkaç operasyonel detayı vardır: yalnızca beş dakika geçerli erişim token'ları, her kullanımda değişen yenileme token'ları ve her isteğin hangi işletmeye yazacağını belirleyen bir başlık. Bu rehberde Sage Accounting'i Next.js App Router ile adım adım bağlayacak, her aşama için kod örnekleri paylaşacağız. Bu rehberin bulut tabanlı Sage Accounting ürününü kapsadığını belirtelim; Sage 50 ve Sage 200 gibi masaüstü ürünler farklı entegrasyon mekanizmaları kullanır.

 

Başlamadan Önce

İşe Sage Developer portalında bir uygulama kaydederek başlayın. Bir istemci kimliği (client ID) ve istemci anahtarı (client secret) alacak ve callback adresinizi eklemeniz gerekecek; örneğin geliştirme sırasında http://localhost:3000/api/sage/callback. Gerçek defterleri etkilemeden rahatça fatura oluşturabilmek için geliştirme aşamasında bir deneme veya test işletmesi kullanmanızı önemle öneririz.

Bu rehber, üçüncü taraf bir SDK yerine Next.js'te hazır bulunan fetch API'sini kullanır; bu da bağımlılıkları minimumda tutar ve token yönetimi üzerinde tam kontrol sağlar. Kimlik bilgilerinizi ortam değişkenlerine ekleyin ve tarayıcıya asla açılmamaları için NEXT_PUBLIC_ öneki kullanmayın.

# .env.local
SAGE_CLIENT_ID=...
SAGE_CLIENT_SECRET=...
SAGE_REDIRECT_URI=http://localhost:3000/api/sage/callback

 

OAuth 2.0 Akışını Kurmak

Yetkilendirme akışı, kullanıcıyı Sage yetkilendirme sayfasına yönlendiren bir route handler ile başlar. Rastgele bir state değeri üretilir ve HTTP-only bir cookie'de saklanır; böylece callback, yanıtın aynı kullanıcıya ait olduğunu doğrulayabilir ve siteler arası istek sahteciliğine (CSRF) karşı koruma sağlanır. full_access scope'u veri okuma ve yazmaya izin verir; entegrasyonunuz yalnızca okuma yapacaksa readonly kullanın.

// app/api/sage/connect/route.ts
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function GET() {
  const state = crypto.randomUUID();
  (await cookies()).set("sage_oauth_state", state, {
    httpOnly: true,
    secure: true,
    maxAge: 600,
  });

  const url = new URL("https://www.sageone.com/oauth2/auth/central");
  url.searchParams.set("filter", "apiv3.1");
  url.searchParams.set("response_type", "code");
  url.searchParams.set("client_id", process.env.SAGE_CLIENT_ID!);
  url.searchParams.set("redirect_uri", process.env.SAGE_REDIRECT_URI!);
  url.searchParams.set("scope", "full_access");
  url.searchParams.set("state", state);

  return NextResponse.redirect(url);
}

Kullanıcı erişimi onayladığında Sage, bir yetkilendirme koduyla callback route'unuza geri yönlendirir. Route state değerini doğrular, kodu Sage token adresinde token'larla takas eder ve erişim token'ını, yenileme token'ını ve bitiş süresini veritabanınıza kaydeder.

// app/api/sage/callback/route.ts
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { saveSageTokens } from "@/lib/db";

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const code = searchParams.get("code");
  const state = searchParams.get("state");
  const savedState = (await cookies()).get("sage_oauth_state")?.value;

  if (!code || !state || state !== savedState) {
    return new Response("Invalid OAuth state", { status: 400 });
  }

  const res = await fetch("https://oauth.accounting.sage.com/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: process.env.SAGE_REDIRECT_URI!,
      client_id: process.env.SAGE_CLIENT_ID!,
      client_secret: process.env.SAGE_CLIENT_SECRET!,
    }),
  });
  const tokens = await res.json();

  await saveSageTokens({
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresAt: Date.now() + tokens.expires_in * 1000,
  });

  return NextResponse.redirect(new URL("/settings/integrations", req.url));
}

 

Kısa Ömürlü ve Dönen Token'ları Yönetmek

Sage erişim token'larının süresi yaklaşık beş dakikada dolar ve her yenileme, eskisini geçersiz kılarak yepyeni bir yenileme token'ı döndürür. Yaklaşık bir ay içinde kullanılmayan yenileme token'larının da süresi dolar ve kullanıcının yeniden bağlanması gerekir. Bu da token yönetimini bir Sage entegrasyonunun en önemli parçası haline getirir. Aşağıdaki yardımcı fonksiyon, token'ı süresi dolmadan kısa bir süre önce yeniler, dönen yenileme token'ını hemen kaydeder ve her API çağrısını gerekli başlıklarla sarar.

// lib/sage.ts
import "server-only";
import { getSageTokens, saveSageTokens } from "@/lib/db";

const API_BASE = "https://api.accounting.sage.com/v3.1";

async function getAccessToken() {
  const tokens = await getSageTokens();
  if (Date.now() < tokens.expiresAt - 60_000) return tokens.accessToken;

  const res = await fetch("https://oauth.accounting.sage.com/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: tokens.refreshToken,
      client_id: process.env.SAGE_CLIENT_ID!,
      client_secret: process.env.SAGE_CLIENT_SECRET!,
    }),
  });
  if (!res.ok) throw new Error("Sage re-authorization required");

  const fresh = await res.json();
  await saveSageTokens({
    ...tokens,
    accessToken: fresh.access_token,
    refreshToken: fresh.refresh_token, // refresh tokens rotate: always store the new one
    expiresAt: Date.now() + fresh.expires_in * 1000,
  });

  return fresh.access_token;
}

export async function sageFetch(path: string, init: RequestInit = {}) {
  const { businessId } = await getSageTokens();

  const res = await fetch(`${API_BASE}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${await getAccessToken()}`,
      "Content-Type": "application/json",
      ...(businessId ? { "X-Business": businessId } : {}),
      ...init.headers,
    },
    cache: "no-store",
  });

  if (!res.ok) throw new Error(`Sage API error: ${res.status}`);
  return res.json();
}

Yenileme token'ları döndüğü için, aynı anda yenileme yapmaya çalışan iki paralel istek birbirini geçersiz kılabilir ve bağlantıyı bozabilir. Üretim ortamında yenileme adımını bir veritabanı satır kilidi veya Redis'te dağıtık bir kilit gibi bir mekanizmayla koruyun; böylece aynı anda yalnızca bir yenileme çalışır. Entegrasyonu nadiren kullanan müşteriler için token'ları düzenli olarak yenileyen zamanlanmış bir iş, kullanılmadan süresinin dolmasını önler. cache: "no-store" ayarı da Next.js'in finansal veri içeren yanıtları asla önbelleğe almamasını sağlar.

 

İşletmeyi Seçmek ve API Çağrıları Yapmak

Tek bir Sage kullanıcısı, örneğin birden fazla müşterinin defterlerini yöneten bir muhasebeci gibi, birkaç işletmeye erişebilir. OAuth akışı kullanıcıyı doğrular ancak hangi işletmenin kullanılacağına karar vermez. Yetkilendirmeden sonra erişilebilir işletmeleri listeleyin, kullanıcının seçim yapmasını sağlayın ve seçilen işletme kimliğini saklayın. Bundan sonra bu kimliği her istekte X-Business başlığıyla gönderin; başlık gönderilmezse Sage, kullanıcının ana işletmesine (lead business) yönelir ve bu, yazmak istediğiniz işletme olmayabilir.

// List the businesses the user can access, then store the chosen business id
const businesses = await sageFetch("/businesses");

// Create a sales invoice in the selected business
const invoice = await sageFetch("/sales_invoices", {
  method: "POST",
  body: JSON.stringify({
    sales_invoice: {
      contact_id: contactId,
      date: new Date().toISOString().slice(0, 10),
      invoice_lines: [
        {
          description: "Consulting services",
          ledger_account_id: salesLedgerAccountId,
          quantity: 1,
          unit_price: 500,
          tax_rate_id: "GB_STANDARD",
        },
      ],
    },
  }),
});

Hesap planı kalemleri ve vergi oranları ülkeden ülkeye ve işletmeden işletmeye farklılık gösterir; bu yüzden kimlikleri koda sabitlemek yerine kurulum sırasında API'den sorgulayın. Yukarıdaki örnekteki vergi oranı Birleşik Krallık işletmeleri için geçerlidir.

 

Verileri Senkronize Tutmak

Sage Accounting entegrasyonları genellikle zamanlanmış, artımlı senkronizasyonlara dayanır. Her seferinde her şeyi indirmek yerine son başarılı senkronizasyonun zamanını saklayın ve yalnızca o zamandan beri oluşturulan veya güncellenen kayıtları isteyin. Bir Next.js uygulamasında, cron ile tetiklenen bir route handler bu işi çalıştırmanın pratik bir yoludur; yalnızca zamanlayıcınızın çağırabilmesi için bir gizli anahtarla korunmalıdır.

// app/api/cron/sage-sync/route.ts
import { sageFetch } from "@/lib/sage";
import { getLastSync, setLastSync, upsertInvoices } from "@/lib/db";

export async function GET(req: Request) {
  if (req.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response("Unauthorized", { status: 401 });
  }

  const since = await getLastSync("sales_invoices");
  const startedAt = new Date().toISOString();

  const data = await sageFetch(
    `/sales_invoices?updated_or_created_since=${encodeURIComponent(since)}&items_per_page=200`
  );
  await upsertInvoices(data.$items);

  await setLastSync("sales_invoices", startedAt);
  return Response.json({ synced: data.$items.length });
}

Yanıtlar sayfalı döner; bu yüzden üretim kodu tüm kayıtlar alınana kadar sonraki sayfa bağlantılarını takip etmelidir. Büyük veri setlerinde serverless çalışma süresi limitlerinin içinde kalmak için işi bir arka plan kuyruğuna taşıyın ve API 429 limit yanıtı döndüğünde geri çekilin.

 

Sonuç

Sage'i Next.js ile bağlamak, uygulamanıza müşterilerinizin muhasebe verilerine güvenilir ve otomatik erişim kazandırır. State doğrulamalı güvenli bir OAuth 2.0 akışı kurarak, beş dakikalık erişim token'larını ve dönen yenileme token'larını özenle yöneterek, X-Business başlığını her zaman göndererek ve verileri artımlı olarak senkronize ederek yayına alındıktan çok sonra da çalışmaya devam eden bir entegrasyon kurarsınız. İster bir SaaS ürününe faturalama ekliyor ister finans operasyonlarını otomatikleştiriyor olun, entegrasyonu doğru kurmak şarttır ve hem Sage'i hem de yazılım mimarisini anlayan deneyimli bir Next.js ekibiyle çalışmak, finansal verilerinizin sorunsuz ve güvenli biçimde akmasını sağlar.

sagenext.jssage entegrasyonumuhasebe entegrasyonuweb geliştirme

Bu yazıyı paylaş

Benzer Yazılar

Benzer yazı bulunamadı.

İlgili Hizmetimiz

Next.js Yazılım Hizmetimiz

Bu konuda profesyonel destek almak ister misiniz?

Hizmeti İncele

İletişim Formu

Bu form üzerinden tarafımıza ulaşabilirsiniz

© 2024-2026 Codefacture Yazılım A.Ş. Tüm Hakları Saklıdır
Hızlı Teklif

Ortalama Yanıt Süresi: 15 Dakika