agent-enderun 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,902 +0,0 @@
1
- # KENTİM — Detaylı İş Akışları ve Süreç Algoritmaları Kılavuzu (MVP Sürümü)
2
-
3
- Bu belge, KENTİM (Belediye Vatandaş Etkileşim ve İş Yönetim Sistemi) platformunda çalışan **tüm ana iş akışlarını, durum makinesi geçişlerini, eskalasyon kurallarını ve operasyonel algoritmaları** adım adım ve görsel şemalarla tanımlamaktadır.
4
-
5
- ---
6
-
7
- ## 1. İş Emri Durum Makinesi (State Machine) Matrisi
8
-
9
- Sistemdeki her iş emri (Work Order) aşağıdaki durum geçiş tablosuna sıkı sıkıya bağlıdır. Yetkisiz bir rolün durumu değiştirmesi API katmanında (RBAC Middleware) engellenir.
10
-
11
- | Mevcut Durum | Tetikleyici Eylem | Yetkili Rol | Yeni Durum | Sistem Arka Plan Eylemi |
12
- | :--- | :--- | :--- | :--- | :--- |
13
- | **- (Başlangıç)** | Vatandaş Rapor Gönderimi | `citizen` / Anonim | **pending** | UUIDv4 atanır. PostGIS Geofencing çalışır, `tenant_id` belirlenir. |
14
- | **pending** | Spam / Kapsam Dışı Kontrolü | `system` | **moderation** / **rejected** | reCAPTCHA v3 skoru < 0.5 ise veya coğrafi sınır dışındaysa otomatik `rejected` olur. |
15
- | **moderation** | Müdürlüğe Yönlendirme | `muni_moderator` / `muni_admin` | **dispatched_to_department** | SLA sayacı başlatılır (Normal şartlarda çalışma saatleri/hafta sonu düşülür; ancak `Acil/Kriz` öncelikli işlerde **7/24 kesintisiz SLA sayacı** tetiklenir). Bildirim gönderilir. |
16
- | **moderation** | Raporu Reddetme | `muni_moderator` / `muni_admin` | **rejected** | Vatandaşa gerekçeli ret bildirimi (SMS/Push) gönderilir. |
17
- | **dispatched_to_department** | Şefe / Ekibe Havale | `muni_department_manager` | **assigned_to_team** | İş, şefin "atanmamış havuzuna" düşer. Şefe push bildirimi gider. |
18
- | **dispatched_to_department** | Doğrudan Personele Havale | `muni_department_manager` | **assigned_to_worker** | **Şef bypass kuralı** tetiklenir. Onay yetkisi doğrudan müdüre yazılır. **Not:** Bu kural yalnızca müdürün şefi bypass etmesi içindir. Şef kendi manuel iş emrini doğrudan personele atarsa onay yetkisi şefe kalır (bkz. proje.md F6.1). |
19
- | **dispatched_to_department** | Re-dispatch (Geri Yönlendirme) | `muni_department_manager` / `muni_admin` | **moderation** | Re-dispatch sayacı 1 artar, SLA çalışmaya devam eder. |
20
- | **dispatched_to_department** | Re-dispatch (Geri Yönlendirme) | `muni_moderator` | **moderation** | Re-dispatch sayacı artmaz (ping-pong koruması için sayılmaz), SLA çalışmaya devam eder. |
21
- | **assigned_to_team** | Personele Görev Atama | `muni_team_chief` | **assigned_to_worker** | Görev saha çalışanının mobil cihazına indirilir (MMKV/SQLite). |
22
- | **assigned_to_team** | Re-dispatch (Geri Yönlendirme) | `muni_department_manager` / `muni_admin` | **moderation** | Re-dispatch sayacı 1 artar, SLA çalışmaya devam eder. |
23
- | **in_progress** | İşi Tamamlama | `muni_worker` | **pending_chief_approval** | 50m geofence mesafe kontrolü yapılır. Mesafe > 50m ise personel "Geofence Bypass Talebi" (EXIF ve gerekçeyle) oluşturabilir. Şef onaylayınca koordinat güncellenir ve geofence bypass edilir. Fotoğraflı kanıtlar yüklenir. |
24
- | **in_progress** | Görevi Yarıda Bırakma | `muni_worker` | **assigned_to_team** / **dispatched_to_department** | İş personelden alınır. **Normal şef atamasında:** şef havuzuna (`assigned_to_team`) iade edilir. **Müdür bypass atamasında:** `dispatched_to_department` (müdür havuzu) olarak iade edilir; `is_manager_bypassed = true` flag'i korunur ve şef devreye girmez. (Vardiya kapatma esnasında `pending_chief_approval` durumundaki işler bu devir işleminden muaftır ve personelin üzerinde kalır). |
25
- | **in_progress** | Re-dispatch (Geri Yönlendirme) | `muni_department_manager` / `muni_admin` | **moderation** | Re-dispatch sayacı 1 artar, SLA çalışmaya devam eder. |
26
- | **pending_chief_approval** | Kanıtları Onayla | `muni_team_chief` / `muni_department_manager` / `muni_admin` | **resolved** | Şef onaylayabilir veya müdür/admin 'manual_approval' aksiyonu ile şef onayını bypass ederek doğrudan resolved durumuna çekebilir. Çakışan veya mobil çevrimdışıyken `409 Conflict` (sürüm uyuşmazlığı) alan iş teslimleri veritabanındaki "Senkronizasyon Uyuşmazlık Havuzuna" (`dispute_logs`) düşer. Şef çakışan kayıtları inceleyip personelin emeğini ("Hak Edişi Onayla") manuel olarak onaylayabilir; bu durumda personelin hanesine hak ediş yazılır. Vatandaşa çözüldü bildirimi ve kanıt linki gönderilir, SLA durdurulur. |
27
- | **pending_chief_approval** | Kanıtları Reddetme | `muni_team_chief` | **in_progress** | Ret sayacı `1` artırılır. Sayaç `>= 3` ise otomatik **pending_manager_review** olur ve müdüre eskalasyon bildirimi gönderilir. (QA Düzeltme Grace Period: şef işi reddettiğinde sisteme otomatik ek 2 saatlik veya min 4 saate yuvarlanmış SLA süresi eklenir). |
28
- | **pending_manager_review** | `force_resolve` — Zorla Çözüldü Yap | `muni_department_manager` / `muni_admin` | **resolved** | "Üst Yetkiyle Kapatma" logu (`force_resolve`) oluşturulur; `muni_admin` performans raporunda ayrı sütunda listelenir. Vatandaşa çözüldü bildirimi gönderilir, SLA durdurulur. |
29
- | **pending_manager_review** | `reassign_worker` — Personel Değiştir | `muni_department_manager` / `muni_admin` | **assigned_to_team** | İş mevcut personelden alınır; ret sayacı sıfırlanır, `is_manager_bypassed` flag'i sıfırlanır (false yapılır). İş, daha önce atanmış olan şefin ekibinin "atanmamış havuzuna" (`assigned_to_team`) düşer. Bu sayede şef kendi görünümünde işi görebilir ve yeni bir personel atar. Eğer şef de değiştirilmek isteniyorsa müdür önce şefi güncellemeli, ardından atama yapmalıdır. Müdür dilerse şef bypass kuralıyla yeni bir atama yapabilir. |
30
- | **pending_manager_review** | `reject` — Gerekçeli İptal | `muni_department_manager` / `muni_admin` | **rejected** | Gerekçe zorunludur. Vatandaşa ret bildirimi (SMS/Push) gönderilir; denetim loguna kaydedilir. |
31
- | **resolved** | Geri Bildirim: 1-2 Yıldız | `citizen` / Anonim | **resolved** (QA Modu) | İş otomatik olarak şefin **Kalite Denetim (QA)** kuyruğuna düşer. SLA sayacı `resolved` durumuna geçtiğinde zaten durdurulmuştu; QA kuyruğunda bekleme süresince **durdurulmuş hâlde kalır** (yeniden başlamaz). **QA SLA Limit ve Otomatik Çözüm:** Şefin QA kuyruğunda bekleyen işleri incelemek için maksimum 5 iş günü (Business Days) süresi vardır. Süre dolduğunda sistem otomatik olarak "Çözümü Kesinleştir" kararı vererek işi resolved olarak kapatır. Şef "İşe Döndür" kararı verirse sayaç dondurulduğu yerden devam eder; "Çözümü Kesinleştir" kararı verirse sayaç değişmez. |
32
- | **resolved** (QA Modu) | Yetersiz Bulma (İşe Döndür) | `muni_team_chief` | **in_progress** | Veritabanında saklanan `assigned_worker_id` referansına göre iş önceki personele iade edilir. Eğer personel sistemde halâ aktif ve aynı ekipteyse iş doğrudan o personele düşer; personel sistemden ayrılmış veya ekip değiştirmişse iş `assigned_to_team` (atanmamış) olarak şefin havuzuna geri düşer. **SLA Grace Period:** İşe döndürmede yapay SLA aşımlarını önlemek için sisteme otomatik ek 2 saatlik (veya kalan sürenin min 4 saate yuvarlanmasıyla) "QA Düzeltme Grace Period" eklenir. SLA dondurulduğu yerden devam eder. |
33
- | **resolved** (QA Modu) | Çözümü Kesinleştir | `muni_team_chief` | **resolved** | İş QA kuyruğundan kalkar; `resolved` statüsü korunur. SLA değişmez. |
34
- | **citizen_report** (`resolved` / `rejected`) | Sorun Devam Ediyor (Reopen) | `citizen` / Anonim | **reopened** | **Yalnızca** kaynak tipi `citizen_report` olan işlere uygulanır. `rejected` bir vatandaş raporu da yeniden açılabilir (moderatör yanlış reddi düzeltme imkânı sağlar). Dahili (`manual_internal`), entegrasyon (`integration`) ve periyodik (`scheduled`) kaynaklı işler vatandaş tarafından açılamaz; bu işler yalnızca amir yetkisiyle (`muni_department_manager` veya üstü) panelden manuel olarak yeniden aktifleştirilebilir ve re-open sayısı uygulanmaz. Maksimum 2 kez yeniden açılabilir; limit dolduktan sonra sistem yeni rapor oluşturulması yönünde uyarı gösterir. **Reopen SLA:** Rapor `reopened` durumuna geçtiğinde eski SLA geçmişi denetim amacıyla saklanır; **yeni SLA henüz başlamaz.** Yeni SLA süresi (standart SLA'in %50'si kadar), moderatörün işi yeniden inceleyip onaylayarak `dispatched_to_department` olarak sevk ettiği anda başlatılır. |
35
- | **resolved / rejected** | Kronik Arıza Bağlantısı | citizen / Anonim (2. reopen limiti sonrası) | **chronic_archived** | Eski rapor arşive alınır; `is_public_visible = false` yapılarak public haritada görünmez; SLA durdurulur. Yeni rapor `pending` olarak açılır ve eski rapora `parent_reopened_work_order_id` ile bağlanır. |
36
- | **Herhangi Durum (resolved hariç)** | "+1 Beni de Etkiliyor" Desteği | `citizen` (JWT — E-posta onaylı hesap zorunlu) | **(Durum Değişmez)** | `support_votes` tablosuna benzersiz `(work_order_id, citizen_id)` kaydı atılır. Anonim kullanıcılar +1 desteği veremez; işlem yalnızca JWT sağlamlığı doğrulanmış e-posta onaylı hesaplar için geçerlidir. `work_orders.support_count` PostgreSQL tabanlı debounced asenkron worker ile güncellenir (row-lock önleme); moderatör ekranına 5 dakika içinde yansıtılır. |
37
- | **Herhangi Durum** | Public Haritada Göster / Gizle | `muni_moderator` / `muni_admin` | **(Durum Değişmez)** | `is_public_visible` değeri güncellenir. Gizleme gerekçesi zorunludur. `PUBLIC_VISIBILITY_CHANGED` denetim logu yazılır ve ilgili önbellek satırları temizlenir. |
38
- | **assigned_to_worker / in_progress** | `muni_worker` Pasife Alınma | `system` (`ROLE_CHANGE_CLEANUP`) | **assigned_to_team** / **dispatched_to_department** | Personelin üzerindeki `in_progress` ve `assigned_to_worker` işleri otomatik `assigned_to_team` (şef havuzu) durumuna çekilir; eğer iş müdür bypass ataması ise doğrudan `dispatched_to_department` (müdür havuzu) durumuna iade edilerek sahibine döner ve `is_manager_bypassed = true` flag'i korunur. `pending_chief_approval` durumundaki işler değişmeden şef onay kuyruğunda kalır (atanan personel alanı pasifleşmiş olarak işaretlenir). Denetim loguna `ROLE_CHANGE_CLEANUP` yazılır. |
39
- | **moderation (Disputes kuyruğundan)** | Admin Kesin Atama | `muni_admin` | **dispatched_to_department** | Re-dispatch sayacı sıfırlanır. İş emrine `is_force_assigned = true` flag'i yazılır. Bu flag aktifken hedef müdürlük/müdür tarafından re-dispatch yapılması API katmanında engellenir (yalnızca `muni_admin` tekrar müdahale edebilir). |
40
-
41
- ---
42
-
43
- ## 2. Görselleştirilmiş Detaylı İş Akışları (Mermaid)
44
-
45
-
46
- ### AKIŞ 1: Vatandaş Raporlama ve Moderasyon Akışı
47
- Vatandaşın sorunu haritada işaretlemesiyle başlayan, arka planda PostGIS geofencing denetiminden geçerek moderatör tarafından müdürlüğe havale edildiği süreçtir.
48
-
49
- ```mermaid
50
- sequenceDiagram
51
- autonumber
52
- actor C as Vatandaş (Hesaplı/Anonim)
53
- participant G as Central SaaS Gateway
54
- participant R as PostGIS Geofence Engine
55
- participant DB as Central & Tenant DB
56
- actor M as Belediye Moderatörü
57
-
58
- Note over C,DB: 1. Önleyici Mükerrer Rapor Kontrolü
59
- C->>G: Haritaya Tıkla & Konum Seç (GPS)
60
- G->>DB: GET /public/reports/nearby?lat=&lng=&radius=100
61
- DB-->>G: 100m Yakındaki Raporları Dön
62
- alt Yakın Konumda Aktif Rapor Varsa
63
- G-->>C: Bilgi Kutusu & "+1 Beni de Etkiliyor" Seçeneğini Göster
64
- alt Vatandaş "+1" Butonuna Basar
65
- C->>G: Raporu Destekle (+1) (JWT Yetkili)
66
- G->>DB: support_votes idempotent ekle & support_count güncelle
67
- G-->>C: İşlem Başarılı & Haritada Soruna Yönlendir
68
- else Vatandaş "Hayır, Farklı Rapor" Der
69
- Note over C,G: Normal Rapor Oluşturma Formuna Devam Edilir
70
- end
71
- end
72
-
73
- Note over C,DB: 2. Normal Rapor Gönderim ve Coğrafi Sınır Akışı
74
- C->>G: Rapor Formu Gönder (Konum + Kategori + Fotoğraf + reCAPTCHA)
75
- G->>G: rate limiting & CAPTCHA doğrula
76
- alt Doğrulama Başarısız
77
- G-->>C: 429 Too Many Requests VEYA 400 Bad Request
78
- else Doğrulama Başarılı
79
- G->>R: Konum Koordinatını Gönder (ST_Contains)
80
- R->>R: Belediye sınır poligonu eşleştirmesini sorgula
81
- alt Sınır Dışı (Kapsam Dışı)
82
- R-->>G: Belediye bulunamadı
83
- G->>DB: Rapor durumunu otomatik 'rejected' kaydet
84
- G-->>C: "Belediyemiz sınırları dışındasınız" hata mesajı
85
- else Sınır İçi (Başarılı)
86
- R-->>G: tenant_id = muni_a (Eşleşti)
87
- G->>DB: UUIDv4 ile 'pending' durumunda rapora kaydet
88
- G-->>C: Takip Kodu Üret (KENT-34-XXXX) & Başarı Sayfası
89
- DB->>M: Raporu Moderasyon Kuyruğuna Ekle ('moderation')
90
- M->>M: 50m mükerrer rapor kontrolü yap (Moderatör ekranı)
91
- alt Mükerrer Kayıt Mevcut
92
- M->>DB: Raporu 'Mükerrer' işaretle ve Ana Raporla bağla (Kapat)
93
- else Tekil Kayıt (Yeni Sorun)
94
- M->>DB: Birincil müdürlüğü seç & SLA öncelik ata & Yönlendir
95
- DB-->>DB: SLA Sayacını Başlat ('dispatched_to_department')
96
- end
97
- end
98
- end
99
- ```
100
-
101
- ---
102
-
103
- ### AKIŞ 2: Departman İçi Atama ve Saha Operasyonu Akışı
104
- Müdürlüğe ulaşan iş emrinin ekiplere dağıtılması, saha personeli tarafından üstlenilmesi, çevrimdışı çalışma kuralları ve geofence bariyerli tamamlama akışıdır.
105
-
106
- ```mermaid
107
- graph TD
108
- Start[İş Departmana Düştü <br/> dispatched_to_department] --> MgrDecision{Müdür Atama Kararı}
109
-
110
- %% Şef Atama Yolu
111
- MgrDecision -->|Şefe Ata| AssignToChief[İş Şef Havuzunda <br/> assigned_to_team]
112
- AssignToChief --> ChiefAssign[Şef Personele Atadı <br/> assigned_to_worker]
113
-
114
- %% Müdür Bypass Yolu
115
- MgrDecision -->|Personele Doğrudan Ata| DirectAssign[Bypass Kuralı: <br/> assigned_to_worker <br/> Onay Yetkisi: Müdür]
116
-
117
- ChiefAssign --> WorkerApp[Personel Cihazına Düştü <br/> Offline Pre-fetch]
118
- DirectAssign --> WorkerApp
119
-
120
- WorkerApp --> WorkerStart[Personel İşi Başlattı <br/> in_progress]
121
- WorkerStart --> WorkerJob{Saha Çalışma Süreci}
122
-
123
- %% Vardiya Kapatma / Devir
124
- WorkerJob -->|Vardiya Kapat / Yarıda Bırak| ShiftEnd{İşin Durumu Nedir?}
125
- ShiftEnd -->|in_progress| Abandon[İş Personelden Alınır <br/> Durum: assigned_to_team]
126
- ShiftEnd -->|pending_chief_approval| Exempt[Devir Muafiyeti! <br/> İş Personelde Kalır]
127
- Abandon --> AssignToChief
128
-
129
- %% Çevrimdışı Senkronizasyon ve Çakışma Kontrolü
130
- WorkerJob -->|Çevrimdışı İşi Bitir| OfflineSubmit[Çevrimdışı Kaydet]
131
- OfflineSubmit --> NetworkRestore[İnternet Bağlantısı Sağlandı]
132
- NetworkRestore --> ConflictCheck{Çakışma Kontrolü: <br/> İş Başkasına Atandı mı?}
133
- ConflictCheck -->|Evet| DisputePool[Senkronizasyon Uyuşmazlık Havuzu <br/> Manuel Çakışma Çözümü Uyarısı]
134
- ConflictCheck -->|Hayır| GeofenceCheck
135
-
136
- %% Çevrimiçi İşi Tamamlama
137
- WorkerJob -->|Çevrimiçi İşi Bitir| GeofenceCheck{Geofence Kontrolü: <br/> Konum ile İş Mesafesi <= 50m?}
138
-
139
- GeofenceCheck -->|Hayır| LockForm[Formu Kilitle! <br/> 'Noktaya Yaklaşın' Uyarısı]
140
- LockForm -->|Yaklaş ve Tekrar Dene| GeofenceCheck
141
-
142
- GeofenceCheck -->|Evet| PhotoUpload[Konum & Zaman Damgalı <br/> Fotoğraf Yükle]
143
- PhotoUpload --> SubmitJob[Şef/Müdür Onayına Gönder <br/> pending_chief_approval]
144
- ```
145
-
146
- ---
147
-
148
- ### AKIŞ 3: Onay, Fotoğraf Reddi (Circuit Breaker) ve QA Döngüsü
149
- İş bitiminde sunulan fotoğrafların şef tarafından denetlenmesi, ardışık ret durumunda müdüre eskalasyon ve vatandaşın düşük puan vermesiyle tetiklenen kalite kontrol akışıdır.
150
-
151
- ```mermaid
152
- graph TD
153
- PendingApp[Şef Onay Bekleyen İş <br/> pending_chief_approval] --> ChiefReview{Şef Fotoğrafları İnceledi}
154
-
155
- %% Onay Yolu
156
- ChiefReview -->|Kanıtlar Yeterli| Approve[İşi Çözüldü Yap <br/> resolved]
157
- Approve --> CitizenRating[Vatandaş Puanlama Yapar <br/> 1-5 Yıldız]
158
-
159
- CitizenRating --> RatingCheck{Puan Durumu}
160
- RatingCheck -->|3-5 Yıldız| Complete[İşlem Başarıyla Tamamlandı]
161
- RatingCheck -->|1-2 Yıldız| QACycle[Kalite Denetim QA Kuyruğuna Düşür]
162
-
163
- QACycle --> ChiefQADecision{Şef QA İncelemesi}
164
- ChiefQADecision -->|Çözümü Kesinleştir| Complete
165
- ChiefQADecision -->|Yetersiz - İşe Döndür| ReOpenToWorker[İşi in_progress Yap <br/> Saha Personeline Bildirim Gönder]
166
- ReOpenToWorker -->|Saha Personeli Düzeltir| PendingApp
167
-
168
- %% Ret ve Devre Kesici Yolu
169
- ChiefReview -->|Kanıtlar Yetersiz| Reject[Fotoğrafı Reddet V1]
170
- Reject --> IncremetCounter[Ret Sayacını 1 Artır]
171
- IncremetCounter --> CounterCheck{Ret Sayacı >= 3?}
172
-
173
- CounterCheck -->|Hayır < 3| SendBackToWorker[İşi in_progress Yap <br/> Personele Bildirim Gönder]
174
- SendBackToWorker -->|Personel Yeni Fotoğraf Yükler| PendingApp
175
-
176
- CounterCheck -->|Evet >= 3| CircuitBreaker[DEVRE KESİCİ TETİKLENDİ! <br/> pending_manager_review]
177
-
178
- CircuitBreaker --> MgrReview{Müdür Eskalasyon Kararı}
179
- MgrReview -->|force_resolve <br/> Zorla Çözüldü Yap| Approve
180
- MgrReview -->|reassign_worker <br/> Başka Personele Aktar| ReassignPool[İş Şefin Atanmamış Havuzuna Döner <br/> assigned_to_team · Ret Sayacı Sıfırlandı]
181
- ReassignPool -->|Şef Yeni Personel Atar| PendingApp
182
- MgrReview -->|reject <br/> Gerekçeli İptal Et| RejectJob[İşi Reddedildi Yap <br/> rejected]
183
- MgrReview -->|manual_approval <br/> Eksiklikleri Onayı Alıp Kapat| Approve
184
- ```
185
-
186
- ---
187
-
188
- ### AKIŞ 4: Yeniden Yönlendirme (Re-dispatch & Ping-Pong Koruması)
189
- Müdürlüklerin işi kendilerine ait olmadığı gerekçesiyle başka birimlere yönlendirmesi ve bunun sonucunda oluşan ping-pong döngüsünün engellenmesi akışıdır.
190
-
191
- ```mermaid
192
- sequenceDiagram
193
- autonumber
194
- actor M1 as Fen İşleri Müdürü
195
- participant DB as Sistem Veritabanı
196
- actor Mod as Belediye Moderatörü
197
- actor Admin as Belediye Admini
198
-
199
- M1->>DB: İş Fen İşleri'ne ait değil. Re-dispatch Et.
200
- DB-->>DB: Re-dispatch sayacını 1 artır
201
- DB->>Mod: İşi Gerekçesiyle Birlikte Moderatörün "Karar Bekleyenler" Kuyruğuna Döndür
202
- Mod->>DB: İşi İnceledi ve Park Bahçeler Müdürü'ne Yönlendirdi
203
-
204
- actor M2 as Park Bahçeler Müdürü
205
- M2->>DB: İş Park Bahçeler'e ait değil. Re-dispatch Et.
206
- DB-->>DB: Re-dispatch sayacını 1 artır (Sayaç = 2)
207
- DB->>Mod: İşi Tekrar Moderatör Kuyruğuna Gönder
208
- Mod->>DB: İşi İnceledi ve Temizlik İşleri Müdürü'ne Yönlendirdi
209
-
210
- actor M3 as Temizlik İşleri Müdürü
211
- M3->>DB: İş Temizlik İşleri'ne de ait değil! Re-dispatch Et.
212
- DB-->>DB: Re-dispatch sayacını 1 artır (Sayaç = 3)
213
- DB-->>DB: PING-PONG KORUMASI DEVREYE GİRDİ!
214
- DB->>Admin: İşi Moderatörden alıp Adminin "Anlaşmazlık Çözüm (Disputes)" Kuyruğuna Taşır.
215
- Admin->>DB: İşi İnceledi ve Temizlik İşleri'ne "Kesin Atama" Yaptı (Döngü Sonlandırıldı).
216
- ```
217
-
218
- > **Not — `is_force_assigned` Kilidi:** Admin kesin atama yaptığında iş emrine `is_force_assigned = true` flag'i yazılır. Bu flag aktifken yeni atanan müdürlük/müdür re-dispatch yapamaz (API `403` döner). Yalnızca `muni_admin` tekrar müdahale edebilir. Re-dispatch sayacı sıfırlanır; iş sıfır sayaçla yeni müdürlüğe düşer. Akış denetim loguna `DISPUTE_RESOLVED` olarak kaydedilir.
219
-
220
- ---
221
-
222
-
223
- ### AKIŞ 5: Dinamik IoT Veri Alma ve Payload Eşleştirme Akışı
224
-
225
- Her belediyenin kendi bağımsız IoT sensörlerinden (doluluk, arıza, konum vb.) gelen değişken ham JSON verilerinin, belediyenin kendi tanımladığı mapping (eşleştirme) veya JS Parser sandbox motorundan geçirilerek standardize edildiği ve otomatik iş emrine dönüştürüldüğü akıştır.
226
-
227
- ```mermaid
228
- sequenceDiagram
229
- autonumber
230
- actor D as IoT Cihazı (Sensör)
231
- participant GW as Central SaaS Gateway
232
- participant TS as Dynamic Payload Transformer Engine
233
- participant DB as Tenant DB (schema_muni_x)
234
- participant SO as İş Emri Durum Makinesi (State Machine)
235
-
236
- D->>GW: POST /v1/integrations/iot/telemetry (API Key + Ham JSON)
237
- GW->>GW: API anahtarından tenant_id tespiti
238
- alt Geçersiz Anahtar
239
- GW-->>D: 401 Unauthorized
240
- else Yetki Onaylandı
241
- GW->>DB: Belediye entegrasyon ayarlarını sorgula (Config önbelleği)
242
- DB-->>GW: Eşleştirme (Mapping) kuralları & Parser Script
243
- GW->>TS: Ham JSON + Parser Script ilet
244
- TS->>TS: V8 Sandbox içinde Custom JS betiğini çalıştır
245
- alt Parser Hatası
246
- TS-->>GW: Parser Error
247
- GW->>DB: integration_logs tablosuna FAILED olarak kaydet
248
- GW-->>D: 422 Unprocessable Entity
249
- else Başarılı Dönüşüm
250
- TS-->>GW: KENTİM Standart IoT Şeması (JSON)
251
- GW->>GW: ST_Contains ile PostGIS Geofence Sınır Kontrolü
252
- alt Sınır Dışı
253
- GW->>DB: integration_logs tablosuna FAILED (Out of Boundaries) kaydet
254
- GW-->>D: 400 Bad Request
255
- else Sınır İçi (Başarılı)
256
- GW->>DB: Telemetri logunu & Cihaz durumunu güncelle
257
- GW->>SO: Eşik Kontrolü (%85 doluluk veya arıza var mı?)
258
- alt Doluluk >= %85 VEYA Durum = faulty
259
- SO->>SO: Aktif / kapatılmamış iş emri kontrolü
260
- alt Aktif İş Emri Zaten Var
261
- SO-->>GW: İş emri oluşturma (Mükerrerlik bypass)
262
- GW-->>D: 200 OK (Telemetri kaydedildi, iş emri zaten var)
263
- else Yeni İş Emri Tetiklendi
264
- SO->>DB: Temizlik/Sıfır Atık Müdürlüğüne "dispatched_to_department" kaydet
265
- DB-->>GW: Yeni iş emri oluşturuldu (work_order_id)
266
- GW-->>D: 201 Created (Yeni İş Emri Oluşturuldu)
267
- end
268
- else Eşik Aşılmadı
269
- GW-->>D: 200 OK (Telemetri kaydedildi, aksiyon gerekmiyor)
270
- end
271
- end
272
- end
273
- end
274
- ```
275
-
276
- ### Dinamik IoT Entegrasyonu Süreç Algoritması
277
-
278
- 1. **İstek Karşılama ve Ön Filtreleme:** Akıllı çöp veya geri dönüşüm cihazı, belediye sistem yöneticisinden aldığı `API_KEY` ile platformun ortak `/v1/integrations/iot/telemetry` adresine veri basar. API Gateway ham telemetri payload boyutu **maksimum 10KB** olacak şekilde ön filtreleme yapar; sınırı aşan istekler `400 Bad Request` ile anında reddedilir.
279
- 2. **Kimlik ve İzolasyon Çözümleme:** API Gateway, gelen istekteki API anahtarı üzerinden veritabanında `tenant_id` sorgusu yapar. RLS kuralları uyarınca veriler anında o belediyenin izole şemasıyla ilişkilendirilir.
280
- 3. **Kuyruklama ve Asenkron Çözümleme (Decoupling):** Ana API thread'ini meşgul etmemek veya parser kilitlenmelerinin Fastify'ı dondurmasını önlemek için, telemetri paketi anlık olarak bir PostgreSQL tabanlı kuyruğa yazılır (`INSERT` into `iot_telemetry_queue`) ve `LISTEN/NOTIFY` mekanizması ile worker'lar tetiklenir, cihaza `202 Accepted` dönülür.
281
- 4. **Dinamik Dönüştürme (Transformer & V8 Sandbox):** Arka planda çalışan warm-worker instance'lar (Isolate Havuzu) kuyruktan gelen veriyi asenkron olarak tüketir:
282
- * Eğer entegrasyon tipi `generic_webhook` ise standart JSON eşleştirmesi uygulanır.
283
- * Eğer entegrasyon tipi `custom_iot_sensor` ise belediyenin girdiği JavaScript kodu izole **V8 Sandbox** motorunda (max 50ms CPU, 16MB RAM) çalıştırılır.
284
- * Dönüştürme sonucunda `"device_id"`, `"device_type"`, `"fill_level_percentage"`, `"status"`, `"coordinates"` ve `"waste_type"` alanları standart formatta elde edilir.
285
- 5. **Coğrafi Sınır (PostGIS) Denetimi:** Standartlaştırılan koordinat bilgisi veritabanında `ST_Contains` fonksiyonuyla o belediyenin PostGIS aktif sınır sürümü (`municipal_boundary_versions` tablosundaki boundaries) kapsamında test edilir. Koordinat sınır dışındaysa işlem sonlandırılır ve log tablosuna `FAILED` yazılır.
286
- 6. **Durum ve Eşik Kontrolü:**
287
- * Sensör doluluk oranı `%85` ve üzerinde ise veya sensör sağlık durumu `"faulty"` ise sistem otomatik olarak **belediye adminin entegrasyon formunda belirlediği hedef müdürlüğe (örn. Temizlik İşleri veya Sıfır Atık)** havale edilmek üzere `dispatched_to_department` durumunda bir iş emri tetikler (moderatör onayı bypass edilir). **402 Grace Period Kısıtı:** Belediyenin aboneliği askıdayken telemetri paketleri sensör tampon bellek taşmalarını önlemek amacıyla 202 Accepted ile kuyruğa alınmaya devam edilir; ancak sandbox ve eşik kontrolü sonrasında doluluk >= %85 olsa dahi yeni iş emri tetiklenmesi Fastify transformer pipeline tarafından aktif olarak engellenir ve telemetri logu static olarak kaydedilir.
288
- * Eğer o cihaz kimliğine (`device_id`) ait henüz çözülmemiş (`resolved` olmayan) aktif bir iş emri varsa, mükerrer iş gücü kaybını engellemek için **yeni bir iş emri oluşturulmaz**, telemetri logu güncellenir.
289
-
290
- ---
291
-
292
- ## 3. Akışların Teknik Validasyon Sınırları
293
-
294
- Tüm iş akışlarında sunucu tarafında (Server-Side) çalıştırılan ve bypass edilmesi imkansız olan validasyon kuralları şunlardır:
295
-
296
- 1. **Idempotency Anahtarı (`x-idempotency-key`)**: Rapor oluşturma (`POST /reports`) veya durum güncelleme (`PATCH /work-orders/:id`) isteklerinde UUIDv4 formatında bir idempotency key gönderilmelidir. Sunucu aynı istek anahtarını görürse işlemi tekrarlamaz, ilk başarılı cevabı döner (24 saatlik önbellek).
297
- 2. **SLA Hesaplama ve Durdurma Noktaları**: SLA sayacı iş `dispatched_to_department` durumuna geçtiğinde başlar. Normal durumlarda belediyenin yerel çalışma saatleri ve hafta sonu tatilleri SLA hesaplamasından düşülür. Ancak `Acil` veya `Kriz` seviyesindeki iş emirlerinde bu kural tamamen bypass edilerek **7/24 kesintisiz SLA sayacı** işletilir. Şef veya müdür işi onaylayıp `resolved` yaptığında, gerekçeli ret verip `rejected` durumuna çektiğinde veya moderatör tarafından **mükerrer rapor olarak işaretlenip kapatıldığında** SLA sayacı anında durdurulur. Mükerrer rapor birleştirmelerindeki anlık durdurma işlemi SLA uyum oranlarına (SLA Compliance) olumlu/nötr olarak yansıtılarak yapay aşımlar önlenir. **SLA Öncelik Çarpanları:** İş öncelik seviyelerine göre SLA süreleri çarpanlarla hesaplanır: `low = x1.5 SLA`, `normal = x1.0 SLA`, `high = x0.5 SLA`, `urgent = x0.25 SLA`, `crisis = x0.1 SLA` (7/24 kesintisiz). **Re-dispatch sürecinde iş moderation kuyruğunda beklerken SLA sayacı durmaz, çalışmaya devam eder.** **QA Döngüsü SLA Davranışı:** İş `resolved` durumuna geçtiğinde SLA sayacı durdurulur. Vatandaş 1-2 yıldız verip iş QA kuyruğuna düştüğünde sayaç durdurulmuş hâlde kalır. Şef QA incelemesinde işi tekrar `in_progress` yaparsa SLA sayacı **dondurulduğu noktadan kaldığı yerden devam eder** (yeniden başlamaz). Şef `resolved` kararı verirse sayaç değişmez. **Ping-Pong Koruma Eşiği:** Re-dispatch sayacı her müdür re-dispatch'inde `1` artırılır. Sayaç `> 2` (yani 3. re-dispatch gerçekleştiğinde) ping-pong koruması devreye girer; iş moderatör kuyruğundan alınarak doğrudan `muni_admin`'in "Anlaşmazlık Çözüm" kuyruğuna taşınır. Moderatör re-dispatch'leri bu sayaca dahil edilmez; yalnızca müdür/admin seviyesindeki re-dispatch'ler sayılır. **Admin anlaşmazlık kuyruğundan kesin atama yaptığında re-dispatch sayacı sıfırlanır, iş sıfır sayaçla yeni müdürlüğe düşer.**
298
- 3. **Vardiya Devir İşlemi**: Personel vardiyayı kapattığında sunucu üzerindeki `in_progress` işleri tarar. Varsa, RLS kapsamında bu işlerin `assigned_worker_id` alanı `NULL` yapılır ve durum `assigned_to_team` olarak güncellenir. Ancak tamamlanıp fotoğrafı yüklenmiş olan ve onay bekleyen `pending_chief_approval` durumundaki işler bu devir işleminden muaftır ve personelin üzerinde kalmaya devam eder.
299
- 4. **GDPR Anonimleştirme ve Derin Temizlik Tetikleyicisi**:
300
- * *Hesap Silme*: Kullanıcı hesabını sildiğinde `muni_admin` onay mekanizması tetiklenir. Onay alındığında vatandaşın `citizen` tablosundaki kişisel verileri silinir; `work_orders` tablosundaki rapor tanımları korunurken, raporu açan vatandaşın kişisel alanları (Ad, Soyad, Telefon, E-posta) maskelenir ve veritabanı loglarındaki IP adresleri `0.0.0.0` olarak güncellenir.
301
- * **Denetim Logu Geçmişi Maskeleme (Yeni — KVKK Zorunluluğu):** Ana silme/anonimleştirme işlemi tamamlandıktan sonra asenkron bir arka plan job'u başlatılır. Bu job, `audit_logs` tablosundaki `old_value` ve `new_value` JSON alanlarında silinen vatandaşa ait kayıtları indeksli `citizen_id` kolonu üzerinden doğrudan filtreleyerek (full table scan yapmadan) tarar ve telefon numarası, T.C. kimlik numarası, e-posta adresi ile ad-soyad bilgilerini `[KİŞİSEL VERİ SİLİNDİ]` mask'iyle değiştirir. Job tamamlandığında `gdpr_requests` tablosundaki ilgili talep kaydı `completed` + `anonymized_at` zaman damgasıyla güncellenir. Bu adımın atlanması KVKK/GDPR ihlali oluşturur.
302
- * *Derin Temizlik Regex Filtreleri*: Gerek vatandaş raporu açıklaması gerekse personelin serbest açıklama metinleri içerisinde bulunabilecek hassas kişisel veriler (telefon numaraları, T.C. Kimlik numaraları, e-posta adresleri) derin regex filtreleri kullanılarak kayıt aşamasında sunucu tarafında maskelenir. Regex tanımları:
303
- * **Telefon**: `(?:\+?90[-. ]?)?5[0-9]{2}[-. ]?[0-9]{3}[-. ]?[0-9]{2}[-. ]?[0-9]{2}` -> `[TELEFON MASKELENDİ]`
304
- * **E-posta**: `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}` -> `[E-POSTA MASKELENDİ]`
305
- * **T.C. Kimlik**: `[1-9][0-9]{10}` -> `[TCKN MASKELENDİ]`
306
- 5. **"+1 Desteği" Idempotency ve Benzersizlik Sınırı**: `support_votes` tablosunda `(work_order_id, citizen_id)` alanı üzerinde benzersiz bir kısıt (unique constraint) bulunur. Kullanıcı aynı rapora birden fazla kez "+1" vermeye çalışırsa sunucu sessizce `200 OK` döner (idempotent davranış), ancak veri tekrar yazılmaz. Rapor çözüldüğünde (`resolved` durumunda) veya silindiğinde destek oyu geri çekilemez.
307
- 6. **Public API Veri Maskeleme ve Güvenlik Sınırı**: Halka açık haritada sunulan raporlarda (`GET /v1/public/reports`) vatandaş kimliği (`citizen_id`, ad, soyad, telefon, e-posta) tamamen maskelenir. Coğrafi koordinatlar, tam noktanın tespit edilerek kişisel mahremiyetin ihlal edilmesini önlemek amacıyla ±0.0005° (yaklaşık 50 metre) fuzzing (sapma) uygulanarak `approximate_coordinates` şeklinde sunulur.
308
- 7. **Kategori Seviyesinde Public Engelleme**: Central Admin tarafından yönetilen global kategorilerde `default_public_visible = false` veya "kategori-level deny" olarak işaretlenmiş olan kategoriler, ilgili belediyenin ayarlarından bağımsız olarak hiçbir koşulda public haritada (`is_public_visible = false`) gösterilemez.
309
- 8. **`ROLE_CHANGE_CLEANUP` — Otomatik Yetki Devir Sınırı**: Herhangi bir belediye personelinin rolü değiştiğinde veya hesabı pasife alındığında, sistem `ROLE_CHANGE_CLEANUP` akışını sunucu tarafında otomatik olarak tetikler; bu bypass edilemez. Hiyerarşi kuralları şunlardır:
310
- * **Müdür pasife alınırsa** → tüm aktif işler `muni_admin` havuzuna.
311
- * **Şef pasife alınırsa** → `in_progress` ve `assigned_to_team` işleri müdür havuzuna; `pending_chief_approval` işlerinin onay yetkisi müdüre geçer.
312
- * **`muni_worker` pasife alınırsa** → `in_progress` ve `assigned_to_worker` işleri `assigned_to_team` (şef atanmamış havuzu) durumuna çekilir; **müdür bypass atamasıyla gelen işler ise `dispatched_to_department` (müdür havuzu) olarak iade edilir** (`is_manager_bypassed = true` korunur); `pending_chief_approval` işleri muaftır ve şefin onay kuyruğunda kalmaya devam eder (atanan personel alanı pasifleşmiş olarak işaretlenir). Tüm akış denetim loguna `ROLE_CHANGE_CLEANUP` olarak kaydedilir.
313
- * **`is_force_assigned` Kilidi**: Admin tarafından anlaşmazlık kuyruğundan kesin atama yapılan iş emirlerinde `is_force_assigned = true` flag'i bulunur. Bu flag aktifken müdür veya şef tarafından re-dispatch yapılması API katmanında reddedilir (`403 Forbidden`); yalnızca `muni_admin` müdahale edebilir.
314
- 9. **Çevrimdışı (Offline) Uyuşmazlık Çözümü Edge-Case Kuralları**: Saha personeli çevrimdışıyken tamamladığı bir iş emrini internet geldiğinde sunucuya senkronize etmeye çalışırken `409 Conflict` (işin başkasına atanmış olması) çakışması yaşanırsa şu kurallar işletilir:
315
- * **Çevrimdışı Handover İstisnası:** Saha çalışanının yerel cihazındaki yerel tamamlanma zaman damgası (local timestamp), sunucunun otomatik vardiya kapatma (auto-handover) zaman damgasından daha önce ise sistem bunu `409 Conflict` olarak reddetmez, doğrudan uyuşmazlık havuzuna (`dispute_logs` tablosu) "Hak Ediş Teyidi Bekliyor" notuyla yönlendirir. Şef onaylayınca hak ediş puanı personelin hesabına yazılır.
316
- * *Senaryo A (Yeni Atanan İşe Başlamamışsa)*: Eğer işin atandığı yeni personel işi henüz başlatmamış (`in_progress` yapmamış) veya tamamlamamışsa; sistem otomatik olarak işi yeni personelden geri alır (`assigned_to_team` havuzuna çeker), iş emrini çevrimdışı bitiren personelin kimliğiyle `pending_chief_approval` durumuna getirir ve şefe bildirim gönderir. Yeni personele görevin iptal edildiğine dair push gider.
317
- * *Senaryo B (Yeni Atanan İşe Başlamış/Bitirmişse)*: Eğer yeni personel işi zaten üstlenmiş veya bitirmişse; çevrimdışı tamamlanan bu eski iş kaydı doğrudan **Senkronizasyon Uyuşmazlık Havuzuna** (`dispute_logs` tablosu) asenkron aktarılır. Şef "Çakışma Çözümü" ekranında iki personelin de kanıtlarını ve zaman damgalarını karşılaştırır. Yerinde incelemeye göre çevrimdışı personelin emeğini `"Hak Edişi Onayla"` butonu ile onaylayabilir. Bu onay, ana işin durumunu bozmaz ancak personelin performans hak ediş puanını sisteme artı olarak yansıtır.
318
- 10. **Hata Dayanıklılığı ve Dead Letter Queue (DLQ) Mekanizmaları**:
319
- * *Bildirim Hataları*: Vatandaş veya personele gönderilecek SMS/E-posta veya Push bildirimleri servis sağlayıcı kesintileri nedeniyle başarısız olursa; sistem PostgreSQL tabanlı asenkron kuyruğunda üst üste 3 kez exponential backoff ile tekrar dener. 3 deneme sonunda halâ başarısız olan bildirim paketleri `notification_dlq` tablosuna (Dead Letter Queue) taşınır ve muni_admin panelinde "Hatalı İletişimler" olarak listelenir.
320
- * *IoT Parser Betiği Çökmeleri*: V8 Sandbox üzerinde çalışan custom parser betiği bir syntax veya bellek hatası nedeniyle çökerse (`crash`); sistem ilgili telemetri paketini `failed_telemetry_dlq` tablosuna ham JSON halinde yazar, ilgili IoT cihazını otomatik olarak "Pasif/Hatalı" durumuna çeker ve admin paneline `INTEGRATION_PARSER_CRASH` audit loguyla beraber kritik sistem uyarısı basar.
321
- 11. **Otomatik Transactional Denetim Loglama (Audit Logging) ve JSONB Geçmişi**:
322
- * **Transactional Bütünlük:** Tüm durum geçişleri, eskalasyonlar, geofence bypass onayları, offline çakışma çözümleri ve yetkilendirilmiş administratör müdahaleleri (`force_resolve`, `manual_approval`, `re-dispatch`, `role_change_cleanup`) veritabanında gerçekleştirilen ana operasyonla aynı **SQL Transaction (`BEGIN ... COMMIT`)** bloğu içerisinde kaydedilir. Bu sayede, iş emri durumu değiştiği halde denetim logunun yazılamaması veya denetim logu yazılıp durumun güncellenememesi gibi veri tutarsızlıkları tamamen önlenir.
323
- * **JSONB Old/New Payloads Oluşturma:** Loglama mekanizması, güncellenen tablonun (örn: `work_orders`, `municipalities`, `users`) ilgili satırının güncellemeden hemen önceki tüm sütun değerlerini `payload_old` (JSONB) alanına, güncellemeden sonraki halini ise `payload_new` (JSONB) alanına kaydeder.
324
- * **Asenkron Tetikleme ve Trigger Kurgusu:** Yoğun durum geçişlerinde veritabanına ek gecikme (latency) bindirmemek için, personelin gerçekleştirdiği durum güncellemeleri PostgreSQL'deki `AFTER UPDATE` satır düzeyindeki trigger'ları veya API katmanındaki asenkron audit interceptor'ları aracılığıyla `audit_logs` tablosuna yazılır.
325
- * **Bypass ve Özel Log Akışları:**
326
- * *Geofence Bypass Onayı:* Şef geofence bypass talebini onayladığında, `event_type = 'GEOFENCE_BYPASS_APPROVED'` logu tetiklenir. `payload_old` içinde `geofence_bypass_requested = true` ve eski personel bilgileri yer alırken, `payload_new` içinde onaylanan yeni koordinat ve `is_manager_bypassed` durumu yansıtılır.
327
- * *Ping-Pong Dispute Çözümü:* Admin dispute kuyruğundan kesin atama gerçekleştirdiğinde `event_type = 'DISPUTE_RESOLVED'` logu yazılır; `payload_old` içinde re-dispatch limitini aşan eski departman bilgileri, `payload_new` içinde ise `is_force_assigned = true` ve atanan kesin departman verisi JSONB olarak yer alır.
328
-
329
- *MVP Sürümü — KENTİM İş Akışları ve Süreç Algoritmaları Belgesi*
330
-
331
- ## EK AKIŞLAR: Public Görünürlük ve "+1 Beni de Etkiliyor" İşlemleri
332
-
333
- ### "+1 Desteği" Aksiyonu (Tekilleştirme)
334
- - Uygulama: `moderation` ve sonrası (`resolved` hariç) durumdaki raporlara hesaplı kullanıcılar "+1" desteği verebilir.
335
- - Veri modeli: `support_votes(work_order_id, citizen_id, created_at)` tablosu ile idempotent unique constraint `(work_order_id, citizen_id)` uygulanır.
336
- - Arka plan: `work_orders.support_count` alanı asenkron debounced worker ile güncellenmektedir (row-locking önleme tasarımı).
337
-
338
- ### Public Görünürlük Yönetimi Akışı
339
- - Moderatör veya admin: raporu public haritadan gizleyebilir veya tekrar gösterebilir (`is_public_visible` boolean toggle).
340
- - Gizleme gerekçesi (zorunlu): `personal_data` / `sensitive_location` / `inappropriate` / `official_investigation` / `other (text)`.
341
- - İşlem sonucu: `public_hidden_reason`, `public_hidden_at`, `public_hidden_by` alanları güncellenir ve `PUBLIC_VISIBILITY_CHANGED` audit log kaydı oluşturulur.
342
- **Not:** `public_map_cache` invalidation mekanizması entegre edilmiştir. Rapor durumu, public görünürlüğü veya destek sayısı değiştiğinde önbellek otomatik olarak temizlenir.
343
-
344
- ### EK - GDPR / Hesap Silme ve IoT Kaynaklı İşlerin Public Davranışı
345
-
346
- 1) GDPR: Hesap Silme ve +1 Oyların Kaldırılması (Cascade)
347
- - Kullanıcı hesabı silinmeye onay verildiğinde aşağıdaki transactional + asenkron akış çalışır:
348
- - `BEGIN TRANSACTION`
349
- - `DELETE FROM support_votes WHERE citizen_id = <deleted_citizen_id>`
350
- - `UPDATE work_orders SET support_count = (SELECT COUNT(1) FROM support_votes WHERE work_order_id = work_orders.id) WHERE id IN (SELECT work_order_id FROM support_votes WHERE citizen_id = <deleted_citizen_id>)`
351
- - `COMMIT`
352
- - **[Asenkron Job — Ayrı Görev]:** `audit_logs` tablosunda silinene ait geçmiş `old_value`/`new_value` alanları regex ile taranarak kişisel veriler `[KİŞİSEL VERİ SİLİNDİ]` ile maskelenir. `gdpr_requests` kaydı `completed` + `anonymized_at` ile güncellenir.
353
-
354
- 2) AKIŞ 5 Genişletme: IoT Kaynaklı İşlerde `default_public` Davranışı
355
- - IoT entegrasyonundan tetiklenen yeni iş emirleri oluşturulurken entegrasyon tanımında bulunan `default_public` boolean alanı okunacak:
356
- - Eğer `default_public = true` ise yeni iş `is_public_visible = TRUE` ile oluşturulur.
357
- - Eğer `default_public = false` ise yeni iş `is_public_visible = FALSE` ile oluşturulur ve moderatör onayı olmadan public görünür yapılmaz.
358
- - Bu davranış, IoT parser/transformer pipeline'ına açıkça eklenmelidir; ayrıca admin panelindeki entegrasyon formunda `default_public` checkbox'u gösterilerek sistem yöneticisinin entegrasyon bazında politika belirlemesine izin verilecektir.
359
-
360
- ### Rapor Oluşturma — Önleyici Mükerrer Akışı (Akış 1: genişletme)
361
- - Adım: Konum seçildiğinde sistem `GET /public/reports/nearby?lat=&lng=&radius=100` çağrısı yapar.
362
- - Eğer yakın konumda aktif rapor bulunursa: kullanıcıya bilgi kutusu gösterilir; kullanıcı +1 verirse form kapanır ve ana sayfadaki harita ilgili pine odaklanarak gösterilir; "Hayır, farklı" derse normal forma devam eder.
363
- Bu ek akışlar, moderatör iş yükünü azaltmak ve vatandaş katılımını artırmak için tasarlanmıştır.
364
-
365
- ---
366
-
367
- ## 4. Gelişmiş Sistem Algoritmaları (MVP Mimarisine Entegre)
368
-
369
- ### Algoritma 5: Asenkron Panik Sinyali ve Debouncing Mimarisi
370
- **Amaç:** Afet Modu aktifken binlerce vatandaştan aynı anda saniyede gelen binlerce `POST /v1/emergency/panic` GPS sinyalinin PostgreSQL I/O tıkanmalarına ve kilitlenmelerine (DB Deadlocks) yol açmasını engellemek, cihaz bazlı istismarları (Spam/DDoS) debouncing katmanı ile engellemek.
371
-
372
- ```javascript
373
- // Sunucu Tarafı Panik Alım ve Hız Sınırlama Akışı
374
- async function handleEmergencyPanic(request, reply) {
375
- const { device_id, latitude, longitude, altitude, citizen_id } = request.body;
376
- const clientIp = request.ip;
377
-
378
- // 1. Cihaz Bazlı Hız Sınırlama (Device-Level Sliding-Window Rate Limiter)
379
- // PostgreSQL tabanlı sayaç veya bir fonksiyon ile hız sınırlama uygulanır.
380
- // Bu, 'panic_rate_limits' gibi bir tabloda device_id ve timestamp tutularak yönetilebilir.
381
- const isRateLimited = await checkAndIncrementPanicRateLimit(device_id);
382
-
383
- if (isRateLimited) {
384
- return reply.status(429).send({ error: "Çok fazla istek, lütfen sakin olun ve bekleyin." });
385
- }
386
-
387
- // 2. Asenkron Kuyruğa Yazma (Fast-Ingest Pipeline via PostgreSQL Table + LISTEN/NOTIFY)
388
- const panicPayload = {
389
- device_id,
390
- citizen_id: citizen_id || 'anonymous',
391
- latitude: latitude.toString(),
392
- longitude: longitude.toString(),
393
- altitude: altitude ? altitude.toString() : '0',
394
- received_at: new Date().toISOString(),
395
- ip: clientIp
396
- };
397
-
398
- // Veritabanı transaction içinde panik sinyali kuyruk tablosuna eklenir
399
- await db.query('INSERT INTO emergency_panic_queue (payload) VALUES ($1) RETURNING id', [panicPayload]);
400
- // LISTEN/NOTIFY ile worker'lara sinyal gönderilir
401
- await db.query("SELECT pg_notify('emergency_panic_channel', $1)", [device_id]);
402
-
403
- // Veritabanı yazma işlemi beklenmeksizin vatandaşa 202 Accepted ve takip UUID'si döndürülür
404
- const tempTrackingId = uuid.v4(); // Gerçek bir tracking_id daha sonra worker tarafından üretilebilir
405
- return reply.status(202).send({
406
- status: "Accepted",
407
- message: "Konumunuz afet koordinasyon merkezine asenkron olarak iletilmiştir.",
408
- tracking_id: tempTrackingId
409
- });
410
- }
411
-
412
- // Arka Plan Consumer Worker Servisi (Bulk Debouncer Engine)
413
- async function panicQueueConsumer() {
414
- const client = await pgPool.connect();
415
- await client.query('LISTEN emergency_panic_channel');
416
-
417
- client.on('notification', async (msg) => {
418
- logger.info('Received notification on emergency_panic_channel:', msg.payload);
419
- // İşleme alınacak mesajlar için bir bekleme veya toplama mekanizması (debouncing) burada uygulanır.
420
- // Örneğin, belirli bir süre içinde gelen bildirimleri toplayıp toplu işlem yapmak.
421
-
422
- // Kuyruktan işlenecek öğeleri çek (toplu okuma)
423
- const unprocessedPanics = await client.query(`
424
- SELECT id, payload FROM emergency_panic_queue WHERE processed = FALSE LIMIT 50 FOR UPDATE SKIP LOCKED;
425
- `);
426
-
427
- if (unprocessedPanics.rows.length === 0) {
428
- return;
429
- }
430
-
431
- const uniquePanics = new Map();
432
-
433
- // Debouncing: Aynı Device ID'den gelen ardışık sinyalleri grupla, sadece en son/güncel koordinatı tut
434
- for (const row of unprocessedPanics.rows) {
435
- const panicObj = row.payload; // payload zaten JSONB olduğu varsayılıyor
436
- uniquePanics.set(panicObj.device_id, {
437
- ...panicObj,
438
- queue_id: row.id // Kuyruk ID'sini de takip et
439
- });
440
- }
441
-
442
- // Toplu Yazma (Bulk Database Transaction)
443
- if (uniquePanics.size > 0) {
444
- const dbWorkerClient = await pgPool.connect(); // İşlemler için ayrı bir client
445
- try {
446
- await dbWorkerClient.query("BEGIN");
447
- for (const [deviceId, data] of uniquePanics) {
448
- // PostGIS Centroid-based Nearest Neighbor (KNN) ile sinyale en yakın aktif belediyeyi (tenant_id) tespit et
449
- const nearestMuni = await dbWorkerClient.query(`
450
- SELECT m.id AS tenant_id
451
- FROM municipalities m
452
- JOIN municipal_boundary_versions mbv ON m.id = mbv.municipality_id
453
- WHERE m.status = 'active' AND mbv.active_to IS NULL
454
- ORDER BY mbv.boundaries <-> ST_SetSRID(ST_MakePoint($1, $2), 4326)
455
- LIMIT 1;
456
- `, [data.longitude, data.latitude]);
457
-
458
- const targetTenantId = nearestMuni.rows[0]?.tenant_id || null; // default null, worker belirlesin
459
-
460
- // PostgreSQL bulk upsert ile kriz masası can haritası tablosuna yazılır
461
- await dbWorkerClient.query(`
462
- INSERT INTO crisis_signals (device_id, citizen_id, coordinates, received_at, tenant_id)
463
- VALUES ($1, $2, ST_SetSRID(ST_MakePoint($3, $4), 4326), $5, $6)
464
- ON CONFLICT (device_id)
465
- DO UPDATE SET coordinates = EXCLUDED.coordinates, received_at = EXCLUDED.received_at, tenant_id = EXCLUDED.tenant_id;
466
- `, [deviceId, data.citizen_id, data.longitude, data.latitude, data.received_at, targetTenantId]);
467
-
468
- // İşlenen panik sinyalini kuyruktan çıkar (veya processed = TRUE yap)
469
- await dbWorkerClient.query(`UPDATE emergency_panic_queue SET processed = TRUE WHERE id = $1`, [data.queue_id]);
470
- }
471
- await dbWorkerClient.query("COMMIT");
472
- } catch (err) {
473
- await dbWorkerClient.query("ROLLBACK");
474
- logger.error("Panic bulk insert error, moving to DLQ: ", err); // Gerekirse DLQ mekanizması eklenebilir
475
- } finally {
476
- dbWorkerClient.release();
477
- }
478
- }
479
- });
480
-
481
- client.on('error', (err) => {
482
- logger.error('PostgreSQL LISTEN client error:', err);
483
- });
484
- }
485
-
486
- // Yardımcı fonksiyon: Basit bir debouncing için rate limit kontrolü (örnek, gerçekte daha kompleks olabilir)
487
- async function checkAndIncrementPanicRateLimit(deviceId) {
488
- const result = await db.query(`
489
- INSERT INTO panic_rate_limits (device_id, last_request_at, request_count)
490
- VALUES ($1, NOW(), 1)
491
- ON CONFLICT (device_id) DO UPDATE SET
492
- request_count = CASE
493
- WHEN (NOW() - panic_rate_limits.last_request_at) < INTERVAL '1 minute' THEN panic_rate_limits.request_count + 1
494
- ELSE 1
495
- END,
496
- last_request_at = NOW()
497
- RETURNING request_count;
498
- `, [deviceId]);
499
-
500
- return result.rows[0].request_count > 5; // 1 dakikada 5 isteği aşarsa rate-limited say
501
- }
502
- ```
503
-
504
- ### Algoritma 6: Geofence Boundary Snapshot Kilitlenme Önleyici Doğrulama
505
- **Amaç:** Belediye sınırları (`boundaries` poligonu) meclis kararı veya coğrafi güncellemeler nedeniyle daraltıldığında ya da değiştirildiğinde, personelin üzerinde açık olan (`in_progress`) işleri kapatırken "Belediye Sınırları Dışında" (lockout) hatası alarak kilitlenmesini engellemek.
506
-
507
- ```sql
508
- -- 1. İş Emri Oluşturulma Anında Sınır Kilitleme (Trigger / Ingestion Aşaması)
509
- CREATE OR REPLACE FUNCTION snap_municipal_boundary_on_creation()
510
- RETURNS TRIGGER AS $$
511
- BEGIN
512
- -- İş emrinin oluşturulduğu andaki aktif belediye PostGIS sınır poligonu sürümü (ID) sorgulanır
513
- -- ve doğrudan work_orders.boundary_version_id kolonuna atanarak kilitlenir.
514
- SELECT id INTO NEW.boundary_version_id
515
- FROM municipal_boundary_versions
516
- WHERE municipality_id = NEW.municipality_id
517
- AND active_from <= NOW()
518
- AND (active_to IS NULL OR active_to >= NOW())
519
- LIMIT 1;
520
-
521
- -- Eğer aktif sınır sürümü bulunamazsa sistem hata fırlatmaz, default fallback sürüm atar.
522
- RETURN NEW;
523
- END;
524
- $$ LANGUAGE plpgsql;
525
-
526
- CREATE TRIGGER trg_lock_boundary_snapshot
527
- BEFORE INSERT ON work_orders
528
- FOR EACH ROW
529
- EXECUTE FUNCTION snap_municipal_boundary_on_creation();
530
- ```
531
-
532
- ```javascript
533
- // 2. İş Kapatma Anında Validasyon Akışı (Server-Side ST_Contains Validation)
534
- async function validateGeofenceOnCompletion(workOrderId, workerLat, workerLng, isBypassRequested) {
535
- // Veritabanından iş emrinin boundary_version_id ve bypass durumu çekilir
536
- const workOrder = await db.query(
537
- "SELECT boundary_version_id, geofence_bypass_approved FROM work_orders WHERE id = $1",
538
- [workOrderId]
539
- );
540
-
541
- if (!workOrder) throw new Error("İş emri bulunamadı.");
542
-
543
- // Eğer personel şef onaylı geofence bypass talebi onaylanmışsa direkt başarılı say
544
- if (workOrder.geofence_bypass_approved) {
545
- return { success: true, bypass: true, message: "Geofence kontrolü şef onaylı bypass edildi." };
546
- }
547
-
548
- // ST_Contains ile personelin son konumunu, boundary_version_id üzerinden JOIN ile kontrol et
549
- const result = await db.query(`
550
- SELECT ST_Contains(
551
- mbv.boundaries,
552
- ST_SetSRID(ST_MakePoint($2, $3), 4326)
553
- ) as is_inside
554
- FROM municipal_boundary_versions mbv
555
- WHERE mbv.id = $1;
556
- `, [workOrder.boundary_version_id, workerLng, workerLat]);
557
-
558
- if (result.rows[0]?.is_inside) {
559
- return { success: true, bypass: false, message: "Personel snap konum sınırları içerisindedir." };
560
- } else {
561
- // Sınır dışındaysa hata fırlatılır; personel geofence bypass talebi ekranına yönlendirilir
562
- return {
563
- success: false,
564
- bypass: false,
565
- message: "Koordinatlarınız iş emrinin oluşturulduğu andaki geçerli hizmet sınırlarının dışındadır. Konum düzeltme/bypass talebi oluşturabilirsiniz."
566
- };
567
- }
568
- }
569
- ```
570
-
571
- ### Algoritma 7: Reopen Sınırı ve Kronik Arıza (Parent Link) Kayıt Zinciri
572
- **Amaç:** Vatandaşların aynı sorunu sonsuza kadar "reopen" ederek spam yapmasını engellerken (max 2 limit), çözülemeyen kronik sorunların tarihsel ve konumsal bağını kaybetmeden yeni biletler (`parent_reopened_work_order_id`) üzerinden takip edilmesini ve analiz edilmesini sağlamak.
573
-
574
- ```javascript
575
- // Rapor Reopen ve Kronik Rapor İlişkilendirme Algoritması
576
- async function processReportReopenOrNew(trackingCode, reopenReason, userNotes) {
577
- // 1. Raporu veri tabanından getir
578
- const report = await db.query("SELECT id, status, reopen_count FROM work_orders WHERE tracking_code = $1", [trackingCode]);
579
- if (!report) throw new Error("Rapor bulunamadı.");
580
-
581
- // 2. Eğer reopen sayısı limitin altındaysa ( < 2 ) normal reopen sürecini işlet
582
- if (report.reopen_count < 2) {
583
- await db.query(`
584
- UPDATE work_orders
585
- SET status = 'reopened',
586
- reopen_count = reopen_count + 1,
587
- reopen_reason = $2,
588
- reopen_notes = $3,
589
- updated_at = NOW()
590
- WHERE id = $1
591
- `, [report.id, reopenReason, userNotes]);
592
-
593
- return {
594
- action: "REOPENED",
595
- message: "Raporunuz başarıyla yeniden açıldı ve incelemeye alındı.",
596
- reopen_count: report.reopen_count + 1
597
- };
598
- }
599
-
600
- // 3. Reopen limiti dolmuşsa ( >= 2 ) buton pasifleşir. Vatandaş yeni rapor oluşturduğunda:
601
- // İstemci yeni rapor oluşturma modalına parent_id = report.id parametresini geçirir.
602
- return {
603
- action: "LOCKED",
604
- message: "Bu rapor için maksimum yeniden açma sınırına (2) ulaşıldı. Lütfen yeni bir sorun kaydı oluşturun.",
605
- parent_reopened_work_order_id: report.id // İstemciye aktarılan kronik ilişki ID'si
606
- };
607
- }
608
-
609
- // Kronik Bağlantılı Yeni Rapor Kaydetme Fonksiyonu
610
- async function createNewChronicReport(data) {
611
- const { category_id, coordinates, description, parent_reopened_work_order_id } = data;
612
-
613
- const result = await db.query(`
614
- INSERT INTO work_orders (
615
- category_id,
616
- coordinates,
617
- description,
618
- parent_reopened_work_order_id, -- Kronik arıza bağlayıcı referans UUID
619
- reopen_count,
620
- status
621
- )
622
- VALUES ($1, ST_SetSRID(ST_MakePoint($2, $3), 4326), $4, $5, 0, 'pending')
623
- RETURNING id, tracking_code;
624
- `, [category_id, coordinates.lng, coordinates.lat, parent_reopened_work_order_id, description]);
625
-
626
- // Eğer parent varsa, eski raporun durumunu 'chronic_archived' olarak güncelle ki haritada kalabalık yapmasın
627
- if (parent_reopened_work_order_id) {
628
- await db.query("UPDATE work_orders SET status = 'chronic_archived' WHERE id = $1", [parent_reopened_work_order_id]);
629
-
630
- // Denetim loguna kronik ilişki zincirini kaydet
631
- await db.query(`
632
- INSERT INTO audit_logs (event_type, description, citizen_id)
633
- VALUES ('CHRONIC_LINK_CREATED', 'Kronik arıza bağlandı. Eski Rapor: ' || $1 || ' -> Yeni Rapor: ' || $2, $3)
634
- `, [parent_reopened_work_order_id, result.id, data.citizen_id]);
635
- }
636
-
637
- return result;
638
- }
639
- ```
640
-
641
- ### Algoritma 8: Çoklu Belediye Rapor Derleme ve Güvenlik Filtreleme Algoritması
642
- **Amaç:** Belediye panellerinde yöneticiler tarafından çalıştırılan dinamik sorgularda (Custom Report Builder) ve SaaS merkez panelindeki (Executive Presentation-Ready Reports) karşılaştırma işlemlerinde, kiracılar arası (cross-tenant) veri sızıntılarını sıfırlamak, devasa verilerde bellek tükenmelerini önlemek ve yüksek performanslı sayfalama (pagination) mekanizmasını işletmek.
643
-
644
- ```javascript
645
- // 1. Belediye Özel Rapor Oluşturucu (Read-Replica & Custom Query Builder with Cursor Pagination)
646
- async function compileMunicipalCustomReport(tenantId, queryConfig) {
647
- const { dimensions, metrics, filters, limit = 50, next_cursor } = queryConfig;
648
-
649
- // Güvenlik Önlemi: Sorguyu doğrudan canlı OLTP yerine read-replica bağlantı havuzuna yönlendir
650
- const replicaClient = await pgReplicaPool.connect();
651
-
652
- try {
653
- let selectClause = dimensions.map(d => sanitizeColumn(d)).join(", ");
654
- let groupByClause = dimensions.map(d => sanitizeColumn(d)).join(", ");
655
-
656
- // Metrik Eşleştirmeleri
657
- let metricClauses = [];
658
- if (metrics.includes("total_jobs")) metricClauses.push("COUNT(id) as total_jobs");
659
- if (metrics.includes("avg_resolution_time")) metricClauses.push("AVG(EXTRACT(EPOCH FROM (resolved_at - created_at)))/3600 as avg_resolution_time_hours");
660
- if (metrics.includes("sla_breach_rate")) metricClauses.push("COUNT(CASE WHEN is_sla_breached = true THEN 1 END)::float / COUNT(id) * 100 as sla_breach_rate");
661
- if (metrics.includes("avg_satisfaction_score")) metricClauses.push("AVG(satisfaction_stars) as avg_satisfaction_score");
662
-
663
- let sql = `
664
- SELECT ${selectClause}, ${metricClauses.join(", ")}
665
- FROM work_orders
666
- WHERE tenant_id = $1 -- %100 Multi-Tenant RLS İzolasyon Güvencesi
667
- `;
668
-
669
- const params = [tenantId];
670
- let paramIndex = 2;
671
-
672
- // Dinamik Filtreler
673
- if (filters.category_id) {
674
- sql += ` AND category_id = $${paramIndex++}`;
675
- params.push(filters.category_id);
676
- }
677
- if (filters.start_date && filters.end_date) {
678
- sql += ` AND created_at BETWEEN $${paramIndex++} AND $${paramIndex++}`;
679
- params.push(filters.start_date, filters.end_date);
680
- }
681
-
682
- // Cursor-Based Sayfalama Validasyonu (Anlık listelemeler ve pivotlar için)
683
- if (next_cursor) {
684
- sql += ` AND created_at < $${paramIndex++}`;
685
- params.push(Buffer.from(next_cursor, 'base64').toString('ascii')); // Cursor çözümü
686
- }
687
-
688
- sql += ` GROUP BY ${groupByClause}`;
689
- sql += ` ORDER BY created_at DESC LIMIT $${paramIndex++}`;
690
- params.push(limit + 1); // Sonraki sayfa kontrolü için +1 kayıt çekilir
691
-
692
- const result = await replicaClient.query(sql, params);
693
-
694
- let hasNextPage = false;
695
- let nextCursorBase64 = null;
696
-
697
- if (result.rows.length > limit) {
698
- hasNextPage = true;
699
- result.rows.pop(); // Fazlalık satırı çıkar
700
- const lastRowDate = result.rows[result.rows.length - 1].created_at;
701
- nextCursorBase64 = Buffer.from(lastRowDate.toISOString()).toString('base64');
702
- }
703
-
704
- return {
705
- data: result.rows,
706
- pagination: {
707
- hasNextPage,
708
- next_cursor: nextCursorBase64,
709
- limit
710
- }
711
- };
712
- } finally {
713
- replicaClient.release();
714
- }
715
- }
716
-
717
- // 2. Central SaaS Karşılaştırmalı Yönetici Raporu Derleme Algoritması (Analytical DB Model)
718
- async function compileCentralExecutiveReport(centralUserId, reportConfig) {
719
- const { municipality_ids, target_metrics, format = 'pdf' } = reportConfig;
720
-
721
- // Güvenlik & RLS Denetimi: Bireysel citizen veya ham work_order verilerine asla erişilmez!
722
- // Sadece asenkron ETL ile `central_analytics_metadata` tablosuna aktarılan agrege veriler taranır.
723
- const sql = `
724
- SELECT
725
- m.name as municipality_name,
726
- cam.month,
727
- cam.total_work_orders,
728
- cam.sla_compliance_rate,
729
- cam.avg_resolution_time_minutes,
730
- cam.citizen_satisfaction_stars
731
- FROM central_analytics_metadata cam
732
- JOIN municipalities m ON cam.municipality_id = m.id
733
- WHERE cam.municipality_id = ANY($1) -- Sadece yetkilendirilmiş belediyeler
734
- ORDER BY cam.sla_compliance_rate DESC;
735
- `;
736
-
737
- const reportData = await db.query(sql, [municipality_ids]);
738
-
739
- if (format === 'pdf') {
740
- // Stilize, modern kurumsal renk paletleriyle (Danger Red #a44a3f, Forest Green #2C5F2D)
741
- // KENTİM logolu, sayfa numaralı vektörel PDF sunum dosyası oluşturulup stream edilir.
742
- return generateStyledPresentationPdf(reportData.rows);
743
- }
744
-
745
- return reportData.rows; // Pivot Excel (XLSX) için ham agrege veri
746
- }
747
-
748
- ### 5. Gelişmiş Dağıtık Sağlık ve Onay Sistem Algoritmaları (Yeni)
749
-
750
- #### Algoritma 10: Merkezi Sistem Sağlığı İzleme ve Dağıtık Polling Döngüsü (Central Health Monitor)
751
- Bu algoritma, Central SaaS sunucusunun kendi servislerinin yanı sıra, tescilli tüm belediyelerin `/ready` sağlık uç noktalarını 1 dakikalık periyotlarla aktif olarak nasıl sorguladığını (polling) ve heartbeat sinyallerini nasıl değerlendirdiğini gösterir.
752
-
753
- ```javascript
754
- // Central SaaS Sağlık Sorgulama Döngüsü (Her 1 dakikada bir tetiklenir)
755
- async function centralSaaSHealthCheckScheduler() {
756
- const municipalities = await db.query(
757
- "SELECT id, name, backend_url, status FROM municipalities WHERE status IN ('active', 'suspended')"
758
- );
759
-
760
- for (const muni of municipalities.rows) {
761
- const start = Date.now();
762
- try {
763
- // HMAC-SHA256 Master Key ile sorgu bütünlük imzası üretme (x-kentim-signature)
764
- const timestamp = Date.now().toString();
765
- const message = `${muni.id}:${timestamp}`;
766
- const signature = crypto.createHmac('sha256', process.env.HMAC_MASTER_KEY).update(message).digest('hex');
767
-
768
- // 1. Belediye sunucusuna /ready (health check) isteği gönderilir
769
- const response = await axios.get(`${muni.backend_url}/ready`, {
770
- headers: {
771
- 'x-kentim-timestamp': timestamp,
772
- 'x-kentim-signature': signature
773
- },
774
- timeout: 5000 // 5 saniye zaman aşımı bariyeri
775
- });
776
-
777
- const latency = Date.now() - start;
778
-
779
- // 2. Yanıt içeriği doğrulanır ve central_health_logs tablosuna yazılır
780
- if (response.status === 200 && response.data.status === 'ok') {
781
- await db.query(`
782
- INSERT INTO central_health_logs (municipality_id, status, latency_ms, db_connected, pg_listener_active, disk_usage_percentage, checked_at)
783
- VALUES ($1, 'UP', $2, $3, $4, $5, NOW())
784
- `, [
785
- muni.id,
786
- latency,
787
- response.data.services.database === 'connected',
788
- response.data.services.pg_listener_active === true,
789
- response.data.services.disk_usage_percentage
790
- ]);
791
- } else {
792
- throw new Error("Sistem servisleri hazır değil.");
793
- }
794
- } catch (error) {
795
- // 3. Hata durumunda pasif heartbeat kaydı kontrol edilir. Son 3 dakika heartbeat alınmış mı?
796
- const lastHeartbeatResult = await db.query(`SELECT last_heartbeat_at FROM muni_heartbeats WHERE municipality_id = $1`, [muni.id]);
797
- const lastHeartbeat = lastHeartbeatResult.rows[0]?.last_heartbeat_at ? new Date(lastHeartbeatResult.rows[0].last_heartbeat_at).getTime() : 0;
798
- const isHealthyByHeartbeat = lastHeartbeat && (Date.now() - lastHeartbeat) < 180000;
799
-
800
- const finalStatus = isHealthyByHeartbeat ? 'DEGRADED' : 'DOWN';
801
-
802
- await db.query(`
803
- INSERT INTO central_health_logs (municipality_id, status, latency_ms, error_message, checked_at)
804
- VALUES ($1, $2, $3, $4, NOW())
805
- `, [muni.id, finalStatus, Date.now() - start, error.message]);
806
-
807
- if (finalStatus === 'DOWN') {
808
- await triggerCentralAlert(muni.id, `Belediye Sunucusu Çöktü veya Yanıt Vermiyor: ${muni.name}`);
809
- }
810
- }
811
- }
812
- }
813
- ```
814
-
815
- #### Algoritma 11: Belediye Giriş Kilidi ve Merkezi SaaS Onay Kontrolü (SaaS Lockout Pipeline)
816
- Belediye personellerinin sisteme giriş talepleri sırasında, belediye backend'inin Central DB/Cache üzerinden belediyenin merkezi onay durumunu (`status`) nasıl kontrol ettiğini ve onaylanmayan belediyelerin girişini nasıl kilitlediğini tanımlar.
817
-
818
- ```javascript
819
- // Belediye Personeli Giriş Doğrulama ve SaaS Lockout Kontrolü
820
- async function validateMunicipalUserLogin(email, password, ipAddress, userAgent) {
821
- // 1. Kullanıcıyı ve şifresini yerel veritabanında doğrula
822
- const user = await db.query(
823
- "SELECT id, tenant_id, password_hash, role, status FROM users WHERE email = $1",
824
- [email]
825
- );
826
-
827
- if (user.rows.length === 0) {
828
- throw new Error("E-posta veya şifre hatalı.");
829
- }
830
-
831
- if (user.rows[0].status === 'passive') {
832
- throw new Error("Kullanıcı hesabınız pasif durumdadır.");
833
- }
834
-
835
- const isPasswordValid = await bcrypt.compare(password, user.rows[0].password_hash);
836
- if (!isPasswordValid) {
837
- throw new Error("E-posta veya şifre hatalı.");
838
- }
839
-
840
- // 2. Merkezi SaaS (Central DB) aktivasyon durumunu kontrol et
841
- // Municipal backend, central tenant registry üzerinden belediye lisans ve aktivasyon durumunu sorgular
842
- const muniStatus = await db.query(
843
- "SELECT status, license_expires_at, grace_period_ends_at FROM central_municipalities WHERE tenant_id = $1",
844
- [user.rows[0].tenant_id]
845
- );
846
-
847
- if (muniStatus.rows.length === 0) {
848
- throw new Error("Belediye tescil kaydı bulunamadı.");
849
- }
850
-
851
- const { status } = muniStatus.rows[0];
852
-
853
- // 3. Central onay kontrolü (Giriş Kilidi)
854
- // Durum 'active' değilse giriş anında reddedilir (Lockout)
855
- if (status !== 'active') {
856
- // Security audit log kaydı yazılır
857
- await logSecurityAudit(user.rows[0].id, 'LOGIN_ATTEMPT_LOCKED', ipAddress, userAgent, {
858
- reason: "Belediye central onaylı değil (aktif değil).",
859
- municipality_status: status
860
- });
861
-
862
- const err = new Error("Belediyeniz henüz merkez (Central SaaS) tarafından onaylanmamıştır veya aboneliği askıdadır.");
863
- err.code = "MUNICIPALITY_NOT_APPROVED";
864
- err.statusCode = 403;
865
- throw err;
866
- }
867
-
868
- // 4. JWT Üret ve başarılı oturum açma logu yaz (Access Token: 15 dakika, Refresh Token Rotation ile desteklenir)
869
- const token = jwt.sign({
870
- userId: user.rows[0].id,
871
- tenantId: user.rows[0].tenant_id,
872
- role: user.rows[0].role
873
- }, process.env.JWT_SECRET, { expiresIn: '15m' });
874
-
875
- await logSecurityAudit(user.rows[0].id, 'LOGIN_SUCCESS', ipAddress, userAgent);
876
-
877
- return {
878
- token,
879
- user: {
880
- id: user.rows[0].id,
881
- email,
882
- role: user.rows[0].role,
883
- tenant_id: user.rows[0].tenant_id
884
- }
885
- };
886
- }
887
- ```
888
-
889
- ```javascript
890
- // Yardımcı Güvenlik Loglama Fonksiyonu
891
- async function logSecurityAudit(userId, eventType, ipAddress, userAgent, metadata = {}) {
892
- await db.query(`
893
- INSERT INTO audit_logs (user_id, event_type, ip_address, user_agent, payload_new, created_at)
894
- VALUES ($1, $2, $3, $4, $5, NOW())
895
- `, [userId, eventType, ipAddress, userAgent, JSON.stringify(metadata)]);
896
- }
897
- ```
898
-
899
- ---
900
-
901
- *MVP Sürümü — KENTİM İş Akışları ve Süreç Algoritmaları Belgesi*
902
-