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.
- package/.enderun/STATUS.md +3 -4
- package/.enderun/knowledge/SHIM_TEMPLATE.md +11 -0
- package/.enderun/knowledge/evaluation_engine.md +11 -0
- package/.enderun/knowledge/legacy_onboarding.md +11 -0
- package/.enderun/knowledge/project_scaffold_guidelines.md +11 -0
- package/.enderun/knowledge/reference_application_guidelines.md +11 -0
- package/.enderun/logs/manager.json +51 -23
- package/.enderun/memory/PROJECT_MEMORY.md +9 -13
- package/.enderun/observability/README.md +19 -0
- package/README.md +12 -76
- package/bin/hermes-sandbox.js +13 -0
- package/bin/init-check.js +10 -0
- package/docs/getting-started.md +4 -4
- package/package.json +2 -2
- package/src/cli/commands/check.ts +8 -5
- package/src/cli/commands/init.ts +7 -6
- package/src/cli/index.ts +5 -9
- package/src/cli/utils/pkg.ts +40 -5
- package/docs/api-referans.md +0 -1137
- package/docs/is_akislari.md +0 -902
- package/docs/mimari.md +0 -926
- package/docs/moduller.md +0 -294
- package/docs/proje.md +0 -521
- package/docs/yap/304/261.md +0 -2150
package/docs/is_akislari.md
DELETED
|
@@ -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
|
-
|