laravel-birfatura maintained by aenzenith
Laravel BirFatura
BirFatura Özel Entegrasyon servisleri için Laravel paketi.
Özel entegrasyonda BirFatura siparişleri sizin sitenizden alır ve faturalarını keser. Kesilen faturanın bağlantısını ve kargo bilgisini de sitenize geri gönderir.
Bu paket, BirFatura'nın bağlandığı adresleri Laravel projenize ekler, gelen isteklerin BirFatura'dan geldiğini doğrular ve yanıtları resmî dokümandaki biçimde hazırlar. Sizin tarafınızda, siparişlerinizi pakete vermeniz ve geri gönderilen fatura ile kargo bilgisini kaydetmeniz yeterlidir.
| Gereksinim | Sürüm |
|---|---|
| PHP | 8.2+ (ext-bcmath) |
| Laravel | 11, 12, 13 |
İçindekiler
Kurulum
composer require aenzenith/laravel-birfatura
php artisan vendor:publish --tag=birfatura-config
php artisan birfatura:token # yeni bir GUID üretir
BIRFATURA_TOKEN=6f1c2a0e-8f5b-4c1e-9a77-2b0d3e4f5a6b
BirFatura panelinde:
- Özel Entegrasyon mağazası oluşturun.
- API Şifresi alanına token'ı girin.
- Mağazanın site adresi olarak
php artisan birfatura:aboutkomutunun yazdığı adresi girin.
$ php artisan birfatura:about
Site address (enter in the panel) ........ https://ornek.com/birfatura
Token ........................................................ resolved
orderStatus https://ornek.com/birfatura/api/orderStatus ........ open
paymentMethods https://ornek.com/birfatura/api/paymentMethods .. open
orders https://ornek.com/birfatura/api/orders .................. open
orderCargoUpdate https://ornek.com/birfatura/api/orderCargoUpdate closed
invoiceLinkUpdate https://ornek.com/birfatura/api/invoiceLinkUpdate open
BirFatura adresin sonuna sabit yolları kendisi ekler. Paket bu rotaları otomatik kaydeder. Rotalar web
ve api gruplarının dışındadır: oturum, CSRF ve uygulamanın api hız sınırı uygulanmaz.
.env dosyasına yalnız token girer (BIRFATURA_TOKEN). Rota
öneki, alan adı, HTTPS zorunluluğu, IP listesi, hız sınırları ve tarih aralığı gibi diğer ayarlar
config/birfatura.php içinden düzenlenir. Tam liste için bkz.
Kurulum ve yapılandırma.
Token
Token çözülemediğinde (boş ya da null) entegrasyon kapalıdır: beş uç da 404 döner.
Token her istekte yeniden çözülür, yani panelden değiştirilen token hemen geçerli olur. Üç yol vardır.
.env içinde düz değer
BIRFATURA_TOKEN=6f1c2a0e-8f5b-4c1e-9a77-2b0d3e4f5a6b
Veritabanındaki (şifreli) ayardan: resolver sınıfı
// config/birfatura.php
'token' => App\Support\BirFatura\SettingsTokenResolver::class,
namespace App\Support\BirFatura;
use Aenzenith\BirFatura\Contracts\TokenResolver;
use App\Models\Setting;
use Illuminate\Support\Facades\Crypt;
final class SettingsTokenResolver implements TokenResolver
{
public function resolve(): ?string
{
$stored = Setting::query()->where('key', 'birfatura_token')->value('value');
// null dönerse entegrasyon kapalı olur (bütün uçlar 404).
return $stored ? Crypt::decryptString($stored) : null;
}
}
Çalışma zamanında closure
// AppServiceProvider::boot()
use Aenzenith\BirFatura\Facades\BirFatura;
BirFatura::resolveTokenUsing(fn (): ?string => app(Vault::class)->get('birfatura'));
Config dosyasına closure konmaz:
php artisan config:cacheclosure'ı serileştiremez. Closure gerekiyorsaresolveTokenUsingkullanın.
Uygulamanın tarafı
Paket yalnız sözleşmeyi, tipleri ve güvenliği getirir. Enum'lar, sağlayıcılar ve handler'lar projeniz tarafından yazılır; hangi siparişin faturalanacağı, id'lerin ne anlama geldiği ve geri yazımın nereye kaydedileceği projenin kararıdır. Aşağıdaki sınıflar yalnız örnektir. Adlarını, konumlarını ve içlerini projenizin modellerine ve kurallarına göre değiştirin. Paketin beklediği tek şey, sınıfın ilgili arayüzü uygulaması ve config'e bağlanmasıdır:
// config/birfatura.php — sınıf adları örnektir, kendi sınıflarınızı yazın
'order_statuses' => App\Support\BirFatura\BirFaturaOrderStatus::class, // enum | OrderStatusProvider | [id => ad]
'payment_methods' => App\Support\BirFatura\BirFaturaPaymentMethod::class, // enum | PaymentMethodProvider | [id => ad]
'orders' => App\Support\BirFatura\OrderSource::class, // OrderProvider (zorunlu)
'invoice_link' => App\Support\BirFatura\StoreInvoiceLink::class, // InvoiceLinkHandler | null
'cargo_update' => App\Support\BirFatura\StoreCargo::class, // CargoUpdateHandler | null
| Config | Uygulanacak arayüz | Uç |
|---|---|---|
order_statuses |
int-backed enum, OrderStatusProvider ya da dizi |
api/orderStatus |
payment_methods |
int-backed enum, PaymentMethodProvider ya da dizi |
api/paymentMethods |
orders |
OrderProvider |
api/orders |
invoice_link |
InvoiceLinkHandler |
api/invoiceLinkUpdate |
cargo_update |
CargoUpdateHandler |
api/orderCargoUpdate |
Bağlanmayan servis 404 döner. Sınıflar container'dan çözülür, constructor injection çalışır.
Aşağıdaki parçaların birbirine bağlı, eksiksiz hâli (config, token resolver, iki enum, sipariş sağlayıcı, iki handler ve olay dinleyicileri) Tam örnek sayfasındadır.
1. Sözlükler: sipariş durumları ve ödeme yöntemleri
BirFatura bu id'leri kendi tarafında saklar ve siparişleri çekerken orderStatusId olarak geri gönderir.
Bu nedenle id'ler sabit kalmalıdır. Id'yi dizideki sıradan türetmeyin.
// Örnek — durumlar ve eşlemeleri projenize göre tanımlanır.
namespace App\Support\BirFatura;
use Aenzenith\BirFatura\Contracts\HasBirFaturaLabel;
enum BirFaturaOrderStatus: int implements HasBirFaturaLabel
{
case Approved = 1;
case Shipped = 2;
case Cancelled = 3;
public function birFaturaLabel(): string
{
return match ($this) {
self::Approved => 'Onaylandı',
self::Shipped => 'Kargolandı',
self::Cancelled => 'İptal Edildi',
};
}
/** Projenin kendi sipariş durumlarına eşleme — tamamen projeye özgü. */
public function orderStatuses(): array
{
return match ($this) {
self::Approved => ['paid', 'completed'],
self::Shipped => ['shipped'],
self::Cancelled => ['cancelled', 'refunded'],
};
}
}
// Örnek
namespace App\Support\BirFatura;
use Aenzenith\BirFatura\Contracts\HasBirFaturaLabel;
enum BirFaturaPaymentMethod: int implements HasBirFaturaLabel
{
case CreditCard = 1;
case BankTransfer = 2;
case CashOnDelivery = 3;
public function birFaturaLabel(): string
{
return match ($this) {
self::CreditCard => 'Kredi Kartı',
self::BankTransfer => 'Banka EFT-Havale',
self::CashOnDelivery => 'Kapıda Ödeme Nakit',
};
}
}
Enum yerine dizi ([1 => 'Kredi Kartı', 2 => 'Havale']) ya da listeyi veritabanından okuyan bir
OrderStatusProvider / PaymentMethodProvider sınıfı da verilebilir. Bkz.
Sözlükler.
2. Siparişler
Paket isteği doğrular ve sağlayıcıya tipli bir OrdersQuery verir. Tarihler dd.MM.yyyy HH:mm:ss
biçiminde katı ayrıştırılır, aralık en fazla 31 gündür. Sağlayıcı yalnız faturalanması gereken
siparişleri döner. Hangi siparişlerin bu kapsama girdiği projenin kararıdır.
// Örnek — sorgu, alan adları ve kurallar projenin modeline göre yazılır.
namespace App\Support\BirFatura;
use Aenzenith\BirFatura\Contracts\OrderProvider;
use Aenzenith\BirFatura\Data\BillingParty;
use Aenzenith\BirFatura\Data\Order;
use Aenzenith\BirFatura\Data\OrderLine;
use Aenzenith\BirFatura\Data\OrdersQuery;
use Aenzenith\BirFatura\Data\PricePair;
use Aenzenith\BirFatura\Data\ShippingParty;
use Aenzenith\BirFatura\Data\Totals;
use App\Models\Order as ShopOrder;
final class OrderSource implements OrderProvider
{
public function orders(OrdersQuery $query): iterable
{
$status = BirFaturaOrderStatus::from($query->statusId);
$orders = ShopOrder::query()
->with('items')
->whereIn('status', $status->orderStatuses())
->whereBetween('updated_at', [$query->from, $query->to])
->lazy(); // iterable: bellek sabit kalır
foreach ($orders as $order) {
yield $this->toBirFatura($order);
}
}
private function toBirFatura(ShopOrder $order): Order
{
$billing = $order->is_corporate
? BillingParty::corporate(
name: $order->company_name,
taxOffice: $order->tax_office,
taxNumber: $order->tax_number,
address: $order->billing_address,
town: $order->billing_town,
city: $order->billing_city,
mobilePhone: $order->phone,
email: $order->email,
)
: BillingParty::individual(
name: $order->full_name,
identityNumber: $order->tckn,
address: $order->billing_address,
town: $order->billing_town,
city: $order->billing_city,
mobilePhone: $order->phone,
email: $order->email,
);
$lines = $order->items->map(fn ($item): OrderLine => new OrderLine(
productId: $item->product_id,
productCode: $item->sku,
productName: $item->name,
vatRate: $item->vat_rate,
unitPrice: PricePair::fromTaxIncluding($item->unit_price, $item->vat_rate), // BİRİM fiyat
quantity: $item->quantity,
))->all();
return new Order(
id: $order->id,
code: $order->number,
date: $order->paid_at,
billing: $billing,
shipping: ShippingParty::fromBilling($billing), // fiziksel teslimat varsa kendi adresiyle kurun
paymentTypeId: $order->payment_method_id, // ödeme sözlüğündeki id
totals: new Totals(
paid: PricePair::fromTaxIncluding($order->total, 20),
products: PricePair::fromTaxIncluding($order->subtotal, 20),
discount: PricePair::fromTaxIncluding($order->discount, 20),
),
lines: $lines,
currency: 'TRY',
customerId: $order->user_id,
);
}
}
idveproductIdint|stringkabul eder. Sözleşmeintegerönerir.- Tutarlar
Amountile bcmath'le hesaplanır, float kayması olmaz.PricePair::fromTaxIncluding/fromTaxExcludingKDV'nin öbür tarafını türetir. İki tutar da elinizdeysePricePair::of($haric, $dahil)kullanın. - Opsiyonel alanlar (
ShippingChargeTotal,ExtraFees,Variants,InvoiceExplanation…)nullbırakıldığında yanıta hiç yazılmaz. - Zorunlu bir alanı boş olan sipariş listeden atlanır,
OrderSkippedolayı yayınlanır ve diğer siparişler yine gider (orders_options.skip_invalid).
Bütün alanlar için bkz. Sipariş verisi. Her parametrenin hangi sözleşme alanına karşılık geldiği Veri referansı sayfasındadır.
3. Fatura bağlantısı (opsiyonel)
BirFatura faturayı kestikten sonra bağlantıyı, numarayı ve tarihi gönderir. Paket faturaUrl değerini
yalnız HTTPS ve *.birfatura.com adreslerinden kabul eder. Handler idempotent olmalıdır:
BirFatura aynı güncellemeyi tekrar gönderebilir.
// Örnek — nereye ve nasıl kaydedileceği projeye özgüdür.
namespace App\Support\BirFatura;
use Aenzenith\BirFatura\Contracts\InvoiceLinkHandler;
use Aenzenith\BirFatura\Data\HandlerResult;
use Aenzenith\BirFatura\Data\InvoiceLinkUpdate;
use App\Models\Order;
final class StoreInvoiceLink implements InvoiceLinkHandler
{
public function handle(InvoiceLinkUpdate $update): HandlerResult
{
$order = Order::find($update->orderId); // string olarak gelir
if ($order === null) {
return HandlerResult::notFound(); // 404 "Sipariş bulunamadı."
}
$order->update([
'invoice_url' => $update->url,
'invoice_number' => $update->number, // ?string
'invoice_date' => $update->date, // ?CarbonImmutable
]);
return HandlerResult::ok(); // 200 "Fatura bağlantısı güncellendi."
}
}
4. Kargo (opsiyonel)
// Örnek
namespace App\Support\BirFatura;
use Aenzenith\BirFatura\Contracts\CargoUpdateHandler;
use Aenzenith\BirFatura\Data\CargoUpdate;
use Aenzenith\BirFatura\Data\HandlerResult;
use App\Models\Shipment;
final class StoreCargo implements CargoUpdateHandler
{
public function handle(CargoUpdate $update): HandlerResult
{
Shipment::updateOrCreate(
['order_id' => $update->orderId],
[
'status_id' => $update->statusId,
'tracking_code' => $update->trackingCode,
'tracking_url' => $update->trackingUrl,
'company' => $update->company,
],
);
return HandlerResult::ok();
}
}
HandlerResult üç biçimde döner: ok(?mesaj) → 200, notFound(?mesaj) → 404,
rejected('mesaj', 409) → verilen kod. Yanıt her zaman sözleşmenin biçimindedir:
{"Success": true, "Message": "..."}. Bkz. Geri yazım.
Olaylar
AuthenticationFailed, OrdersPulled, OrderSkipped, InvoiceLinkReceived, CargoUpdateReceived
(Aenzenith\BirFatura\Events). İzleme ya da uyarı için dinlenebilir. Olaylar token değerini taşımaz.
Event::listen(fn (\Aenzenith\BirFatura\Events\OrderSkipped $event) => report(
new RuntimeException("BirFatura siparişi atladı: {$event->order->code}")
));
Test
Projenin testlerinde uçlar BirFatura'nın çağırdığı gibi çağrılır:
use Aenzenith\BirFatura\Testing\MakesBirFaturaRequests;
uses(MakesBirFaturaRequests::class);
it('faturalanacak siparişleri verir', function (): void {
config()->set('birfatura.token', '6f1c2a0e-8f5b-4c1e-9a77-2b0d3e4f5a6b');
$this->birFaturaOrders(1, now()->subDay(), now())
->assertOk()
->assertJsonPath('Orders.0.OrderCode', 'ORD-2026-000001');
});
it('fatura bağlantısını kaydeder', function (): void {
$this->birFaturaCall('invoiceLinkUpdate', [
'orderId' => 14948,
'faturaUrl' => 'https://uygulama.birfatura.com/dosyagetir?guid=x.pdf',
'faturaNo' => 'ARS2026000000001',
])->assertOk()->assertJson(['Success' => true]);
});
Paketin kendi testleri için composer test, composer analyse ve composer format kullanılır.
Güvenlik
Paket, gövdeden tek bayt okumadan önce çağıranı doğrular. Sırasıyla:
- Entegrasyon kapalıysa 404.
- HTTPS değilse 403.
- IP izin listesi dışındaysa 403.
- Genel hız sınırı aşıldıysa 429.
- Başarısız token kilidi devredeyse 429.
tokenbaşlığı yanlışsa 401. Karşılaştırmahash_equalsile yapılır.- Gövde JSON nesnesi değilse 400.
Token yalnız başlıktan okunur; hiçbir yanıta ya da log kaydına yazılmaz. Uygulamada çıkan bir hata dışarıya
sabit bir mesajla yansır: istisna metni, yığın izi ve SQL sızmaz. Uygulama bir proxy arkasındaysa
TrustProxies doğru ayarlanmalıdır. Ayrıntılar Güvenlik sayfasında.
Dokümantasyon
docs/ klasöründe:
- Kurulum ve yapılandırma
- Kimlik doğrulama
- Sipariş verisi
- Sözlükler
- Geri yazım
- Güvenlik
- Olaylar ve loglama
- Veri referansı: hangi alan neye karşılık gelir
- Tam örnek: App\Support\BirFatura
- Test
Lisans
MIT. Ayrıntı için LICENSE.