Laravel ile Sage Bağlantısı Nasıl Yapılır?

By Codefacture6 dk okuma

Laravel 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. Laravel 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 birkaç operasyonel detay geliştiricileri sıklıkla hazırlıksız yakalar: 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. Laravel, bunların hepsini yönetecek doğru araçlara sahiptir. Bu rehberde Sage Accounting'i Laravel 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:8000/sage/callback. Gerçek defterleri etkilemeden rahatça fatura oluşturabilmek için bir deneme veya test işletmesi kullanmanızı önemle öneririz.

Bu rehber, üçüncü taraf bir SDK yerine Laravel'in yerleşik HTTP istemcisini 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 config/services.php dosyasına kaydedin.

# .env
SAGE_CLIENT_ID=...
SAGE_CLIENT_SECRET=...
SAGE_REDIRECT_URI=http://localhost:8000/sage/callback

// config/services.php
'sage' => [
    'client_id' => env('SAGE_CLIENT_ID'),
    'client_secret' => env('SAGE_CLIENT_SECRET'),
    'redirect' => env('SAGE_REDIRECT_URI'),
],

 

Bağlantıları Güvenle Saklamak

Her bağlantının güncel erişim token'ına, yenileme token'ına, bitiş süresine, seçilen işletme kimliğine ve son başarılı senkronizasyonun zamanına ihtiyacı vardır. Sage token'ları uzun olduğu için text sütunlar kullanın ve token'ların veritabanına ulaşmadan önce uygulama anahtarınızla şifrelenmesi için Laravel'in encrypted cast'ini uygulayın.

// database/migrations/xxxx_create_sage_connections_table.php
Schema::create('sage_connections', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('business_id')->nullable();
    $table->text('access_token');
    $table->text('refresh_token');
    $table->timestamp('expires_at');
    $table->timestamp('last_synced_at')->nullable();
    $table->timestamps();
});

// app/Models/SageConnection.php
protected function casts(): array
{
    return [
        'access_token' => 'encrypted',
        'refresh_token' => 'encrypted',
        'expires_at' => 'datetime',
        'last_synced_at' => 'datetime',
    ];
}

 

OAuth 2.0 Akışını Kurmak

İlk action rastgele bir state değeri üretir, bunu session'da saklar ve kullanıcıyı Sage yetkilendirme sayfasına yönlendirir. full_access scope'u veri okuma ve yazmaya izin verir; entegrasyonunuz yalnızca okuma yapacaksa readonly kullanın. Callback action'ı siteler arası istek sahteciliğine (CSRF) karşı state değerini doğrular, yetkilendirme kodunu Sage token adresinde token'larla takas eder ve bağlantıyı kaydeder.

// app/Http/Controllers/SageController.php
namespace App\Http\Controllers;

use App\Models\SageConnection;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class SageController extends Controller
{
    public function connect(Request $request)
    {
        $state = Str::random(40);
        $request->session()->put('sage_state', $state);

        $query = http_build_query([
            'filter' => 'apiv3.1',
            'response_type' => 'code',
            'client_id' => config('services.sage.client_id'),
            'redirect_uri' => config('services.sage.redirect'),
            'scope' => 'full_access',
            'state' => $state,
        ]);

        return redirect('https://www.sageone.com/oauth2/auth/central?'.$query);
    }

    public function callback(Request $request)
    {
        abort_unless(
            $request->state && $request->state === $request->session()->pull('sage_state'),
            403
        );

        $tokens = Http::asForm()
            ->post('https://oauth.accounting.sage.com/token', [
                'grant_type' => 'authorization_code',
                'code' => $request->code,
                'redirect_uri' => config('services.sage.redirect'),
                'client_id' => config('services.sage.client_id'),
                'client_secret' => config('services.sage.client_secret'),
            ])
            ->throw()
            ->json();

        SageConnection::updateOrCreate(
            ['user_id' => $request->user()->id],
            [
                'access_token' => $tokens['access_token'],
                'refresh_token' => $tokens['refresh_token'],
                'expires_at' => now()->addSeconds($tokens['expires_in']),
            ]
        );

        return redirect()->route('sage.businesses');
    }
}

 

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, özellikle birden fazla queue worker'ın API'yi aynı anda çağırabildiği Laravel uygulamalarında, bir Sage entegrasyonunun en önemli parçası haline getirir.

İki worker aynı bağlantıyı aynı anda yenilerse biri az önce geçersiz kılınmış bir yenileme token'ı kullanır ve bağlantı bozulur. Aşağıdaki servis sınıfı bunu bir cache kilidiyle önler: yalnızca bir worker yenileme yapar, diğerleri bekler, kaydı yeniden yükler ve yeni token'ı kullanır. Sınıf ayrıca her istekte X-Business başlığını gönderir ve 429 limit yanıtlarında otomatik olarak yeniden dener.

// app/Services/SageClient.php
namespace App\Services;

