API Endpoint Listesi
Tüm endpoint'ler JWT authentication gerektirir (aksi belirtilmedikçe).
Auth
| Method | Path | Açıklama | Auth |
|---|---|---|---|
| POST | /api/auth/login | Email/password ile giriş | Hayır |
| POST | /api/auth/refresh | Token yenileme | Hayır |
| GET | /api/auth/me | Mevcut kullanıcı profili | Evet |
Users & Roles
| Method | Path | Açıklama |
|---|---|---|
| GET | /api/users | Kiracı kullanıcıları |
| POST | /api/users | Yeni kullanıcı |
| GET/POST | /api/roles | Rol listele/oluştur |
Regions & Subregions
| Method | Path | Açıklama |
|---|---|---|
| GET/POST | /api/regions | Bölge listele/oluştur |
| GET/PUT/DELETE | /api/regions/{id} | Bölge detay/güncelle/sil |
| GET/POST | /api/subregions | Alt bölge listele/oluştur |
| GET/PUT/DELETE | /api/subregions/{id} | Alt bölge detay/güncelle/sil |
Gateways
| Method | Path | Açıklama |
|---|---|---|
| GET | /api/gateways | Gateway listesi |
| GET | /api/gateways/mqtt | Lightweight MQTT gateway listesi |
| POST | /api/gateways | Yeni gateway |
| PUT/DELETE | /api/gateways/{id} | Gateway güncelle/sil |
| POST | /api/gateways/{id}/rawconfig/sync | Raw config push |
| GET | /api/gateways/{id}/rawconfig/preview | Config önizleme |
Devices
| Method | Path | Açıklama |
|---|---|---|
| GET/POST | /api/devices | Cihaz listele/oluştur |
| GET/PUT/DELETE | /api/devices/{id} | Cihaz detay/güncelle/sil |
| GET | /api/devices/{id}/hierarchy | Cihaz ağacı |
| GET | /api/devices/{id}/children | Alt cihazlar |
| GET | /api/devices/{id}/energy-consumption | Enerji tüketimi |
| GET | /api/devices/device-templates | Template listesi |
| GET | /api/devices/device-templates/{id} | Template detay |
| GET | .../register-map/download | Register map indir |
| PUT | .../register-map | Register map yükle |
Measurements
| Method | Path | Açıklama |
|---|---|---|
| GET | /api/measurements/map-data | Harita snapshot |
| GET | /api/measurements/aggregated | Aggregated veri (hourly/daily/monthly) |
| GET | /api/measurements/devices/{device_id}/aggregated | Cihaz tüketim toplulaştırması (hourly/daily/monthly) |
| GET | /api/measurements/devices/{device_id}/aggregated-production | Cihaz PV üretim toplulaştırması (üretim muadili; hourly/daily/monthly) |
| GET | /api/measurements/reactive-ratio | Reaktif oran |
| POST | /api/measurements/export | CSV/Excel/PDF export |
.../aggregated-productionnotu:.../aggregated(tüketim) endpoint'inin PV üretim muadilidir; aynıAggregatedResponseşemasını döner. Farklar:total_energy= PV üretim kWh deltası (≥ 0clamp);total_reactive_energyher zamannull; raw-fallback yolundapower_factor/voltage/currentnull. Detay: API changelog (docs/api/CHANGELOG.md, 2026-07-03).
is_estimated+total_energysentezi (her iki aggregated ucu; 2026-07-21):AggregatedResponse.data[]satırlarına additiveis_estimated: bool(defaultfalse) alanı eklendi. Kümülatif enerji register'ı flat/sparse (delta NULL) ama cihaz güç raporluyorsatotal_energyavg_power × bucket_saatiile sentezlenir ve o satıris_estimated: true(TAHMİN) olur; register deltasından ölçüldüysefalse. Ne register ne güç →total_energy: null(is_estimated: false). Ayrıca gerçek0.0artıknullyerine0döner (0= ölçüldü/yok,null= veri-yok). Breaking YOK (opsiyonel alan; tip/birim aynı). Detay: API changelog (docs/api/CHANGELOG.md, 2026-07-21).
Alarms
| Method | Path | Açıklama |
|---|---|---|
| GET/POST | /api/alarms | Alarm policy listele/oluştur |
| GET/PUT/DELETE | /api/alarms/{id} | Policy detay/güncelle/sil |
| GET | /api/alarms/incidents | Tetiklenen alarmlar |
| GET | /api/alarms/incidents/count | Aktif alarm sayıları |
| GET | /api/alarms/incidents/{id} | Alarm detay |
| POST | /api/alarms/incidents/{id}/ack | Alarm onayla |
| POST | /api/alarms/incidents/{id}/snooze | Alarm ertele |
| POST | /api/alarms/incidents/{id}/close | Alarm kapat |
| GET | /api/alarms/incidents/{id}/events | Alarm audit trail |
PR-7d —
AlarmIncidentResponse(incident yanıtı):device_idnullable +alarm_type(additive). Mahsuplaşma/EPİAŞ sistem alarmları cihazsızdır — incident yanıtındadevice_id: UUID | nullolur (null = cihazsız sistem alarmı; cihazlı alarmlarda davranış aynen dolu). Grup/tesis bağlamıpayloadiçinde taşınır (group_id/group_name/facility_id/facility_name/rule_code+scopes). Ayrıca additivealarm_type: string | nullalanı eklendi (policy'den join'lenir; FE i18n etiket çözümü için). Breaking YOK (device_idtipi genişledi, daralmadı;alarm_typedefaultnull). Şema evrimi: mig 0100 (alarms.device_idDROP NOT NULL + dedup index NULLS NOT DISTINCT) + mig 0101 (33 tenant-global sistem alarm politikası seed — 29 mahsuplaşma + 4 EPİAŞ; in-app-only). Sistem alarm politikalarıdevice_id=NULLtenant-global'dir vePOST /api/alarmsalarm-tanımlama yüzeyinde görünmez (otomatik gelir). Detay:docs/api/CHANGELOG.md(2026-07-07, PR-7d girdisi); kullanıcı kılavuzu: Sistem Alarmları.
Firmware & OTA
| Method | Path | Açıklama |
|---|---|---|
| POST | /api/firmware/versions | Firmware upload |
| GET | /api/firmware/versions | Firmware listesi |
| GET | /api/firmware/versions/{id} | Firmware detay |
| GET | /api/firmware/versions/{id}/download | Firmware indir |
| PATCH | /api/firmware/versions/{id} | Metadata güncelle |
| POST | /api/firmware/versions/{id}/release | Release |
| POST | /api/firmware/versions/{id}/deprecate | Deprecated işaretle |
| POST | /api/ota/updates | Tekil OTA başlat |
| POST | /api/ota/updates/batch | Batch OTA |
| GET | /api/ota/updates | OTA listesi |
| GET | /api/ota/updates/{id} | OTA detay |
| POST | /api/ota/updates/{id}/cancel | OTA iptal |
| POST | /api/ota/updates/{id}/retry | OTA tekrar dene |
LOTO
| Method | Path | Açıklama |
|---|---|---|
| GET/POST | /api/loto/points | İzolasyon noktaları |
| GET/POST | /api/loto/sessions | LOTO oturumları |
| GET/PUT | /api/loto/sessions/{id} | Oturum detay/güncelle |
| POST | /api/loto/sessions/{id}/approve | Onayla |
| POST | /api/loto/sessions/{id}/reject | Reddet |
| POST | /api/loto/sessions/{id}/apply | Uygula |
| POST | /api/loto/sessions/{id}/release | Serbest bırak |
| POST | /api/loto/locks | Kilit uygula |
| DELETE | /api/loto/locks/{id} | Kilit kaldır |
Zigbee
| Method | Path | Açıklama |
|---|---|---|
| POST | /api/v1/zigbee/claims/generate | Claim code oluştur |
| GET | /api/v1/zigbee/claims/pending | Bekleyen claim'ler |
| DELETE | /api/v1/zigbee/claims/{id} | Claim iptal |
| GET | /api/v1/zigbee/gateways | Zigbee gateway'ler |
| GET/PUT/DELETE | /api/v1/zigbee/gateways/{id} | Gateway CRUD |
| GET | /api/v1/zigbee/gateways/{id}/devices | Gateway cihazları |
Widgets
| Method | Path | Açıklama |
|---|---|---|
| GET | /api/subregions/{id}/widgets | Widget listesi |
| POST | /api/widgets | Widget oluştur |
| PUT/DELETE | /api/widgets/{id} | Widget güncelle/sil |
| GET | /api/subregions/{id}/widgets/data | Toplu veri — alt bölgedeki tüm aktif widget'ların verisi tek yanıtta |
| GET | /api/widgets/{id}/data/device-read-rate | Cihaz okuma oranı |
| GET | /api/widgets/{id}/data/consumption-chart · consumption-numeric | Tüketim (grafik / sayısal) |
| GET | /api/widgets/{id}/data/cost-chart · cost-numeric | Maliyet (grafik / sayısal) |
| GET | /api/widgets/{id}/data/reactive | Reaktif oran |
| GET | /api/widgets/{id}/data/realtime-value · realtime-gauge · realtime-3phase · realtime-power | Anlık ölçüm kartları |
Tüm veri uçları read:measurement izni ister ve tenant-scoped'tur. Toplu uçta
alt bölge çağıranın kiracısına ait değilse 404 döner (cross-tenant bilgi
sızdırılmaz). Toplu yanıttaki her widget'ın data gövdesi, karşılık gelen
per-widget ucunun yanıtıyla birebir aynıdır; bir widget'ın verisi
hesaplanamazsa yalnız onun error alanı dolar (kısmi başarı, yanıt yine
200).
realtime-value / realtime-gauge / realtime-3phase / realtime-power (ve
toplu uçtaki karşılıkları) yalnızca son 60 dakika içindeki ölçümü döndürür.
Daha eskisi için değer alanları ve timestamp null, device_status
"offline" olur; HTTP durumu değişmez (200). Pencere sabit değildir —
cihaz durumu "offline" eşiğinden türetilir. İstemciler null değeri ele
almalıdır (alanlar zaten nullable idi; değişen dağılımdır).
RealtimeGaugeData alan adları (2026-07-29)realtime-gauge yanıtında: min_value → min_range, max_value →
max_range, danger_threshold → critical_threshold. Widget fiilen
bozuktu (seçilen aralık hiç uygulanmıyor, yüzde hesabı geçersiz çıkıyordu);
kanonik ad frontend'in kullandığı adlar seçildi çünkü kayıtlı widget config'leri
o adlarla dolu. İstek (config) tarafı geriye uyumludur — eski anahtarlar
okunmaya devam eder. Ayrıntı ve istemci uyarlaması: docs/api/CHANGELOG.md
(2026-07-29 girdisi).
Dönem çözümü (dönem alan tüm veri uçları): config.period → time_range →
"7d". time_range deprecated değildir; kullanıcının seçimini taşıyan
geriye uyumluluk kaynağıdır. DeviceReadRateData yanıtına additive
period_label alanı eklendi (nullable — eski yanıtlarda yoktu).
widget_type sözleşmesi (POST/PUT gövdesi): 13 kanonik değer — Pydantic
Literal + DB ck_widget_type CHECK birebir senkron (parity sentineli
test_widget_type_contract_parity.py). 13. değer mahsuplasma_balance
(PR-7c, 2026-07-06; migration 0099 — 12 → 13, additive/breaking YOK).
Geçersiz widget_type → 422. Not: osos_consumption_chart ve
mahsuplasma_balance verilerini widget veri servisinden DEĞİL, kendi feature
GET'lerinden çeker (frontend-direct — mahsuplasma_balance için
GET /api/mahsuplasma/groups/{id}/dashboard-summary, okuma izni
read:netting); widget CRUD sözleşmesi bu tiplerle değişmedi. Detay:
docs/api/CHANGELOG.md (2026-07-06, PR-7c girdisi).
Mahsuplaşma (Netting)
EPİAŞ × Mahsuplaşma Faz 1A (plan v3 §11 + §6.3). Tüm endpoint'ler
tenant-scoped'dur; izinler: GET → read:netting (admin + operator +
viewer), mutasyonlar → manage:netting (yalnızca admin). POST'lar
201 Created döner; PATCH'ler partial-update semantiği taşır (yalnızca
gönderilen alanlar uygulanır).
PR-1 — Scopes / Facilities / Organizations
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| GET | /api/mahsuplasma/scopes | read:netting | VKN'siz mahsuplaşma kapsamlarını listele (ad sıralı; limit/offset) | — |
| POST | /api/mahsuplasma/scopes | manage:netting | Yeni kapsam oluştur (beyan alanları bu endpoint'ten yazılamaz) | 409 duplicate (tenant+name) |
| PATCH | /api/mahsuplasma/scopes/{id} | manage:netting | Kapsam güncelle (name/external_party_ref/notes; beyan alanları değiştirilemez) | 404¹; 409 duplicate |
| POST | /api/mahsuplasma/scopes/{id}/declare-same-official-party | manage:netting | "Aynı resmi taraf" beyanını kaydet (onaylayan = token kullanıcısı; aynı/daha güçlü kaynakla yeniden beyan serbest) | 404¹; 409 provenance-downgrade⁶ (F4 guard — PR-2); 422 (not_checked beyan kaynağı olarak kabul edilmez) |
| GET | /api/mahsuplasma/facilities | read:netting | Resmi tesisleri listele (tarihçeli tablo; scope_id/subregion_id filtreleri AND) | — |
| POST | /api/mahsuplasma/facilities | manage:netting | Yeni resmi tesis oluştur (valid_from boşsa DB TR-yerel bugünü yazar). Opsiyonel producer_tariff_group (lisanssiz_uretici_1/lisanssiz_uretici_2) — SKB fiyat tabanı LÜ-grubu override'ı; additive, PR-C2 | 422 (geçersiz/görünür-olmayan referans; ESS tutarlılık; valid_from > valid_to; producer_tariff_group whitelist dışı) |
| PATCH | /api/mahsuplasma/facilities/{id} | manage:netting | Tesisi güncelle (nullable FK'lar null ile temizlenebilir) | 404¹; 422 (ESS/dönem kuralları birleşik durumda doğrulanır) |
| DELETE | /api/mahsuplasma/facilities/{id} | superuserᴱ | Tesisi KALICI sil (hard-delete — GERİ ALINAMAZ; arşivleme değil). Cross-tenant global (superadmin); 204 döner | 401/403ᴱ; 404 (olmayan id); **409 facility_has_dependencies**ᶠ |
| GET | /api/mahsuplasma/organizations | read:netting | Görünür resmi kurumları listele (kendi tenant + sistem-geneli salt-okunur; org_type/include_inactive filtreleri) | — |
| POST | /api/mahsuplasma/organizations | manage:netting | Yeni kurum kaydı (her zaman çağıran tenant'a; sistem-geneli kayıt API'den oluşturulamaz) | 409 duplicate (org_type+name) |
| PATCH | /api/mahsuplasma/organizations/{id} | manage:netting | Kurum kaydını güncelle (yalnız tenant'ın kendi kayıtları) | 404¹ (sistem-geneli/başka tenant kayıt da 404); 409 duplicate |
PR-2 — Metering (ölçüm noktası + rol ataması + inverter_type)
Metering katmanı: tarihçeli ölçüm noktası (metering point) + Subregion içi
cihaz → ölçüm rolü ataması + invertör tipi (grid_inverter/ess_inverter)
backfill önericisi. Migration 0092 (repo'da ilk btree_gist + EXCLUDE).
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| GET | /api/mahsuplasma/facilities/{id}/meters | read:netting | Tesise bağlı ölçüm noktalarını listele (tarihçeli; yeni→eski) | 404¹ (yabancı/olmayan tesis) |
| POST | /api/mahsuplasma/facilities/{id}/meters | manage:netting | Tesise bağlı ölçüm noktası oluştur (facility_id path'ten; valid_from boşsa DB NOW()) | 404¹; 422² |
| PATCH | /api/mahsuplasma/meters/{metering_point_id} | manage:netting | Ölçüm noktasını güncelle (facility_id sabit; nullable FK'lar null ile temizlenebilir) | 404¹; 422² |
| POST | /api/mahsuplasma/meters/assign | manage:netting | Cihaz → ölçüm rolü ataması oluştur (valid_from zorunlu) | 422²; 409 dönem çakışması³ |
| GET | /api/mahsuplasma/meters/assignments | read:netting | Ölçüm rolü atamalarını listele (tarihçeli; subregion_id/facility_id filtreleri AND) | — |
| PATCH | /api/mahsuplasma/meters/assignments/{id} | manage:netting | Atamayı güncelle (valid_to ile dönem kapatma; subregion_id/device_id değiştirilemez) | 404¹; 422²; 409 dönem çakışması³ |
| GET | /api/mahsuplasma/inverter-type/suggestions | read:netting | inverter_type öneri listesi (on-demand; SOFAR+HYD→ess, SUN2000/KTLX→grid, belirsiz→öneri yok) | — |
| POST | /api/mahsuplasma/inverter-type/apply | manage:netting | EXPLICIT (device_id, inverter_type) listesini uygula (items 1-500; atomik) | 404⁴; 422⁵ |
¹ 404 = enumeration-safe: kayıt yok VEYA başka tenant'a ait — cross-tenant enumeration koruması için bilinçli olarak 403 yerine 404 döner.
² 422 (metering ref): geçersiz/görünür-olmayan FK referansı
(subregion_id / device_id / facility_id / metering_point_id /
meter_reading_org_id / osos_installation_id — cross-tenant body FK
IntegrityError'a düşmeden reddedilir) veya şema/whitelist ihlali.
³ 409 dönem çakışması: aynı alt bölge + aynı ANA ölçüm rolü
(main_production / main_consumption / pcc_import_export /
osb_eb_consumption) için çakışan [valid_from, valid_to) dönemi zaten
atanmış (DB EXCLUDE ex_sma_no_overlap_main_meter). Yalnız 4 ANA rol
tekilleştirilir; alt/yardımcı roller bilinçli çakışabilir. Çözüm: önce mevcut
atamanın valid_to'sunu kapat, sonra yeni atamayı oluştur.
⁴ 404 (apply): verilen device_id'lerden en az biri olmayan VEYA
cross-tenant. Tek geçersiz id TÜM işlemi durdurur (atomik — kısmi
uygulama yok); eksik id'ler yanıtta listelenir.
⁵ 422 (apply): aynı istekte duplicate device_id (çelişen atama) VEYA
device_role != 'inverter' cihaza atama VEYA şema/whitelist ihlali
(inverter_type geçersiz; items boş/500'den fazla).
⁶ 409 provenance-downgrade (F4 guard — PR-2'de eklendi): beyan kaynağı bir
güçlülük sıralaması taşır (customer_declaration < official_document <
lum_report/gts_report; son ikisi eş-düzey). Kapsam zaten daha güçlü bir
kaynakla beyan edilmişse, daha zayıf bir kaynakla tekrar-declare 409 ile
reddedilir (regülasyon provenance'i sessizce zayıflatılamaz). Aynı/daha güçlü
kaynakla yeniden beyan serbesttir (idempotent yeniden-teyit + yükseltme).
Davranış değişikliği: PR-1'de declare koşulsuz üzerine yazıyordu; F4 guard bu
PR'da (PR-2) eklendi.
ᴱ 401/403 (tesis silme — superuser): DELETE /facilities/{id} bu router'daki
tek superuser-gated endpoint'tir (diğerleri read/manage:netting izin
kapısıyla). 401 = JWT eksik/geçersiz; 403 = superuser değil (SUPERUSER_REQUIRED;
manage:netting yetkisi yetmez). Silme cross-tenant global'dir (superadmin
başka tenant'ın tesisini de silebilir — smoke-test / örnek-veri temizliği); bu
yüzden tenant izolasyonu 404'ü değil, gerçek yokluk 404'ü uygulanır.
ᶠ 409 facility_has_dependencies (tesis silme): tesise facility_id ile bağlı
en az bir alt kayıt VARSA silme reddedilir (yetim veri / sessiz kayıp yok). 5 alt
tablo SELECT COUNT ile sayılır; gövde dependencies sözlüğü sabit sıralı ve
her anahtarı içerir (0 olanlar da):
{"detail": {"error_code": "facility_has_dependencies", "message": "...", "dependencies": {"metering_points": n, "subregion_meter_assignments": n, "facility_hourly_energy": n, "mahsuplasma_group_members": n, "bedelli_limit_accounts": n}}}. Çözüm: önce sayısı > 0 olan bağımlılıkları
kaldırın, sonra tesisi silin. Say→sil yarışında FK RESTRICT/NO ACTION (DB son
savunma; facility_hourly_energy RESTRICT'tir) aynı 409'a çevrilir (500 değil).
Hard-delete arşivleme değildir — valid_to/archived_at kullanılmaz; dönem-
kapatma semantiği PR-6 CorrectionEngine'e aittir.
PR-3 — Groups / Eligibility
Grup kataloğu + tarihçeli üyelik (tesis) + uygunluk motoru (R001-R013 +
R003M). Migration 0094 (yalnız-yeni-tablo: mahsuplasma_groups +
mahsuplasma_group_members + mahsuplasma_eligibility_results; repo'daki 2.
btree_gist/EXCLUDE kullanımı — ex_mgm_facility_single_group_per_period).
Uygunluk alanları grup CRUD'undan yazılamaz (yalnız evaluate yazar).
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| GET | /api/mahsuplasma/groups | read:netting | Mahsuplaşma gruplarını listele (yeni→eski; scope_id/is_active filtreleri AND) | — |
| POST | /api/mahsuplasma/groups | manage:netting | Yeni grup oluştur (uygunluk alanları yazılamaz; not_checked doğar) | 409 duplicate (tenant+name); 422⁷ |
| PATCH | /api/mahsuplasma/groups/{id} | manage:netting | Grubu güncelle (uygunluk alanları değiştirilemez) | 404¹; 409 state-machine⁸; 422⁷ |
| POST | /api/mahsuplasma/groups/{id}/members | manage:netting | Gruba üye (tesis) ekle (declared_by = token kullanıcısı; valid_from zorunlu) | 404¹; 409 tek-grup EXCLUDE⁹; 422¹⁰ |
| PATCH | /api/mahsuplasma/groups/{id}/members/{member_id} | manage:netting | Üyeyi güncelle (valid_to ile dönem kapatma — tarihçe korunur; group_id/facility_id değiştirilemez) | 404¹; 409⁹; 422¹⁰ |
| DELETE | /api/mahsuplasma/groups/{id}/members/{member_id} | manage:netting | Üyeliği hard delete (satır kalıcı silinir) — tarihçe için PATCH valid_to tercih edilir; 204 döner | 404¹ |
| POST | /api/mahsuplasma/groups/{id}/evaluate | manage:netting | Uygunluk motorunu çalıştır (yazma: FOR UPDATE + salt-append log + eligibility_status günceller) | 404¹ |
| GET | /api/mahsuplasma/groups/{id}/eligibility | read:netting | Uygunluk özeti + en-son-run kural sonuçları (salt-okunur; hiç run yoksa not_checked + boş) | 404¹ |
⁷ 422 (group ref): geçersiz/görünür-olmayan FK referansı (scope_id /
responsible_supplier_id — cross-tenant body FK IntegrityError'a düşmeden
reddedilir) veya şema/whitelist ihlali.
⁸ 409 state-machine (group_status): group_status yalnız ileri-akış
(customer_defined → ek1_submitted → grid_operator_confirmed →
lum_reported) veya her durumdan inactive yönünde değişebilir; geri
düşme/atlama reddedilir (group_status aynı bırakılırsa guard atlanır). UNIQUE
(tenant+name) ihlali de 409 döner.
⁹ 409 tek-grup EXCLUDE: bir tesis aynı dönemde yalnız bir mahsuplaşma
grubunun üyesi olabilir (DB EXCLUDE ex_mgm_facility_single_group_per_period;
SQL detay sızmaz). Aynı [valid_from, valid_to) döneminde 2. gruba ekleme →
409. Çözüm: önce mevcut üyeliğin valid_to'sunu kapat (PATCH), sonra yeni gruba
ekle.
¹⁰ 422 backdate (Md.6(2)): valid_from geçmiş fatura dönemine set edilemez
(geriye-dönük değişiklik yasağı); ayrıca valid_from > valid_to, cross-tenant
facility_id, veya şema/whitelist ihlali. Not (M3 açık ürün kararı):
valid_to'nun geçmişe çekilmesi (üyelik erken kapatma) geriye-dönük etki
yaratabilir; Md.6(2)'nin valid_to yönündeki simetrisi şu an guard ile
zorlanmaz (mevzuat netleşene kadar bilinçli açık — yeni iş kuralı uydurulmaz).
Davranış notu — eligibility motoru CAPABILITY-GATED:
evaluateher zaman 13 kuralı register eder, ancak bazı kurallar bu fazda gerekli veri kaynağı henüz bağlı olmadığındanstatus='not_checked'döner:R007(limit/mesken — PR-4'te AKTİFLEŞTİ ↓), R008 (SKTT — not_checked, PR-8'e ertelenmiş), R009 (kurulu güç saatlik — PR-5), R010 (limit transferi — warning; kalan-limit yeterliliği PR-6), R011 (tesis devri — PR-6) + tarife blocking'i (R00T— PR-6'da AKTİF ↓). Bu kurallar ilgili PR'lar geldikçe PASS/FAIL üretmeye başlar; kod/severity sözleşmesi değişmez.not_checked ≠ valid:eligibility_status='valid'yalnızca hiç blocking-fail YOK ve hiç blocking-sınıfı not_checked YOK iken üretilir. Bir blocking sınıfı kural çözülemedikçe grup en fazlanot_checkedolur.
PR-4 — Limits (bedelli limit ledger)
Bedelli tüketim limiti (ihtiyaç fazlası bedelli enerji) için hesap +
salt-append hareket defteri + türeyen bakiye katmanı. Migration 0095
(yalnız-yeni-tablo: bedelli_limit_accounts + bedelli_limit_movements +
bedelli_limit_balances PLAIN VIEW). Kalan limit canlı türetilir
(initial_limit_kwh + SUM(hareket.amount_kwh)) — mutable used_kwh kolonu yok
(Md.7(5) SKB girdisi stale olamaz).
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| GET | /api/mahsuplasma/limits/accounts | read:netting | Bedelli limit hesaplarını listele (yeni→eski; facility_id/year filtreleri AND). Kalan limit görünmez (mutable kolon yok — VIEW/movements'tan okunur) | — |
| POST | /api/mahsuplasma/limits/accounts | manage:netting | Hesap oluştur (atomik: hesap + initial hareket [amount=0 audit marker] tek tx). Başlangıç limiti initial_limit_kwh kolonunda; kalan = VIEW'de canlı | 409 tekil hesap¹¹; 422¹² |
| GET | /api/mahsuplasma/limits/accounts/{id}/movements | read:netting | Hesabın salt-append hareketlerini listele (yeni→eski; kullanım NEGATIF, artış POZİTİF). UPDATE/DELETE edilmez (Md.15) | 404¹ |
| POST | /api/mahsuplasma/limits/accounts/{id}/adjust | manage:netting | Manuel düzeltme (manual_adjustment hareketi; salt-append). amount_kwh işaretli (sıfır reddedilir); reason zorunlu | 404¹ |
| POST | /api/mahsuplasma/limits/transfer | manage:netting | İSKELET/beta gruplar-arası kalan limit transferi (group_transfer_out+group_transfer_in çifti tek tx; FOR UPDATE serileşme) | 422¹³ (INSUFFICIENT_LIMIT_BALANCE + cross-tenant + kaynak==hedef) |
¹¹ 409 tekil hesap: bu tesis + bu yıl için ikinci hesap (UNIQUE
uq_bedelli_limit_accounts_facility_year — bir tesis, bir yıl, tek hesap).
Mevcut hesabı güncelleyin veya .../adjust ile düzeltme hareketi ekleyin.
¹² 422 (account): facility_id geçersiz/cross-tenant (IntegrityError'a
düşmeden reddedilir) veya şema ihlali (initial_limit_kwh >= 0; limit_source
whitelist).
¹³ 422 (transfer): source_account_id/target_account_id
geçersiz/cross-tenant; kaynak == hedef; kaynak kalan limit < transfer
miktarı → gövde {"detail": {"error_code": "INSUFFICIENT_LIMIT_BALANCE", "message": "..."}}. Eşzamanlılık: kaynak+hedef SELECT ... FOR UPDATE ile
(id-sıralı; deadlock önleme) kilitlenir → aynı kaynaktan eşzamanlı iki transfer
serileşir (TOCTOU over-transfer/negatif-kalan yarışı kapalı). İSKELET/beta
notu: contracted_power_confirmed (Md.5(4)) bu fazda doğrulanmaz — beyan
alanıdır; false ise yanıt warnings + sıra-eşli warning_codes
(TRANSFER_CONTRACTED_POWER_NOT_CONFIRMED — PR-7b ↓) içerir. Md.5(4)
sözleşme-gücü + Yönetmelik 28/7 oran-bazlı tam kalibrasyon PR-6.
🔴 Davranış değişikliği — R007 AKTİVE edildi (PR-4): PR-3'te
not_checkeddönen R007 (bedelli limit hesabı var mı — mesken hariç) kuralı, mig0095ledger geldiği için artık PASS/FAIL üretir. Mesken-dışı bir grubun her tüketim tesisi (member_role consumption/mixed) için ilgili yıla aitbedelli_limit_accountskaydı YOKSA → BLOCKING FAIL → grupeligibility_status='blocked'(daha öncenot_checked'ti). Çözüm: öncePOST /limits/accountsile eksik tesis(ler)in limit hesabını oluştur. Mesken grubu →not_applicable(Md.7(4)); tüketim tesisi olmayan grup →not_applicable. Tenant-görünür: öncedenevaluate'te nötr görünen mesken-dışı gruplar artıkblockeddönebilir (kasıtlı — Md.7(1) limit tüketim tesisi bazında zorunludur). kod/severity sözleşmesi değişmedi (R007 hepblocking); yalnızstatusçıktısı gerçek değer üretmeye başladı (capability-gated tasarımın beklenen aktivasyonu). R010 (kalan limit transferi) hâlâwarning/not_checked— yeterlilik PR-6.
Not —
/organizationsplan §11 dışıdır: organizations endpoint grubu plan v3 §11 listesinde yoktur;official_facilities'in 5 kurum FK'sının (grid operator / dağıtım şirketi / OSB-EB / sayaç okuyan kurum / tedarikçi) UI'dan seçilebilmesi için katalog CRUD'u olarak orchestrator kararıyla PR-1'e eklendi.
Not — dormant izin:
import:settlement_reportsizni PR-1'de DB'ye seed edilir (admin + operator) ancak hiçbir endpoint tarafından tüketilmez — PR-8 reconciliation/import router'ında kullanılacaktır.
PR-5 — Hourly Energy (saatlik enerji fact + resolver)
Bir tesisin saatlik enerji fact'lerini (üretim / tüketim / iç-tüketim / çekiş /
veriş / batarya) kaynak-başına ayrı satır olarak sunan önizleme + on-demand
yeniden-üretim katmanı. Fact üretimi MeasurementResolver (8-basamaklı öncelik
zinciri) ile arka planda 2 Celery beat tarafından yapılır; bu endpoint'ler yalnız
okuma (önizleme) + on-demand rebuild (wizard önizlemesi / düzeltme) sağlar.
Migration 0097 (mahsuplaşma domeninin İLK hypertable'ı: facility_hourly_energy).
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| GET | /api/mahsuplasma/facilities/{id}/hourly-energy | read:netting | Tesis saatlik enerji fact'lerini listele (from/to zorunlu ISO-8601; yarı-açık [from, to); hour ASC; limit ≤ 2000). Kaynak-başına AYRI satır (aynı saat/rol için birden çok kaynak); enerji STRING-DECIMAL; her satır data_quality/completeness_pct/source taşır | 404¹ (yabancı/olmayan tesis) |
| POST | /api/mahsuplasma/facilities/{id}/hourly-energy/rebuild | manage:netting | {from_hour, to_hour} aralığını resolver ile on-demand yeniden üret (upsert; değişen satırlar corrected). Tenant-scoped; aralık ≤ 7 gün. Yanıt: {facility_id, from_hour, to_hour, hours_processed, rows_upserted} | 404¹; 422¹⁴ (from_hour >= to_hour VEYA aralık > 7 gün) |
¹⁴ 422 (rebuild): from_hour >= to_hour (boş/ters aralık) VEYA aralık > 7 gün
(DoS/uzun-tx koruması). Saat sınırlarına kırpılır (istemci tam-saat göndermek
zorunda değil).
Kaynak öncelik zinciri (source — 8 kanonik değer): lum_official_report →
gts_report → pys_import → osos_official_meter → zeus_meter →
customer_declared → profiled_estimate → missing. AKTİF (bu faz):
osos_official_meter + zeus_meter. İSKELET (kaynak/tablo yok → sessizce bir
alt basamağa düşer): lum/gts/pys (PR-8), customer_declared,
profiled_estimate (PR-6+). Hiçbir gerçek ölçüm yoksa missing kaynaklı fact
yine yazılır (sessiz boşluk yerine ACIK missing).
Veri kalitesi (data_quality): complete / partial / missing /
corrected (backfill/rebuild) / suspect (sayaç reset) / estimated_profiled
(gelecek) / unknown. Eşikler completeness_pct üzerinden: ≥95 complete · 50–95
partial · <50 partial+WARN.
Semantik notu — interval_delta Zeus'ta DESTEKLENMEZ: energy_hourly CAGG
enerji kolonlarını LAST() ile (kümülatif) saklar, SUM(delta) değil. Bir atama
Zeus cihazına interval_delta derse gerçek interval kanalı CAGG'de kaybolur →
sessizce YANLIŞ kWh üretmek yerine WARN log + zincir alt basamağa düşer (açık
eksik). OSOS HAM yolu ETKİLENMEZ (orada tablodan SUM doğrudur). Zeus'ta
interval kanalı gerekirse cumulative_counter'a dönüştürülmeli.
Best-effort dayanıklılık: rebuild sırasında tek atama beklenmedik şekilde
patlarsa o atama atlanır (kendi savepoint'i geri alınır + WARN log) ve diğerleri
işlenmeye devam eder — görev bozulmaz. Atlanan atama sayacı (assignments_failed)
yalnız Celery beat özetinde (task_runs) raporlanır; rebuild endpoint gövdesinde
YOK. Sistemik hata (DB kopması / batch-upsert) hâlâ 5xx döner.
Arka plan (Celery beat): mahsuplasma.build_hourly_energy (her saat :10 —
önceki TR saati) + mahsuplasma.build_hourly_backfill (02:30 — son 72h → corrected).
Detay: docs/runbook/0097_facility_hourly_energy.md.
Not —
tekil(PR-7a'da EKLENDİ ↓): PR-5 döneminde sayfa başlığındaki tesis adı liste endpoint'inden client-side eşleştirmeyle çözülüyordu; PR-7a tekilGET /facilities/{id}YOKGET /api/mahsuplasma/facilities/{id}endpoint'ini ekledi (bkz. "PR-7a — UI-destek GET'leri").
Not —
wizard menüde değil(PR-7a ile menüye BAĞLANDI): Sayaç Atama Sihirbazı'na artık sol menüden erişilir: Mahsuplaşma → Resmî Tesisler → tesis satırı. (PR-5 döneminde yalnız doğrudan URL ile erişiliyordu.)
PR-6 — Hesap Çekirdeği (calculate / hourly / virtual-meters / amounts / monthly-summary / corrections)
Saatlik mahsuplaşma hesap çekirdeği: manuel hesap tetikleme + saatlik hesap /
sanal sayaç / tutar / aylık özet okuma + Md.15 düzeltme (cascade). Migration
0098 (6 yeni tablo, 3'ü hypertable — plan §6.8'in "0097" dediği migration'ın
gerçek numarası 0098'dir). Hesaplar arka planda 6 yeni Celery beat ile de
üretilir (evaluate_groups 0 */6 · calc_prev_hour_preliminary :15 ·
calc_prev_hour_corrected :45 · calc_daily_backfill 03:10 · monthly_close
ayın 1'i 01:30 · tariff_missing_scan 07:00). UI PR-7a ile geldi (grup
detayı sekmeleri + aşağıdaki "PR-7a — UI-destek GET'leri"); PR-6 sürümü salt
API + otomatik görevlerdi.
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| POST | /api/mahsuplasma/groups/{id}/calculate | manage:netting | {from_hour, to_hour} penceresini manuel hesapla (triggered_by='manual'). Pencere saat sınırlarına kırpılır, TR fatura dönemlerine bölünür; dönem başına ayrı run + ayrı tx. Pencere sonu şimdiki saate clamp'lenir (gelecek saat işlenmez). Kesişen eski run'lar supersede edilir (ledger correction_reversal + yeni kullanım — bakiye birebir korunur); pencere, kesişen run kapsamlarını örtecek şekilde otomatik genişleyebilir. input_hash değişmediyse dönem no-op (noop_periods). Yanıt: {runs[], hours_calculated, noop_periods[], warnings[], warning_codes[]} (kodlar warnings ile sıra-eşli — PR-7b ↓) | 404¹; 422¹⁵ |
| GET | /api/mahsuplasma/groups/{id}/hourly | read:netting | [from, to) saatlik hesap satırları (Md.9(2) ayrımı + Md.7 limit kolonları; hour bazlı). Varsayılan yalnız current; include_history=true → superseded satırlar da döner (denetim). Enerji alanları STRING-DECIMAL. Mesken grubunda satırlar bilgilendirmedir (mesken_informational bayrağı) — resmî sonuç /monthly-summary. limit ≤ 2000 | 404¹ |
| GET | /api/mahsuplasma/groups/{id}/virtual-meters | read:netting | [from, to) sanal sayaç satırları (Md.10): virtual_meter_type = bedelli/skb_odemeli/bedelsiz; grup × şebeke işletmecisi × kaynak türü AGGREGATE granülerliği; reason_code kanonik set (standard_matched/limit_exceeded/missing_consumption_data/group_conditions_not_met/mesken_monthly…). Varsayılan yalnız current; include_history destekli. limit ≤ 2000 | 404¹ |
| GET | /api/mahsuplasma/groups/{id}/amounts | read:netting | [from, to) saatlik tutar satırları (Md.11-12): amount_type 7 kanonik değer (supplier_payment_for_matched_consumption/producer_payment_for_bedelli_surplus/system_usage_fee_for_skb/sktt_matched_consumption/mesken_monthly_amount/estimated_savings/estimated_loss_or_risk). Fiyatı çözülemeyen SKB/risk satırları quantity-only olabilir (unit_price_try_per_kwh/amount_try NULL); tariff_price_id fiyat provenance'ı taşır. Tutar/miktar alanları STRING-DECIMAL. Varsayılan yalnız current. limit ≤ 2000 | 404¹ |
| GET | /api/mahsuplasma/groups/{id}/monthly-summary | read:netting | Aylık özet satırları (mesken resmî aylık sonuç + standart roll-up). Varsayılan yalnız current (dönem başına tek satır — uq_current_monthly_calc); opsiyonel year daraltması; include_history destekli. limit ≤ 500 | 404¹ |
| POST | /api/mahsuplasma/groups/{id}/corrections | manage:netting | {billing_period, reason} — dönemi düzeltir ve Zeus'ta hesaplanmış takip eden tüm dönemleri kronolojik cascade eder (Md.15(4); yıl sınırında durmaz; dönem başına ayrı tx + run triggered_by='correction_cascade'). Ledger: önce correction_reversal (eski run'ın hesap başına efektif net toplamının tersi), sonra correction_reapply. Cascade ortada durursa yanıt cascade_incomplete=true + failed_period; aynı istek tekrar çalıştırılınca kaldığı dönemden devam eder. reason zorunlu (5-2000 karakter; denetim izi). DUY 133/5: dönem 12 aydan eskiyse istek reddedilmez, warnings uyarı taşır. Yanıttaki warnings'e sıra-eşli warning_codes eşlik eder (PR-7b ↓) | 404¹; 409 compressed-chunk¹⁶; 422 (şema: reason uzunluk / billing_period) |
¹⁵ 422 (calculate): from_hour >= to_hour (boş/ters aralık) VEYA aralık
üst sınırı 7 gün aşımı (DoS/uzun-tx koruması — PR-5 rebuild emsali) VEYA
grup pasif (is_active=false hesaplanamaz) VEYA naive datetime —
from_hour/to_hour TZ-farkındalı (aware) olmak zorundadır; ISO-8601 UTC
offset (Z veya +03:00) gönderilmezse şema doğrulaması 422 döner.
¹⁶ 409 compressed-chunk (corrections/calculate): düzeltme penceresi
sıkıştırılmış (500+ gün eski) TimescaleDB chunk'ına dokunuyor. Kod otomatik
decompress yapmaz — operatör adımları (chunk decompress + yeniden hesap):
docs/runbook/0098_mahsuplasma_calculation_core.md (0097 runbook'una çapraz
referans). Gövde düz-string detail taşır (runbook yönlendirmesi).
include_history semantiği (4 GET endpoint'i): varsayılan false — yalnız
is_current=true (geçerli) satırlar döner. true → superseded
(is_current=false) satırlar da döner; istemci calculation_run_id üzerinden
run bazında gruplayabilir (denetim/karşılaştırma görünümü). Eski satırlar asla
silinmez (salt süpersede).
STRING-DECIMAL sözleşmesi: tüm kWh / TL / fiyat alanları (NUMERIC kolonlar)
JSON'da string olarak serileşir (fatura kalitesi — float'a düşürülmez);
istemci decimal kitaplığıyla parse etmelidir (PR-5 hourly-energy ile aynı
sözleşme).
🔴 Davranış değişikliği — PATCH member yanıtında yeni
warningsalanı (M3/M4):PATCH /groups/{id}/members/{member_id}yanıt şemasınawarnings: string[]alanı eklendi (additive — varsayılan boş liste; create/list akışlarında hep boş). Üyeliğinvalid_to'su geçmişe çekildiğinde artık etkilenen ilk dönemden itibaren düzeltme cascade'i otomatik tetiklenir (üyelik kümesiinput_hash'e dahildir) ve cascade sırasında oluşan uyarılar bu alanda döner. Eski istemciler alanı yok sayabilir; yeni istemciler kullanıcıya göstermelidir. PR-7b ilewarnings'e sıra-eşliwarning_codes: string[]eşlik eder (bkz. "PR-7b —warning_codes" ↓).
Davranış değişikliği — R00T + R010 aktivasyonu (evaluate): PR-6 ile
POST /groups/{id}/evaluateçıktısında R00T (cari dönem × abone grubu için approved regüle tarife fiyatı var mı — blocking) gerçek PASS/FAIL üretir; onaylı fiyat yoksa grupblockedolur. Hesap yine de durmaz —calculate, bloklu gruptainvalid_group_bedelsizsenaryosunu koşar (tüm üretim bedelsiz sanal sayaca + "bedelsiz risk TL" tutar satırı). R010 (limit transferi Md.5(4)+28/7 — başka-grup kullanım tespiti) de warning düzeyinde PASS/FAIL üretir. R008/R009/R011 + R00L hâlânot_checked(kod/severity sözleşmesi değişmedi).
Not — limit ledger artık hesap çekirdeğinden beslenir: PR-4'te tanımlanan
movement_typedeğerlerinin son 4'ü (mahsuplasan_consumption/bedelli_ihtiyac_fazlasirun bağlı doğal hareketler +correction_reversal/correction_reapply) PR-6 ile fiilen yazılmaya başladı; hareketlercalculation_run_idtaşır.POST /limits/transfer'in 422INSUFFICIENT_LIMIT_BALANCEsözleşmesi (¹³) değişmedi.
PR-7a — UI-destek GET'leri
/mahsuplasma frontend sayfa ağacının (genel bakış + kapsamlar + resmî
tesisler + gruplar + 7 sekmeli grup detayı + bedelli limitler; sol menüde
"Mahsuplaşma") ihtiyaç duyduğu salt-okunur destek katmanı. Migration YOK —
5 additive GET; mevcut hiçbir endpoint/şema değişmedi. Tekil GET'lerin yanıt
şemaları liste elemanlarıyla birebir aynıdır (yeni alan yok). Ekran
açıklamaları: docs-site/docs/user-guide/mahsuplasma/web-arayuzu.md.
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| GET | /api/mahsuplasma/groups/{id} | read:netting | Tekil grubu getir (yanıt liste elemanıyla birebir — MahsuplasmaGroupResponse); "liste çek + id eşle" workaround'unu kaldırır. Uygunluk ayrıntısı /eligibility'de, KPI özeti /dashboard-summary'de | 404¹ |
| GET | /api/mahsuplasma/facilities/{id} | read:netting | Tekil resmî tesisi getir (OfficialFacilityResponse — liste elemanıyla birebir; PR-5'teki "tekil-get yok" notunun kapanışı) | 404¹ |
| GET | /api/mahsuplasma/groups/{id}/members | read:netting | Üye tesisleri listele (yeni→eski; tarihçeli — kapatılmış üyelikler de döner). active_only=true → yalnız aktif dönem üyeleri (valid_to NULL veya TR-yerel bugünden sonra). Sayfalama parametresi yok; warnings/warning_codes alanları GET'te hep boş dizi (yalnız PATCH yanıtında dolar — PR-7b ↓) | 404¹ |
| GET | /api/mahsuplasma/groups/{id}/runs | read:netting | Hesap koşumu (run) denetim izi (started_at DESC, id DESC tie-break; limit default 100, en fazla 500; offset). billing_period opsiyonel — ay-başına kırpılır (ayın 15'i gönderilse de o ayın run'ları döner). is_current bilgilendiricidir (current'lik tekilliği veri satırında); parent_run_id düzeltme zinciri kökü; triggered_by = beat/manual/correction_cascade | 404¹ |
| GET | /api/mahsuplasma/groups/{id}/dashboard-summary | read:netting | 8 KPI tek çağrıda (mevcut fact'lerin salt-okunur agregatı — yeni hesap mantığı yok). period boşsa TR-yerel cari ay; gönderilirse ay-başına kırpılır (geçmiş ay sorgulanabilir). Tüm kWh/TL alanları STRING-DECIMAL. Yanıttaki warnings'e sıra-eşli warning_codes eşlik eder (PR-7b ↓) | 404¹; 422 period-band¹⁷ |
¹⁷ 422 period-band (dashboard-summary): period yılı 2000-2100 bandının
dışında (dönem sınır hesabı taşması koruması — repo year parametre bandı
emsali).
dashboard-summary alan semantiği (kaynak haritası):
has_dataempty-state: dönemde hiç current saatlik satır VE hiç run yoksahas_data=false→ FE boş-durum kartı; kWh/TL alanlarınull. Kalan limit + veri kalitesi run'dan bağımsızdır — yine dolu olabilir.- kWh alanları —
mahsuplasma_hourly_group_calcCURRENT satır SUM'u; TL alanları —mahsuplasma_settlement_amount_hourlyCURRENTSUM(amount_try)(mahsuplasan_tuketim_try= supplier + SKTT + mesken-aylık;bedelli_fazla_gelir_try= producer_payment;skb_bedel_try= system_usage_fee). Quantity-only satırlar toplama girmez; hiç fiyatlı satır yoksanull. bedelsiz_risk_trySERVER-side:bedelsiz_kwh × geçerli regüle fiyat(amount_engine 28/11-12-13 kanonik çözümü, tariff_engine üzerinden). FE bu değeri hesaplayamaz (bedelsiz tutar satırları quantity-only olabilir veya hiç üretilmez). Provenance:bedelsiz_risk_tariff_price_id(baskın — en büyük miktarlı — bileşenintariff_prices.id'si). Fiyat çözülmezsenull+warnings(ve sıra-eşliwarning_codes— PR-7b ↓) sebep taşır;bedelsiz_kwh=0ise0.bedelli_limit_kalan_kwhCANLI:bedelli_limit_balancesVIEW canlı SUM (dönem yılındaki tüketim-tesisi hesapları). Geçmiş dönem sorgulansa da bugünkü kalan döner (dönem-sonu anlık görüntüsü değildir); hesap yoksanull.veri_kalitesi_pctbugün-TR, tesis-dengeli:facility_hourly_energy.completeness_pct'nin tesis-başına ortalamaların ortalaması (AVG-of-AVG — çok-satırlı tesis az-satırlıyı boğmaz), TR-yerel BUGÜN için; seçilen dönemden bağımsız tazelik sinyali. Bugün hiç fact yoksanull.last_run— seçilen dönemin en son başlatılan run'ı; tam listeGET /groups/{id}/runs.
PR-7b — warning_codes (makine-okur uyarı kodları)
warnings (insan-okur TR metin listesi) taşıyan 5 yanıt şemasına additive
warning_codes: string[] alanı eklendi (varsayılan boş liste — breaking
YOK; warnings metinleri birebir korundu). Sıra-eşlilik sözleşmesi:
warning_codes[i] ↔ warnings[i] her zaman aynı uzunlukta ve index-eşlidir;
istemci index ile eşleştirir. Kodlar SABİT isimlerdir (tek kaynak
backend/app/features/mahsuplasma/services/warning_codes.py) — FE i18n eşlemesi
bu isimlere yapılır; kod çözülmezse/bilinmiyorsa istemci ham warnings[i]
metnine düşer. Kodlar API yanıtında taşınır — run.notes persist formatına
sızmaz (kalıcı kayıtta yalnız metinler).
Etkilenen 5 yanıt: POST /groups/{id}/calculate (GroupCalculateResult) ·
POST /groups/{id}/corrections (CorrectionResult) · POST /limits/transfer
(BedelliLimitTransferResult) · GET /groups/{id}/dashboard-summary
(GroupDashboardSummaryResponse) · PATCH /groups/{id}/members/{member_id}
(MahsuplasmaGroupMemberResponse — GET /groups/{id}/members listesinde
warnings gibi hep boş dizi; yalnız PATCH yanıtında dolar).
20 kanonik kod (üretici → anlam; PR-C2 ile SKB LÜ-grubu için +3 additive):
| Kod | Üretici | Anlam |
|---|---|---|
TRANSFER_CONTRACTED_POWER_NOT_CONFIRMED | limits/transfer | Md.5(4) sözleşme gücü beyan edilmedi (İSKELET/beta) |
TARIFF_MESKEN_STEP_FALLBACK_FLAT | tutar motoru (28/11) | mesken kademe fiyatı yok → tek-zamanlı tarifeye düşüldü |
TARIFF_MESKEN_UNRESOLVED | tutar motoru (28/11) | mesken tarife fiyatı hiç çözülemedi (satır üretilmedi) |
TARIFF_STEP_STATE_MISSING_CONSERVATIVE | tutar motoru (28/12) | kademe durumu yok → muhafazakâr yüksek-kademe fallback |
TARIFF_LOWEST_RELATED_FALLBACK | tutar motoru (28/13) | lowest_related yok → ilgili tarifelerin en düşüğü |
TARIFF_NONE_RESOLVED | tutar motoru (28/13) | hiçbir ilgili tarife çözülemedi (satır üretilmedi) |
FACILITY_ABONE_GRUBU_UNKNOWN | tutar motoru | tesisin abone grubu bilinmiyor (tedarikçi satırı yok) |
SKTT_PRICE_UNRESOLVED | tutar motoru | SKTT fiyatı çözülemedi (SKTT satırı üretilmedi) |
SUPPLIER_PRICE_UNRESOLVED | tutar motoru | ilgili tarife çözülemedi (supplier_payment satırı yok) |
SKB_PRICE_UNRESOLVED_QUANTITY_ONLY | tutar motoru | system_usage fiyatı yok → SKB satırı quantity-only |
SKB_LU_GROUP_MIXED | tutar motoru (§10.3, C2) | bölgede karma üretici LÜ grubu → SKB fiyatı üretim-kWh ağırlıklı ortalama; provenance baskın grubun satırı |
SKB_LU_GROUP_DERIVED | tutar motoru (§10.3, C2) | üretici LÜ grubu source_type'tan türetildi (açık producer_tariff_group override yok) |
SKB_LU_GROUP_UNKNOWN_DEFAULT | tutar motoru (§10.3, C2) | üretici LÜ grubu belirlenemedi (hybrid/unknown/NULL) → güneş-baskın varsayılan (lisanssiz_uretici_2) |
MESKEN_MONTHLY_PRICE_UNRESOLVED | tutar motoru | mesken aylık tarife çözülemedi → satır quantity-only |
LIMIT_ACCOUNT_MISSING_CONSERVATIVE_SKB | hesap koşumu (R007) | limit hesabı yok → surplus muhafazakâr SKB'ye yazıldı |
GROUP_BLOCKED_BEDELSIZ | hesap koşumu | grup blocked → aylık üretim bedelsiz sayıldı |
DUY_133_5_PERIOD_TOO_OLD | düzeltme motoru | DUY 133/5: dönem 12 aydan eski (istek yine işlenir) |
MONTHLY_REFRESH_FAILED | düzeltme motoru | düzeltme sonrası aylık özet yeniden hesaplanamadı |
CASCADE_STOPPED | düzeltme motoru | cascade ortada durdu (retry aynı endpoint'le devam eder) |
MEMBER_CHANGE_CORRECTION_FAILED | üye PATCH | üyelik değişimi sonrası otomatik düzeltme başarısız |
Yeni kodlar additive eklenir — istemci bilinmeyen kodu ham warnings[i]
metniyle göstermelidir. Detay + client etkisi: docs/api/CHANGELOG.md
(2026-07-06 PR-7b girdisi; 2026-07-21 PR-C2 SKB LÜ-grubu +3 kod girdisi).
Regüle Tarife (Regulated Tariffs)
EPİAŞ × Mahsuplaşma Faz 1A PR-4 (plan v3 §6.6). Regüle (EPDK) tarife
fiyatlarının versiyonlu import + onay katmanı. ULUSAL veri — tenant filtresi
YOK (tarife tüm tenant'lar için aynıdır); tüm endpoint'ler superadmin-only
(require_superuser). Migration 0096 (yalnız-yeni-tablo: tariff_price_sets +
tariff_prices; repo'daki 2. UNIQUE NULLS NOT DISTINCT kullanımı —
uq_tariff_prices_natural_key). Prefix: /api/v1/admin/regulated-tariffs. Gövde
error_code konvansiyonu: {"detail": {"error_code": ..., "message": ...}}.
Onay state-machine: draft → approved (approve action) / approved → archived (PATCH). Onay sonrası fiyatlar IMMUTABLE; onaylanmış/arşivlenmiş
paketin metadata'sı da değiştirilemez. DELETE yalnız draft.
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| GET | /api/v1/admin/regulated-tariffs/price-sets | superuser | Tarife paketlerini listele (yeni→eski; billing_period/approval_status filtreleri AND) | 401/403ᴬ |
| POST | /api/v1/admin/regulated-tariffs/price-sets | superuser | Paket oluştur — her zaman draft doğar (approval_status/approved_* gövdeden yazılamaz) | 401/403ᴬ |
| PATCH | /api/v1/admin/regulated-tariffs/price-sets/{id} | superuser | Paketi güncelle (approved→archived + draft metadata düzeltme) | 401/403ᴬ; 404ᴮ; 409 immutable/transitionᶜ |
| POST | /api/v1/admin/regulated-tariffs/price-sets/{id}/approve | superuser | draft → approved (approved_by=token; fiyatlar immutable olur) | 401/403ᴬ; 404ᴮ; 409 TARIFF_ALREADY_APPROVED |
| DELETE | /api/v1/admin/regulated-tariffs/price-sets/{id} | superuser | Paketi sil — YALNIZ draft (CASCADE fiyatlar); 204 döner | 401/403ᴬ; 404ᴮ; 409 TARIFF_SET_NOT_DELETABLE |
| GET | /api/v1/admin/regulated-tariffs/price-sets/{id}/prices | superuser | Paketin fiyat satırlarını listele (yeni→eski) | 401/403ᴬ; 404ᴮ |
| POST | /api/v1/admin/regulated-tariffs/price-sets/{id}/prices | superuser | Tek fiyat satırı ekle — YALNIZ draft (price>0; whitelist) | 401/403ᴬ; 404ᴮ; 409 immutable/duplicateᶜ |
| POST | /api/v1/admin/regulated-tariffs/price-sets/{id}/import-csv | superuser | CSV veya XLSX'ten toplu import — YALNIZ draft; ATOMİK; güvenli (aşağıda; .xls desteklenmez, hata satır no'su fiziksel) | 401/403ᴬ; 404ᴮ; 413/422 CSVᴰ; 422 XLSX_IMPORT_INVALID; 409 immutable/duplicateᶜ |
| GET | /api/v1/admin/regulated-tariffs/import-template | superuser | Örnek import şablonu indir (?format=csv|xlsx, attachment; 13 sıralı başlık + round-trip garantili örnek satırlar) | 401/403ᴬ |
ᴬ 401/403: 401 JWT eksik/geçersiz; 403 superuser değil (SUPERUSER_REQUIRED).
ᴮ 404: paket bulunamadı (NOT_FOUND).
ᶜ 409 (immutability/state-machine/duplicate): TARIFF_SET_APPROVED_IMMUTABLE
(approved/archived pakete fiyat/metadata yazma), TARIFF_INVALID_TRANSITION
(geçersiz geçiş — yalnız approved→archived meşru), TARIFF_ALREADY_APPROVED
(çift-onay), TARIFF_SET_NOT_DELETABLE (approved/archived silme),
TARIFF_PRICE_DUPLICATE (doğal anahtar abone_grubu+price_purpose+
time_segment+step_no duplicate; NULLS NOT DISTINCT).
ᴰ CSV import hata sözleşmesi — zorunlu başlıklar: valid_from,
billing_period, abone_grubu, price_purpose, price; opsiyonel: valid_to,
gerilim_seviyesi, tariff_code, tariff_class, time_segment, step_no,
unit, source_note. Hata kodları: CSV_IMPORT_TOO_LARGE (>2MB — 413 veya 422),
CSV_IMPORT_INVALID_TYPE (content-type/uzantı), CSV_IMPORT_INVALID_ENCODING
(UTF-8 değil), CSV_IMPORT_MISSING_HEADERS, CSV_IMPORT_TOO_MANY_ROWS (>10.000).
Her satır Pydantic (whitelist + price>0); CSV-injection sanitize (=,+,
@,-,tab,CR ile başlayan hücre reddedilir — formula injection). ATOMİK:
herhangi bir satır hatası → hiçbiri yazılmaz (imported=0 +
errors: [{row, message}]).
Yeni enum'lar: approval_status (3: draft/approved/archived);
price_purpose (7: mahsuplasma_related_tariff/mahsuplasma_lowest_related_tariff/
sktt_active_energy/customer_bill_active_energy/system_usage/distribution/
other); time_segment (4: tek/gunduz/puant/gece — nullable).
abone_grubu DB'de serbest, Pydantic reddeder (kanonik: mesken/ticarethane/
sanayi/tarimsal_sulama/aydinlatma/other).
Frontend:
/console/regulated-tariffs(superadmin-only) — bu epic'in ilk UI'ı. Yüzey A (bedelli limit ledger) UI'sı bu PR'da yok (API yüzeyi).
Enerji Piyasası / EPİAŞ Şeffaflık
EPİAŞ × Mahsuplaşma Faz 1A PR-8a (plan v3 §16). EPİAŞ Şeffaflık
Platformu'ndan ulusal PTF/SMF (piyasa) fiyatlarını çeken fiyat foundation
katmanı + superadmin API Portal hesap/whitelist yönetimi. Migration 0102
(yalnız-yeni-tablo: epias_api_portal_accounts + epias_fetch_logs +
market_prices hypertable).
PTF/SMF piyasa görünümü/advisory'dir; mahsuplaşma tutarının ANA girdisi DEĞİLDİR (Yönetmelik Md.4-l — tutar regüle EPDK tarifesinden hesaplanır). Tarife fiyatı ≠ piyasa fiyatı. Fiyatlar ULUSAL'dir → tenant filtresi YOK.
| Method | Path | İzin | Açıklama | Hata sözleşmesi |
|---|---|---|---|---|
| GET | /api/energy-market/prices | read:netting | ULUSAL PTF/SMF fiyat serisi (aralık + market filtreleri; tenant filtresi YOK) | 401/403ᴬ |
| GET | /api/v1/admin/epias-transparency/accounts | superuser | API Portal hesap listesi (kimlik bilgileri maskeli) | 401/403ᴬ |
| POST | /api/v1/admin/epias-transparency/accounts | superuser | Hesap oluştur (portal_password/subscription_key Fernet şifreli) | 401/403ᴬ; 503ᴰ |
| GET | /api/v1/admin/epias-transparency/accounts/{id} | superuser | Hesap detayı (maskeli) | 401/403ᴬ; 404ᴮ |
| PATCH | /api/v1/admin/epias-transparency/accounts/{id} | superuser | Hesabı güncelle | 401/403ᴬ; 404ᴮ; 503ᴰ |
| POST | /api/v1/admin/epias-transparency/accounts/{id}/transition | superuser | whitelist_status durum geçişi (9 adım) | 401/403ᴬ; 404ᴮ; 409ᶜ |
| POST | /api/v1/admin/epias-transparency/accounts/{id}/test-call | superuser | Bağlantı/kimlik test çağrısı — ASLA 500 üretmez | 401/403ᴬ; 404ᴮ |
ᴬ 401/403: 401 JWT eksik/geçersiz; 403 yetki yetersiz (fiyat GET: read:netting
yok; API Portal: superuser değil — SUPERUSER_REQUIRED).
ᴮ 404: hesap bulunamadı (NOT_FOUND).
ᶜ 409: geçersiz whitelist_status geçişi. Gövde bu modülde
{"detail": {"code": "invalid_whitelist_transition", "message": "..."}}
yapısındadır — anahtar code'dur (mahsuplaşma modülünün error_code
anahtarından farklı). Yalnız ileri doğru, adım atlamayan geçişler meşrudur:
not_started → spec_downloaded → spec_sent → ip_whitelisted → portal_account_created → application_created → subscribed → active. blocked
her durumdan erişilebilir (askıya alma/iptal) ve yarı-terminaldir: tek
kurtarma kolu blocked → not_started'tır (superadmin süreci baştan başlatır;
blocked'tan diğer durumlara doğrudan atlama reddedilir).
ᴰ 503: Fernet şifreleme anahtarı (CREDENTIAL_ENCRYPTION_KEY)
yapılandırılmamışken hesap oluşturma/güncelleme. Gövde
{"detail": {"code": "encryption_not_configured", "message": "..."}}
yapısındadır (anahtar code'dur). Anahtar var fakat geçersizse code yerine
encryption_misconfigured döner. Her iki durumda da kimlik bilgisi açık metin
saklanmaz.
price_status (fiyat olgunluk durumu): tentative (kesinleşmemiş) /
final (kesin) / corrected (düzeltilmiş) / unknown. market_prices PK
(ts, market, price_status) → aynı saatin farklı olgunlukları ayrı satır
(denetim izi; RETENTION YOK — mutabakat/itiraz kaynağı).
Dark-launch — EPIAS_ENABLED (default false): canlı çekiş 3 EPİAŞ beat
(epias.pull_ptf_tentative 13:35 / epias.pull_ptf_final 14:05 — SMF penceresi
4h geriden / epias.pull_market_backfill 02:15) bu anahtarla açılır. Kapalıyken
GET /api/energy-market/prices boş-durum döner (hata değil) ve 4 EPİAŞ
sistem alarmı (epias_api_auth_failed/fetch_failed/portal_not_whitelisted/
subscription_missing) pratikte tetiklenmez. Açılış koşulu: IP whitelist onayı
- hesap aktivasyonu +
test-callyeşil (bkz.docs/runbook/0102_epias_transparency.md).
Frontend:
/energy-market/prices(tenant,read:netting— EpiasChart PTF/SMF advisory) +/console/epias-api-portal(superadmin — hesap + 9-adım whitelist + test-call). Detay + client etkisi:docs/api/CHANGELOG.md(2026-07-07, PR-8a girdisi). Kullanıcı kılavuzu: EPİAŞ Piyasa Fiyatları.
WebSocket
| Protocol | Path | Açıklama |
|---|---|---|
| WS | /ws/telemetry?token=JWT&devices=id1,id2 | Gerçek zamanlı ölçüm |
| GET | /ws/stats | Hub istatistikleri |
Sistem
| Method | Path | Açıklama | Auth |
|---|---|---|---|
| GET | /health | Sağlık kontrolü | Hayır |
| GET | /readiness | DB hazırlık | Hayır |
| GET | /api/config/public | Frontend config | Hayır |
| POST | /api/internal/gateway/connected | EMQX webhook | Hayır* |
| POST | /api/internal/gateway/disconnected | EMQX webhook | Hayır* |
*Internal endpoint'ler firewall/network seviyesinde korunmalıdır.
API Kullanım Örnekleri
1. Auth Akışı (Login ve Authenticated Request)
Adım 1 — Giriş yaparak token al:
curl -X POST https://api.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "operator@firma.com", "password": "GucluSifre123!"}'
Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBl...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBl...",
"token_type": "bearer"
}
Adım 2 — Token ile authenticated request gönder:
curl -X GET https://api.example.com/api/devices \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBl..."
2. POST /api/auth/login
Request:
{
"email": "operator@firma.com",
"password": "GucluSifre123!"
}
Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
Response (401 Unauthorized):
{
"detail": "E-posta veya şifre hatalı",
"error_code": "INVALID_CREDENTIALS"
}
3. GET /api/devices
Request:
curl -X GET "https://api.example.com/api/devices?skip=0&limit=10&sort_by=name&sort_order=asc" \
-H "Authorization: Bearer <access_token>"
Response (200 OK):
{
"items": [
{
"id": "d1a2b3c4-...",
"name": "Entes RG3-12CS #01",
"device_type": "energy_analyzer",
"gateway_id": "gw-uuid-...",
"slave_id": 1,
"template_id": "tpl-uuid-...",
"subregion_id": null,
"is_online": true,
"last_seen": "2026-04-07T12:30:00Z",
"created_at": "2025-11-01T08:00:00Z"
},
{
"id": "e5f6a7b8-...",
"name": "Sofar HYD 5K",
"device_type": "inverter",
"gateway_id": "gw-uuid-...",
"slave_id": 2,
"template_id": "tpl-uuid-...",
"subregion_id": null,
"is_online": false,
"last_seen": "2026-04-07T11:15:00Z",
"created_at": "2025-12-15T10:00:00Z"
},
{
"id": "c0ffee99-...",
"name": "Beny AC-22kW #03",
"device_type": "charger",
"device_role": "charger",
"gateway_id": null,
"mqtt_gateway_id": null,
"subregion_id": "d729c579-1877-4aba-8c0b-ac3c1278ec1a",
"is_online": true,
"last_seen": "2026-05-13T09:42:11Z",
"created_at": "2026-03-20T14:10:00Z"
}
],
"total": 24,
"skip": 0,
"limit": 10
}
subregion_id alanı (B Paketi — OCPP charger consumption)
DeviceResponse.subregion_id: string | null opsiyonel alan; backward-compatible additive değişiklik (eski client'lar etkilenmez).
| Cihaz tipi | subregion_id değeri |
|---|---|
OCPP charger (device_role='charger') | OcppCharger.subregion_id LEFT JOIN ile doldurulur; atama yoksa null. |
| Modbus / MQTT gateway tabanlı cihaz | Şu an null — gateway resolution backend tarafında bu endpoint'te uygulanmıyor. Subregion bilgisi gerekiyorsa frontend GET /api/gateways/{id} üzerinden gateway.subregion_id'i çeker. |
| Bağımsız / dangling cihaz | null. |
Kullanım örnekleri (frontend):
- Energy Consumption analiz filtreleri
Tümü / Region / Subregion / Chargermodunda OCPP charger'ları subregion'a göre filtrelemek için bu alanı kullanır. - SLD ve charger listesi sayfaları zaten
OcppChargerListItem.subregion_id(PR-E2, #287) üzerinden çalışıyor;DeviceResponse.subregion_idaynı semantiği/api/devicesortak yüzeyine taşır.
4. GET /api/measurements/aggregated
Request:
curl -X GET "https://api.example.com/api/measurements/aggregated?device_id=d1a2b3c4-...&start=2026-04-01T00:00:00Z&end=2026-04-07T23:59:59Z&interval=daily" \
-H "Authorization: Bearer <access_token>"
Response (200 OK):
{
"device_id": "d1a2b3c4-...",
"interval": "daily",
"start": "2026-04-01T00:00:00Z",
"end": "2026-04-07T23:59:59Z",
"data": [
{
"timestamp": "2026-04-01T00:00:00Z",
"voltage_avg": 231.4,
"current_avg": 12.8,
"power_avg": 2.96,
"energy_kwh": 71.04,
"power_factor_avg": 0.97
},
{
"timestamp": "2026-04-02T00:00:00Z",
"voltage_avg": 229.8,
"current_avg": 13.1,
"power_avg": 3.01,
"energy_kwh": 72.24,
"power_factor_avg": 0.96
}
]
}
Error Model
Tüm API hataları aşağıdaki standart yapıda döner:
{
"detail": "Hata mesajı açıklaması",
"error_code": "HATA_KODU"
}
HTTP Durum Kodları
| Kod | Ad | Açıklama | Örnek error_code |
|---|---|---|---|
| 400 | Bad Request | İstek gövdesi veya parametrelerde doğrulama hatası | VALIDATION_ERROR |
| 401 | Unauthorized | Token eksik, süresi dolmuş veya geçersiz | TOKEN_EXPIRED, INVALID_CREDENTIALS |
| 403 | Forbidden | Yetki yetersiz (rol/izin kontrolü) | PERMISSION_DENIED |
| 404 | Not Found | İstenen kaynak bulunamadı | RESOURCE_NOT_FOUND |
| 409 | Conflict | Kaynak çakışması (duplicate, state conflict) | DUPLICATE_ENTRY, STATE_CONFLICT |
| 422 | Unprocessable Entity | Sözdizimi doğru fakat iş mantığı kuralı ihlali | BUSINESS_RULE_VIOLATION |
| 429 | Too Many Requests | Rate limit aşıldı | RATE_LIMITED |
| 500 | Internal Server Error | Sunucu tarafı beklenmeyen hata | INTERNAL_ERROR |
| 503 | Service Unavailable | Gerekli sunucu-tarafı yapılandırma eksik (ör. EPİAŞ CREDENTIAL_ENCRYPTION_KEY Fernet anahtarı) | encryption_not_configured (bu modülde anahtar error_code değil code) |
Doğrulama Hatası Detaylı Yapısı (422)
Pydantic doğrulama hataları FastAPI tarafından şu formatta döner:
{
"detail": [
{
"loc": ["body", "email"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
Pagination Kuralları
Tüm liste endpoint'leri (GET /api/devices, GET /api/alarms, vb.) ortak pagination parametreleri kullanır.
Query Parametreleri
| Parametre | Tip | Varsayılan | Açıklama |
|---|---|---|---|
skip | int | 0 | Atlanacak kayıt sayısı (offset) |
limit | int | 50 | Döndürülecek maksimum kayıt (max: 200) |
sort_by | string | created_at | Sıralama alanı |
sort_order | string | desc | Sıralama yönü: asc veya desc |
Response Yapısı
{
"items": [ ... ],
"total": 142,
"skip": 0,
"limit": 50
}
- items: Kayıt dizisi
- total: Filtrelere uyan toplam kayıt sayısı
- skip: Kullanılan offset değeri
- limit: Kullanılan limit değeri
Filtreleme
Alan bazlı filtreleme query parametreleri ile yapılır:
GET /api/devices?device_type=inverter&is_online=true
GET /api/alarms/incidents?severity=critical&status=active
Sıralama
GET /api/devices?sort_by=name&sort_order=asc
GET /api/alarms/incidents?sort_by=triggered_at&sort_order=desc
Rate Limiting
API istekleri Nginx seviyesinde rate limit ile korunmaktadır.
| Endpoint Grubu | Limit | Açıklama |
|---|---|---|
/api/auth/login, /api/auth/refresh | 5 istek/dakika | Brute-force koruması |
/api/* (genel) | 30 istek/saniye | Genel API koruması |
Aşım Durumunda
Rate limit aşıldığında HTTP 429 Too Many Requests döner:
{
"detail": "Çok fazla istek gönderildi. Lütfen bekleyin.",
"error_code": "RATE_LIMITED"
}
Retry-After header'ı ile beklenmesi gereken süre (saniye) belirtilir. Client tarafında bu header kontrol edilerek yeniden deneme stratejisi uygulanmalıdır.
Cihaz Güncelleme Endpoint'i (PUT) Davranışları
PUT /api/devices/<id> endpoint'i partial update semantiği taşır; tüm alanlar opsiyoneldir, gönderilmeyenler değişmez. İki kritik özel davranış vardır.
extra_metadata MERGE Semantiği
extra_metadata JSONB kolonu şallow merge stratejisi ile güncellenir. Frontend'in mevcut metadata'yı kaybetmesini engellemek için aşağıdaki kurallar geçerlidir:
| Gönderilen Değer | Davranış |
|---|---|
| Field hiç gönderilmemiş (request body'de yok) | Mevcut değer korunur (no-op) |
"extra_metadata": null | Mevcut değer korunur (kasıtlı no-op) |
"extra_metadata": \{\} | Tüm metadata silinir, boş dict olur (clear) |
"extra_metadata": \{"key": "val"\} | Shallow merge: mevcut diğer key'ler korunur, yeni key eklenir/üzerine yazılır |
Örnek:
// Mevcut: {"templateId": "sofar-hyd", "color": "blue", "notes": "ana"}
// Request body
{ "extra_metadata": { "color": "red" } }
// Sonuç: {"templateId": "sofar-hyd", "color": "red", "notes": "ana"}
null gönderiminin "alanı silmek" anlamına gelmemesi, frontend'in eksik state ile request gönderdiği durumlarda veri kaybını engeller.
MQTT Cihaz template_id Invariant
MQTT-bağlı cihazlar (mqtt_gateway_id IS NOT NULL) için template kuralı:
| Durum | Yanıt |
|---|---|
template_id boş | 422 Unprocessable Entity |
template_id MQTT-uyumsuz template (yalnızca Modbus TCP için) | 422 Unprocessable Entity |
template_id MQTT-uyumlu template | 200 OK |
Service katmanında _validate_mqtt_template_invariant() fonksiyonu çağrılır:
{
"error_code": "MQTT_TEMPLATE_REQUIRED",
"message": "MQTT-bağlı cihaz için MQTT-uyumlu template seçilmelidir",
"device_id": "...",
"current_template_id": null
}
Bu invariant, ESP32 gateway'in cihazı doğru parse edebilmesi için zorunludur. Modbus TCP gateway'ler için template_id opsiyonel olabilir (raw mode'da çalışabilir).