Brezilya, mal veya hizmet sunan her işletmenin NF-e (Nota Fiscal Eletrônica) düzenlemesini zorunlu kılar — bu, SEFAZ’a anlık olarak sunulan devlet imzalı bir XML’dir. Protokol SOAP tabanlıdır, 2006 yılında geliştirilmiştir, yalnızca Portekizce belgeleri mevcuttur ve homologasyon ortamı hafta sonları çevrimdışı kalır.
Benim için FoxNFe isimli, tüm bunları basit bir REST API’ye dönüştüren çok kiracılı bir SaaS oluşturmak 6 ay sürdü. İşte, zor yoldan öğrendiğim her şey.
Mimarisi
Mimarisi
Tenant App ──REST──► FoxNFe Laravel API ──SOAP/XML──► SEFAZ (27 eyalet)
│
PostgreSQL RLS
Redis + Queues
Certificate Store
- Laravel 11 — API arka uç, kuyruklu işler, servis katmanı
- PostgreSQL + RLS — her kiracı için satır düzeyinde güvenlik
- Redis + Laravel Queues — asenkron işleme (SEFAZ her istek için 2-30 saniye alır)
- A1 Dijital Sertifikaları — her kiracı için şifrelenmiş, çalışma zamanında yüklenen
- 27 SEFAZ son noktası — Brezilya eyaletlerinin her biri için, CNPJ UF ile yönlendirilen
Ders 1: PostgreSQL RLS finally Zorunludur
Ders 1: PostgreSQL RLS
finally ZorunludurPostgreSQL üzerinde çok kiracılı uygulamalar genellikle bir oturum değişkeniyle Satır Düzeyinde Güvenlik kullanır:
CREATE POLICY tenant_isolation ON tenant_plan_usages
USING (
tenant_id = current_setting('app.tenant_id')::int
OR current_setting('app.bypass_rls', true) = );
Admin sorgular için, kiracı sınırlarını aşması gerektiğinde RLS’yi bypass etmeli:
// DANGEROUS without finally
DB::statement("SELECT set_config('app.bypass_rls', 'on', false)");
$usage = TenantPlanUsage::withoutGlobalScopes()->where(, $id)->first();
DB::statement("SELECT set_config('app.bypass_rls', '', false)");
Gönderdiğimiz hata: set_config çağrıları arasında bir istisna, bypass_rls="on" değerinin veritabanı bağlantısı için aktif kalmasına sebep oldu. Bu bağlantı üzerinde yapılan her sonraki sorgu, tüm kiracılardan veri döndürdü.
Düzeltme:
public function getOrCreateUsage(Tenant $tenant): TenantPlanUsage
{
return DB::transaction(function () use ($tenant): TenantPlanUsage {
DB::statement("SELECT set_config('app.bypass_rls', 'on', false)");
try {
$usage = TenantPlanUsage::withoutGlobalScopes()
->where(, $tenant->id)
->lockForUpdate()
->first();
if ($usage === null) {
$plan = $this->getActivePlan($tenant);
$quota = $plan->isEnterprise() ? null : $plan->monthly_note_quota;
$usage = TenantPlanUsage::forceCreate([
=> $tenant->,
=> $plan->,
=> 0,
=> now()->startOfMonth()->toDateString(),
=> now()->endOfMonth()->toDateString(),
=> $quota,
]);
}
return $usage;
} finally {
// İstisna oluşsa bile geri yükle
DB::statement("SELECT set_config('app.bypass_rls', '', false)");
}
});
}
Kural: Her bypass_rls on için bir finally gereklidir. Kod inceleme süreci, bunu bir lint kuralı gibi zorunlu hale getirmelidir.
Ders 2: A1 Sertifikaları — Her şeyi Şifrele, Diskte Hiçbir Şey Yazma
Ders 2: A1 Sertifikaları — Her şeyi Şifrele, Diskte Hiçbir Şey Yazma
Her Brezilyalı şirket, bir ICP-Brasil otoritesi tarafından çıkarılan dijital A1 sertifikasına sahiptir (.pfx dosyası, şifre korumalı). Bu sertifika, her NF-e XML’sini imzalar. Çok kiracılı bir ortamda, her kiracının kendi sertifikası bulunur.
NE YAPMAMALISINIZ: Sertifikaları disk üzerinde CNPJ ile adlandırarak dosya olarak saklamak veya şifrelenmiş içeriği herhangi bir yere kaydetmek.
NE YAPIYORUZ:
// Sakla — DB'ye kaydetmeden önce şifrele
public function storeCertificate(Tenant $tenant, UploadedFile $pfx, string $password): void
{
$tenant->update([
=> encrypt($pfx->get()),
=> encrypt($password),
=> $this->extractExpiry($pfx->get(), $password),
]);
}
// Çalışma zamanında yükle — yalnızca bellekte şifrele, diske hiçbir şey dokunma
public function loadCertificate(Tenant $tenant): array
{
$certData = [];
$success = openssl_pkcs12_read(
decrypt($tenant->),
$certData,
decrypt($tenant->)
);
if (!$success) {
throw new InvalidCertificateException(
{$tenant->});
}
return $certData;// ['cert' => '...PEM...', 'pkey' => '...PEM...']
}
Ek güvenlik önlemleri:
encrypt()Laravel’inAPP_KEY(AES-256-CBC) kullanır — anahtarları dikkatli bir şekilde döndür, yeniden şifreleme planlacertificate_expiresindisi — kiracıları son kullanımdan 30 gün önce uyar, aksi takdirde NF-e yayını durur.pfxdosyasını şifrelenmiş olarak diske yazma,/tmpbile dahil değil
Ders 3: SEFAZ cStat Kodları — Her Biri Önemlidir
Ders 3: SEFAZ cStat Kodları — Her Biri Önemlidir
SEFAZ, bir cStat alanı ile XML döndürür. Çoğu belge yalnızca 100’ü belirtir. İşte üretimde gerçekten göreceğiniz tüm harita:
| cStat | Anlamı | Doğru eylem |
|---|---|---|
| 100 | Yetkilendirilmiş ✅ | XML’i kaydet, kotayı artır |
| 150 | Yetkilendirilmiş (zaman penceresi dışında) | 100 ile aynı |
| 204 | Hali hazırda yetkilendirilmiş (duplikat) | 100 olarak değerlendir |
| 110 | Reddedildi — veri hatası | XML’i düzelt, yeniden gönder |
| 301 | Yetkisiz kullanım (yanlış UF son noktası) | Eyalet yönlendirmesini kontrol et |
| 539 | Şema doğrulama hatası | XML’iniz biçimsel olarak hatalı |
| 999 | SEFAZ iç hata | Üst üste geri çekme ile yeniden dene |
cStat 204’ü tuzak olarak düşünün. Eğer işiniz zaman aşımına uğrarsa ve yeniden denemeler yaparsanız, SEFAZ NF-e’yi zaten işlemeye almış olabilir. 204’ü bir hata olarak değerlendirmek, kota içinde çift kayıt yapılmasına ve kiracıyı kafanızı karıştıracak biçimde yanıltacaktır. Her zaman 204’ü başarı olarak değerlendirin.
private function processResponse(NFeResponse $response): void
{
match (true) {
in_array($response->, [100, 150, 204]) => $this->handleAuthorized($response),
$response->=== 110 => $this->handleDenied($response),
$response->=== 999 => $this->handleRetry($response),
default => $this->handleUnknown($response),
};
}
Ders 4: Kota Artışı Yetkilendirmeden SONRA Olmalıdır
Ders 4: Kota Artışı Yetkilendirmeden SONRA Olmalıdır
İletişim hatası: processResponse() yetkilendirilmiş NF-e’yi kaydetti ama incrementNfeUsage() çağrısını yapmadı. Sonuç: Her kiracı, sonsuza dek sonsuz bir kota aldı.
Düzeltme — artışı yetkilendirme sonrasında yap, ve öyle ki kayıt yapar ama hata vermez:
private function handleAuthorized(NFeResponse $response): void
{
// 1. Önce yetkilendirmeyi kalıcı hale getir
$this->->update([
=> NFeStatus::AUTHORIZED,
=> $response->,
now(),
$response->,
]);
// 2. Kota artışını yap — eğer bu başarısız olursa, NF-e zaten SEFAZ'da yetkilendirilmiştir. Geri dönüşü yok. Hatanın kaydını düş ve devam et.
try {
$this->->($this->);
} catch (\Throwable $e) {
Log::(, [
=> $this->->,
=> $this->->,
=> $e->(),
]);
}
}
Neden hata yerine yakalamalıyız? NF-e artık SEFAZ’da yetkilendirilmiştir — hükümet sisteminde mevcuttur. Eğer buradan bir hata fırlatırsak, iş tekrar eder, SEFAZ 204 (“zaten yetkilendirilmiş”) döner ve sonsuz bir döngüye girmiş oluruz. Kiracı, yetkilendirilmiş NF-e’sine ulaşır ve işlemler, kotaları manuel olarak düzeltmek için bir uyarı alır.
Ders 5: Kiracı Çözümlemesinde Gizli IDOR
Ders 5: Kiracı Çözümlemesinde Gizli IDOR
Kontrolcülerimiz, başlangıçta mevcut kiracıyı X-Tenant-ID başlığından çözümlemekteydi:
// VULNERABLE
private function (Request $request): Tenant
{
return ::(->());
}
// upgrade() bunu kullanıyordu — her oturum açmış kullanıcı, HERHANGİ bir kiracının planını yükseltebiliyordu
function ($request): {
=> ->();
// ... faturalama yükseltme işlemini sürdür
}
Kiracı A için geçerli bir JWT’ye sahip bir saldırgan, X-Tenant-ID: B göndererek B’nin planını yükseltebilir, indirebilir veya kullanım verilerine erişebilir.
Düzeltme — tek satır, her zaman sahipliği doğrula:
private function ($request): {
=> ::(->());
if (->()->!== ->) {
(403, );
}
$tenant;
}
Basit. Ancak, çok kiracılı sistemlerde kiracı ID’sinin istemciden geldiği durumlarda, bu kontrolü varsaymayı unutmak kolaydır. JWT yeterli bir yetkilendirme değildir.
Ders 6: RefreshDatabase Olmadan Test Etme
Ders 6:
RefreshDatabase Olmadan Test EtmeStaging PostgreSQL’umuz DROP TABLE‘e izin vermiyor — güvenlik politikası. RefreshDatabase trait’i tamamen dışarıda. Açık setUp/tearDown kullanıyoruz ve doğrudan temizleme yapıyoruz:
PlanEnforcerTest TestCase
{
?Tenant = ;
function (): void
{
::();
(!->()) {
->();
; // koruma gerektirir — aşağıdaki tearDown notuna bak
}
->=> ::()->();
}
function (): {
// KRİTİK: setUp()'teki markTestSkipped() tearDown()'un çalışmasını engellemez
// $this->tenant null ise (test atlanmış), forceDelete() hata verir. Her zaman null kontrolü yapın.
(->!== ) {
::(, ->->)->();
->->();
}
::();
}
function (): {
// getOrCreateUsage içinde bir istisna simüle et
->(::)
->()
->(\RuntimeException());
{
(::)->(->);
} ($e) {
// Beklenen
}
=> ::();
->(, ->, );
}
}
Bu model — setUp’ta factory, tearDown’da forceDelete, her yerde null kontrolü — 11+ ay boyunca CI koşulları ile güvenilir bir şekilde çalıştı.
Bonus: Gönderimden Önce Yerel XSD Doğrulaması
Bonus: Gönderimden Önce Yerel XSD Doğrulaması
SEFAZ cStat 539 (şema hatası) hangi alanın hatalı olduğunu size söylemez. Hata döngüsü: gönder → 5 saniye bekle → 539 al → neyin yanlış olduğunu tahmin et → tekrar et. Korkunç.
Çözüm: Elektronik imzalamadan önce resmi XSD’ye göre yerel olarak doğrulama yapın:
public function ($xml): {
= \DOMDocument();
->();
= ();
if (!->()) {
= ();
new (
. [0]-> . [0]->);
}
}
Bunu imzalamadan önce ve sonra çalıştırın. SEFAZ XSD’leri portal.nfe.fazenda.gov.br portalında mevcuttur (4 tıklama derinliğinde gömülü).
Sırada Ne Var
Sırada Ne Var
FoxNFe, bu hafta SEFAZ resmi homologasyonuna katılıyor. Sertifika canlıya çıktığında:
- Tüm 27 Brezilya eyaleti için çok durumlu yönlendirme (CNPJ’dan UF otomatik algılama)
- NFS-e (hizmet faturaları) belediye API’leri aracılığıyla — her şehrin kendi protokolü var
- Yetkilendirme, reddetme ve iptal bildirimleri için Webhook bildirimleri
- Kota kullanımı, sertifika son kullanımı sayacı ve NF-e durum zaman çizelgesi ile bir показатьанmış
Brezilya’da mali entegrasyonlar oluşturuyorsanız veya SEFAZ, SOAP veya PHP sertifika yönetimi hakkında sorularınız varsa — yorum bırakmaktan çekinmeyin. Bu konulardan herhangi birine daha derinlemesine girmeye memnuniyetle açığım.
25 yıllık geliştirme deneyimi. Yapıldı: Laravel 11 · PostgreSQL 16 · Redis · Docker · Claude Code (Anthropic)
🔗 foxnfe.centralfox.online | Reddit u/foxdigitaldev
Kaynak: Orijinal Makale
- Mimarisi
- Ders 1: PostgreSQL RLS finally Zorunludur
- Ders 2: A1 Sertifikaları — Her şeyi Şifrele, Diskte Hiçbir Şey Yazma
- Ders 3: SEFAZ cStat Kodları — Her Biri Önemlidir
- Ders 4: Kota Artışı Yetkilendirmeden SONRA Olmalıdır
- Ders 5: Kiracı Çözümlemesinde Gizli IDOR
- Ders 6: RefreshDatabase Olmadan Test Etme
- Bonus: Gönderimden Önce Yerel XSD Doğrulaması
- Sırada Ne Var