use App\Models\SageConnection;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class SageClient
{
    public function __construct(private SageConnection $connection) {}

    public function request(): PendingRequest
    {
        $this->refreshTokenIfNeeded();

        return Http::withToken($this->connection->access_token)
            ->withHeaders(array_filter(['X-Business' => $this->connection->business_id]))
            ->acceptJson()
            ->baseUrl('https://api.accounting.sage.com/v3.1')
            ->retry(3, 2000, fn ($e) => $e instanceof RequestException
                && $e->response->status() === 429);
    }

    private function refreshTokenIfNeeded(): void
    {
        if ($this->connection->expires_at->isAfter(now()->addMinute())) {
            return;
        }

        // Refresh tokens rotate: two parallel refreshes would break the connection
        Cache::lock("sage-refresh-{$this->connection->id}", 10)->block(5, function () {
            $this->connection->refresh();

            if ($this->connection->expires_at->isAfter(now()->addMinute())) {
                return;
            }

            $tokens = Http::asForm()
                ->post('https://oauth.accounting.sage.com/token', [
                    'grant_type' => 'refresh_token',
                    'refresh_token' => $this->connection->refresh_token,
                    'client_id' => config('services.sage.client_id'),
                    'client_secret' => config('services.sage.client_secret'),
                ])
                ->throw()
                ->json();

            $this->connection->update([
                'access_token' => $tokens['access_token'],
                'refresh_token' => $tokens['refresh_token'], // always store the new one
                'expires_at' => now()->addSeconds($tokens['expires_in']),
            ]);
        });
    }
}

Entegrasyonu nadiren kullanan müşteriler için token'ları düzenli olarak yenileyen bir iş zamanlayın; böylece token'lar kullanılmadan süresi dolup yeniden bağlanmayı zorunlu kılmaz.

 

İş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 kaydedin. X-Business başlığı gönderilmezse Sage, kullanıcının ana işletmesine (lead business) yönelir ve bu, yazmak istediğiniz işletme olmayabilir.

$sage = new SageClient($connection);

// List the businesses the user can access, then save the chosen one
$businesses = $sage->request()->get('/businesses')->throw()->json();
$connection->update(['business_id' => $chosenBusinessId]);

// Create a sales invoice in the selected business
$invoice = $sage->request()
    ->post('/sales_invoices', [
        'sales_invoice' => [
            'contact_id' => $contactId,
            'date' => now()->toDateString(),
            'invoice_lines' => [[
                'description' => 'Consulting services',
                'ledger_account_id' => $salesLedgerAccountId,
                'quantity' => 1,
                'unit_price' => 500,
                'tax_rate_id' => 'GB_STANDARD',
            ]],
        ],
    ])
    ->throw()
    ->json();

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.

 

Zamanlanmış Veri Senkronizasyonu

Sage Accounting entegrasyonları genellikle zamanlanmış, artımlı senkronizasyonlara dayanır. Her seferinde her şeyi indirmek yerine son başarılı senkronizasyonun ne zaman başladığını saklayın ve yalnızca o zamandan beri oluşturulan veya güncellenen kayıtları isteyin. Laravel'in zamanlayıcısıyla birleştirilmiş bir Artisan komutu bu iş için biçilmiş kaftandır: sonuçları sayfa sayfa gezer, yerel olarak saklar ve senkronizasyon zamanını ancak her şey başarılı olduktan sonra kaydeder. withoutOverlapping seçeneği, yavaş bir senkronizasyonun asla paralel olarak iki kez çalışmamasını sağlar.

// app/Console/Commands/SyncSageInvoices.php
namespace App\Console\Commands;

use App\Models\SageConnection;
use App\Services\SageClient;
use Illuminate\Console\Command;

class SyncSageInvoices extends Command
{
    protected $signature = 'sage:sync';

    protected $description = 'Sync sales invoices from Sage Accounting';

    public function handle(): void
    {
        SageConnection::whereNotNull('business_id')->each(function (SageConnection $connection) {
            $sage = new SageClient($connection);
            $startedAt = now();
            $page = 1;

            do {
                $response = $sage->request()->get('/sales_invoices', [
                    'updated_or_created_since' => $connection->last_synced_at?->toIso8601String(),
                    'items_per_page' => 200,
                    'page' => $page++,
                ])->throw();

                foreach ($response->json('$items') as $invoice) {
                    // upsert into your local tables
                }
            } while ($response->json('$next'));

            $connection->update(['last_synced_at' => $startedAt]);
        });
    }
}

// routes/console.php
Schedule::command('sage:sync')->everyFifteenMinutes()->withoutOverlapping();

Büyük veri hacmine sahip işletmeler için komuttan her bağlantı adına ayrı bir kuyruk job'ı başlatın; böylece yavaş veya hata veren tek bir işletme diğerlerini geciktirmez. Her çalışmayı kayıt altına alın ve hatalarda uyarı üretin; böylece sorunlar ay sonundan çok önce yakalanır.

 

Sonuç

Sage'i Laravel ile bağlamak, uygulamanıza müşterilerinizin muhasebe verilerine güvenilir ve otomatik erişim kazandırır. OAuth 2.0'ı state doğrulamasıyla kurarak, token'ları şifreli saklayarak, beş dakikalık token'ları cache kilitleriyle güvenle yenileyerek, X-Business başlığını her zaman göndererek ve verileri zamanlayıcıyla 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 ekiple çalışmak, finansal verilerinizin sorunsuz ve güvenli biçimde akmasını sağlar.

sagelaravelsage entegrasyonumuhasebe entegrasyonuweb geliştirme

Bu yazıyı paylaş

Benzer Yazılar

Benzer yazı bulunamadı.

İlgili Hizmetimiz

Laravel 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