Ana içeriğe geç

API Endpoint Listesi

Tüm endpoint'ler JWT authentication gerektirir (aksi belirtilmedikçe).

Auth

MethodPathAçıklamaAuth
POST/api/auth/loginEmail/password ile girişHayır
POST/api/auth/refreshToken yenilemeHayır
GET/api/auth/meMevcut kullanıcı profiliEvet

Users & Roles

MethodPathAçıklama
GET/api/usersKiracı kullanıcıları
POST/api/usersYeni kullanıcı
GET/POST/api/rolesRol listele/oluştur

Regions & Subregions

MethodPathAçıklama
GET/POST/api/regionsBölge listele/oluştur
GET/PUT/DELETE/api/regions/{id}Bölge detay/güncelle/sil
GET/POST/api/subregionsAlt bölge listele/oluştur
GET/PUT/DELETE/api/subregions/{id}Alt bölge detay/güncelle/sil

Gateways

MethodPathAçıklama
GET/api/gatewaysGateway listesi
GET/api/gateways/mqttLightweight MQTT gateway listesi
POST/api/gatewaysYeni gateway
PUT/DELETE/api/gateways/{id}Gateway güncelle/sil
POST/api/gateways/{id}/rawconfig/syncRaw config push
GET/api/gateways/{id}/rawconfig/previewConfig önizleme

Devices

MethodPathAçıklama
GET/POST/api/devicesCihaz listele/oluştur
GET/PUT/DELETE/api/devices/{id}Cihaz detay/güncelle/sil
GET/api/devices/{id}/hierarchyCihaz ağacı
GET/api/devices/{id}/childrenAlt cihazlar
GET/api/devices/{id}/energy-consumptionEnerji tüketimi
GET/api/devices/device-templatesTemplate listesi
GET/api/devices/device-templates/{id}Template detay
GET.../register-map/downloadRegister map indir
PUT.../register-mapRegister map yükle

Measurements

MethodPathAçıklama
GET/api/measurements/map-dataHarita snapshot
GET/api/measurements/aggregatedAggregated veri (hourly/daily/monthly)
GET/api/measurements/devices/{device_id}/aggregatedCihaz tüketim toplulaştırması (hourly/daily/monthly)
GET/api/measurements/devices/{device_id}/aggregated-productionCihaz PV üretim toplulaştırması (üretim muadili; hourly/daily/monthly)
GET/api/measurements/reactive-ratioReaktif oran
POST/api/measurements/exportCSV/Excel/PDF export

.../aggregated-production notu: .../aggregated (tüketim) endpoint'inin PV üretim muadilidir; aynı AggregatedResponse şemasını döner. Farklar: total_energy = PV üretim kWh deltası (≥ 0 clamp); total_reactive_energy her zaman null; raw-fallback yolunda power_factor/voltage/current null. Detay: API changelog (docs/api/CHANGELOG.md, 2026-07-03).

is_estimated + total_energy sentezi (her iki aggregated ucu; 2026-07-21): AggregatedResponse.data[] satırlarına additive is_estimated: bool (default false) alanı eklendi. Kümülatif enerji register'ı flat/sparse (delta NULL) ama cihaz güç raporluyorsa total_energy avg_power × bucket_saati ile sentezlenir ve o satır is_estimated: true (TAHMİN) olur; register deltasından ölçüldüyse false. Ne register ne güç → total_energy: null (is_estimated: false). Ayrıca gerçek 0.0 artık null yerine 0 dö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

MethodPathAçıklama
GET/POST/api/alarmsAlarm policy listele/oluştur
GET/PUT/DELETE/api/alarms/{id}Policy detay/güncelle/sil
GET/api/alarms/incidentsTetiklenen alarmlar
GET/api/alarms/incidents/countAktif alarm sayıları
GET/api/alarms/incidents/{id}Alarm detay
POST/api/alarms/incidents/{id}/ackAlarm onayla
POST/api/alarms/incidents/{id}/snoozeAlarm ertele
POST/api/alarms/incidents/{id}/closeAlarm kapat
GET/api/alarms/incidents/{id}/eventsAlarm audit trail

PR-7d — AlarmIncidentResponse (incident yanıtı): device_id nullable + alarm_type (additive). Mahsuplaşma/EPİAŞ sistem alarmları cihazsızdır — incident yanıtında device_id: UUID | null olur (null = cihazsız sistem alarmı; cihazlı alarmlarda davranış aynen dolu). Grup/tesis bağlamı payload içinde taşınır (group_id/group_name/facility_id/facility_name/rule_code + scopes). Ayrıca additive alarm_type: string | null alanı eklendi (policy'den join'lenir; FE i18n etiket çözümü için). Breaking YOK (device_id tipi genişledi, daralmadı; alarm_type default null). Şema evrimi: mig 0100 (alarms.device_id DROP 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=NULL tenant-global'dir ve POST /api/alarms alarm-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

MethodPathAçıklama
POST/api/firmware/versionsFirmware upload
GET/api/firmware/versionsFirmware listesi
GET/api/firmware/versions/{id}Firmware detay
GET/api/firmware/versions/{id}/downloadFirmware indir
PATCH/api/firmware/versions/{id}Metadata güncelle
POST/api/firmware/versions/{id}/releaseRelease
POST/api/firmware/versions/{id}/deprecateDeprecated işaretle
POST/api/ota/updatesTekil OTA başlat
POST/api/ota/updates/batchBatch OTA
GET/api/ota/updatesOTA listesi
GET/api/ota/updates/{id}OTA detay
POST/api/ota/updates/{id}/cancelOTA iptal
POST/api/ota/updates/{id}/retryOTA tekrar dene

LOTO

MethodPathAçıklama
GET/POST/api/loto/pointsİzolasyon noktaları
GET/POST/api/loto/sessionsLOTO oturumları
GET/PUT/api/loto/sessions/{id}Oturum detay/güncelle
POST/api/loto/sessions/{id}/approveOnayla
POST/api/loto/sessions/{id}/rejectReddet
POST/api/loto/sessions/{id}/applyUygula
POST/api/loto/sessions/{id}/releaseSerbest bırak
POST/api/loto/locksKilit uygula
DELETE/api/loto/locks/{id}Kilit kaldır

Zigbee

MethodPathAçıklama
POST/api/v1/zigbee/claims/generateClaim code oluştur
GET/api/v1/zigbee/claims/pendingBekleyen claim'ler
DELETE/api/v1/zigbee/claims/{id}Claim iptal
GET/api/v1/zigbee/gatewaysZigbee gateway'ler
GET/PUT/DELETE/api/v1/zigbee/gateways/{id}Gateway CRUD
GET/api/v1/zigbee/gateways/{id}/devicesGateway cihazları

Widgets

MethodPathAçıklama
GET/api/subregions/{id}/widgetsWidget listesi
POST/api/widgetsWidget oluştur
PUT/DELETE/api/widgets/{id}Widget güncelle/sil
GET/api/subregions/{id}/widgets/dataToplu veri — alt bölgedeki tüm aktif widget'ların verisi tek yanıtta
GET/api/widgets/{id}/data/device-read-rateCihaz okuma oranı
GET/api/widgets/{id}/data/consumption-chart · consumption-numericTüketim (grafik / sayısal)
GET/api/widgets/{id}/data/cost-chart · cost-numericMaliyet (grafik / sayısal)
GET/api/widgets/{id}/data/reactiveReaktif oran
GET/api/widgets/{id}/data/realtime-value · realtime-gauge · realtime-3phase · realtime-powerAnlı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).

Anlık uçlar yalnız son 60 dakika (2026-07-29 davranış değişikliği)

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).

BREAKING — RealtimeGaugeData alan adları (2026-07-29)

realtime-gauge yanıtında: min_valuemin_range, max_valuemax_range, danger_thresholdcritical_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.periodtime_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

MethodPathİzinAçıklamaHata sözleşmesi
GET/api/mahsuplasma/scopesread:nettingVKN'siz mahsuplaşma kapsamlarını listele (ad sıralı; limit/offset)
POST/api/mahsuplasma/scopesmanage:nettingYeni kapsam oluştur (beyan alanları bu endpoint'ten yazılamaz)409 duplicate (tenant+name)
PATCH/api/mahsuplasma/scopes/{id}manage:nettingKapsam güncelle (name/external_party_ref/notes; beyan alanları değiştirilemez)404¹; 409 duplicate
POST/api/mahsuplasma/scopes/{id}/declare-same-official-partymanage: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/facilitiesread:nettingResmi tesisleri listele (tarihçeli tablo; scope_id/subregion_id filtreleri AND)
POST/api/mahsuplasma/facilitiesmanage:nettingYeni 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-C2422 (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:nettingTesisi 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}superuserTesisi KALICI sil (hard-delete — GERİ ALINAMAZ; arşivleme değil). Cross-tenant global (superadmin); 204 döner401/403ᴱ; 404 (olmayan id); **409 facility_has_dependencies**ᶠ
GET/api/mahsuplasma/organizationsread:nettingGörünür resmi kurumları listele (kendi tenant + sistem-geneli salt-okunur; org_type/include_inactive filtreleri)
POST/api/mahsuplasma/organizationsmanage:nettingYeni 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:nettingKurum 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).

MethodPathİzinAçıklamaHata sözleşmesi
GET/api/mahsuplasma/facilities/{id}/metersread:nettingTesise bağlı ölçüm noktalarını listele (tarihçeli; yeni→eski)404¹ (yabancı/olmayan tesis)
POST/api/mahsuplasma/facilities/{id}/metersmanage:nettingTesise 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/assignmanage:nettingCihaz → ölçüm rolü ataması oluştur (valid_from zorunlu)422²; 409 dönem çakışması³
GET/api/mahsuplasma/meters/assignmentsread:nettingÖlçüm rolü atamalarını listele (tarihçeli; subregion_id/facility_id filtreleri AND)
PATCH/api/mahsuplasma/meters/assignments/{id}manage:nettingAtamayı 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/suggestionsread:nettinginverter_type öneri listesi (on-demand; SOFAR+HYD→ess, SUN2000/KTLX→grid, belirsiz→öneri yok)
POST/api/mahsuplasma/inverter-type/applymanage:nettingEXPLICIT (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ğildirvalid_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).

MethodPathİzinAçıklamaHata sözleşmesi
GET/api/mahsuplasma/groupsread:nettingMahsuplaşma gruplarını listele (yeni→eski; scope_id/is_active filtreleri AND)
POST/api/mahsuplasma/groupsmanage:nettingYeni grup oluştur (uygunluk alanları yazılamaz; not_checked doğar)409 duplicate (tenant+name); 422⁷
PATCH/api/mahsuplasma/groups/{id}manage:nettingGrubu güncelle (uygunluk alanları değiştirilemez)404¹; 409 state-machine⁸; 422⁷
POST/api/mahsuplasma/groups/{id}/membersmanage:nettingGruba ü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öner404¹
POST/api/mahsuplasma/groups/{id}/evaluatemanage:nettingUygunluk motorunu çalıştır (yazma: FOR UPDATE + salt-append log + eligibility_status günceller)404¹
GET/api/mahsuplasma/groups/{id}/eligibilityread:nettingUygunluk ö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_definedek1_submittedgrid_operator_confirmedlum_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: evaluate her zaman 13 kuralı register eder, ancak bazı kurallar bu fazda gerekli veri kaynağı henüz bağlı olmadığından status='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 fazla not_checked olur.

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).

MethodPathİzinAçıklamaHata sözleşmesi
GET/api/mahsuplasma/limits/accountsread:nettingBedelli 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/accountsmanage:nettingHesap 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}/movementsread:nettingHesabı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}/adjustmanage:nettingManuel düzeltme (manual_adjustment hareketi; salt-append). amount_kwh işaretli (sıfır reddedilir); reason zorunlu404¹
POST/api/mahsuplasma/limits/transfermanage: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_checked dönen R007 (bedelli limit hesabı var mı — mesken hariç) kuralı, mig 0095 ledger 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 ait bedelli_limit_accounts kaydı YOKSABLOCKING FAIL → grup eligibility_status='blocked' (daha önce not_checked'ti). Çözüm: önce POST /limits/accounts ile 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: önceden evaluate'te nötr görünen mesken-dışı gruplar artık blocked dönebilir (kasıtlı — Md.7(1) limit tüketim tesisi bazında zorunludur). kod/severity sözleşmesi değişmedi (R007 hep blocking); yalnız status çı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 — /organizations plan §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_reports izni 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).

MethodPathİzinAçıklamaHata sözleşmesi
GET/api/mahsuplasma/facilities/{id}/hourly-energyread:nettingTesis 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şır404¹ (yabancı/olmayan tesis)
POST/api/mahsuplasma/facilities/{id}/hourly-energy/rebuildmanage: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_reportgts_reportpys_importosos_official_meterzeus_metercustomer_declaredprofiled_estimatemissing. 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 GET /facilities/{id} YOK (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 tekil GET /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.

MethodPathİzinAçıklamaHata sözleşmesi
POST/api/mahsuplasma/groups/{id}/calculatemanage: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}/hourlyread: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 ≤ 2000404¹
GET/api/mahsuplasma/groups/{id}/virtual-metersread: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 ≤ 2000404¹
GET/api/mahsuplasma/groups/{id}/amountsread: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 ≤ 2000404¹
GET/api/mahsuplasma/groups/{id}/monthly-summaryread:nettingAylı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 ≤ 500404¹
POST/api/mahsuplasma/groups/{id}/correctionsmanage: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 datetimefrom_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 warnings alanı (M3/M4): PATCH /groups/{id}/members/{member_id} yanıt şemasına warnings: string[] alanı eklendi (additive — varsayılan boş liste; create/list akışlarında hep boş). Üyeliğin valid_to'su geçmişe çekildiğinde artık etkilenen ilk dönemden itibaren düzeltme cascade'i otomatik tetiklenir (üyelik kümesi input_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 ile warnings'e sıra-eşli warning_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 grup blocked olur. Hesap yine de durmazcalculate, bloklu grupta invalid_group_bedelsiz senaryosunu 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_type değerlerinin son 4'ü (mahsuplasan_consumption / bedelli_ihtiyac_fazlasi run bağlı doğal hareketler + correction_reversal / correction_reapply) PR-6 ile fiilen yazılmaya başladı; hareketler calculation_run_id taşır. POST /limits/transfer'in 422 INSUFFICIENT_LIMIT_BALANCE sö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.

MethodPathİzinAçıklamaHata sözleşmesi
GET/api/mahsuplasma/groups/{id}read:nettingTekil 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'de404¹
GET/api/mahsuplasma/facilities/{id}read:nettingTekil resmî tesisi getir (OfficialFacilityResponse — liste elemanıyla birebir; PR-5'teki "tekil-get yok" notunun kapanışı)404¹
GET/api/mahsuplasma/groups/{id}/membersread: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}/runsread:nettingHesap 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_cascade404¹
GET/api/mahsuplasma/groups/{id}/dashboard-summaryread:netting8 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_data empty-state: dönemde hiç current saatlik satır VE hiç run yoksa has_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_calc CURRENT satır SUM'u; TL alanlarımahsuplasma_settlement_amount_hourly CURRENT SUM(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 yoksa null.
  • bedelsiz_risk_try SERVER-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şenin tariff_prices.id'si). Fiyat çözülmezse null + warnings (ve sıra-eşli warning_codes — PR-7b ↓) sebep taşır; bedelsiz_kwh=0 ise 0.
  • bedelli_limit_kalan_kwh CANLI: bedelli_limit_balances VIEW 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 yoksa null.
  • veri_kalitesi_pct bugü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 yoksa null.
  • last_run — seçilen dönemin en son başlatılan run'ı; tam liste GET /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} (MahsuplasmaGroupMemberResponseGET /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ÜreticiAnlam
TRANSFER_CONTRACTED_POWER_NOT_CONFIRMEDlimits/transferMd.5(4) sözleşme gücü beyan edilmedi (İSKELET/beta)
TARIFF_MESKEN_STEP_FALLBACK_FLATtutar motoru (28/11)mesken kademe fiyatı yok → tek-zamanlı tarifeye düşüldü
TARIFF_MESKEN_UNRESOLVEDtutar motoru (28/11)mesken tarife fiyatı hiç çözülemedi (satır üretilmedi)
TARIFF_STEP_STATE_MISSING_CONSERVATIVEtutar motoru (28/12)kademe durumu yok → muhafazakâr yüksek-kademe fallback
TARIFF_LOWEST_RELATED_FALLBACKtutar motoru (28/13)lowest_related yok → ilgili tarifelerin en düşüğü
TARIFF_NONE_RESOLVEDtutar motoru (28/13)hiçbir ilgili tarife çözülemedi (satır üretilmedi)
FACILITY_ABONE_GRUBU_UNKNOWNtutar motorutesisin abone grubu bilinmiyor (tedarikçi satırı yok)
SKTT_PRICE_UNRESOLVEDtutar motoruSKTT fiyatı çözülemedi (SKTT satırı üretilmedi)
SUPPLIER_PRICE_UNRESOLVEDtutar motoruilgili tarife çözülemedi (supplier_payment satırı yok)
SKB_PRICE_UNRESOLVED_QUANTITY_ONLYtutar motorusystem_usage fiyatı yok → SKB satırı quantity-only
SKB_LU_GROUP_MIXEDtutar 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_DERIVEDtutar motoru (§10.3, C2)üretici LÜ grubu source_type'tan türetildi (açık producer_tariff_group override yok)
SKB_LU_GROUP_UNKNOWN_DEFAULTtutar motoru (§10.3, C2)üretici LÜ grubu belirlenemedi (hybrid/unknown/NULL) → güneş-baskın varsayılan (lisanssiz_uretici_2)
MESKEN_MONTHLY_PRICE_UNRESOLVEDtutar motorumesken aylık tarife çözülemedi → satır quantity-only
LIMIT_ACCOUNT_MISSING_CONSERVATIVE_SKBhesap koşumu (R007)limit hesabı yok → surplus muhafazakâr SKB'ye yazıldı
GROUP_BLOCKED_BEDELSIZhesap koşumugrup blocked → aylık üretim bedelsiz sayıldı
DUY_133_5_PERIOD_TOO_OLDdüzeltme motoruDUY 133/5: dönem 12 aydan eski (istek yine işlenir)
MONTHLY_REFRESH_FAILEDdüzeltme motorudüzeltme sonrası aylık özet yeniden hesaplanamadı
CASCADE_STOPPEDdüzeltme motorucascade 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.

MethodPathİzinAçıklamaHata sözleşmesi
GET/api/v1/admin/regulated-tariffs/price-setssuperuserTarife paketlerini listele (yeni→eski; billing_period/approval_status filtreleri AND)401/403ᴬ
POST/api/v1/admin/regulated-tariffs/price-setssuperuserPaket 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}superuserPaketi güncelle (approved→archived + draft metadata düzeltme)401/403ᴬ; 404ᴮ; 409 immutable/transitionᶜ
POST/api/v1/admin/regulated-tariffs/price-sets/{id}/approvesuperuserdraft → approved (approved_by=token; fiyatlar immutable olur)401/403ᴬ; 404ᴮ; 409 TARIFF_ALREADY_APPROVED
DELETE/api/v1/admin/regulated-tariffs/price-sets/{id}superuserPaketi sil — YALNIZ draft (CASCADE fiyatlar); 204 döner401/403ᴬ; 404ᴮ; 409 TARIFF_SET_NOT_DELETABLE
GET/api/v1/admin/regulated-tariffs/price-sets/{id}/pricessuperuserPaketin fiyat satırlarını listele (yeni→eski)401/403ᴬ; 404ᴮ
POST/api/v1/admin/regulated-tariffs/price-sets/{id}/pricessuperuserTek 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-csvsuperuserCSV 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-templatesuperuserÖ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).

KRİTİK ürün kuralı — PTF/SMF advisory'dir

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.

MethodPathİzinAçıklamaHata sözleşmesi
GET/api/energy-market/pricesread:nettingULUSAL PTF/SMF fiyat serisi (aralık + market filtreleri; tenant filtresi YOK)401/403ᴬ
GET/api/v1/admin/epias-transparency/accountssuperuserAPI Portal hesap listesi (kimlik bilgileri maskeli)401/403ᴬ
POST/api/v1/admin/epias-transparency/accountssuperuserHesap oluştur (portal_password/subscription_key Fernet şifreli)401/403ᴬ; 503ᴰ
GET/api/v1/admin/epias-transparency/accounts/{id}superuserHesap detayı (maskeli)401/403ᴬ; 404ᴮ
PATCH/api/v1/admin/epias-transparency/accounts/{id}superuserHesabı güncelle401/403ᴬ; 404ᴮ; 503ᴰ
POST/api/v1/admin/epias-transparency/accounts/{id}/transitionsuperuserwhitelist_status durum geçişi (9 adım)401/403ᴬ; 404ᴮ; 409ᶜ
POST/api/v1/admin/epias-transparency/accounts/{id}/test-callsuperuserBağlantı/kimlik test çağrısı — ASLA 500 üretmez401/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-call yeş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

ProtocolPathAçıklama
WS/ws/telemetry?token=JWT&devices=id1,id2Gerçek zamanlı ölçüm
GET/ws/statsHub istatistikleri

Sistem

MethodPathAçıklamaAuth
GET/healthSağlık kontrolüHayır
GET/readinessDB hazırlıkHayır
GET/api/config/publicFrontend configHayır
POST/api/internal/gateway/connectedEMQX webhookHayır*
POST/api/internal/gateway/disconnectedEMQX webhookHayı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 tipisubregion_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 cihaznull.

Kullanım örnekleri (frontend):

  • Energy Consumption analiz filtreleri Tümü / Region / Subregion / Charger modunda 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_id aynı semantiği /api/devices ortak 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ı

KodAdAçıklamaÖrnek error_code
400Bad Requestİstek gövdesi veya parametrelerde doğrulama hatasıVALIDATION_ERROR
401UnauthorizedToken eksik, süresi dolmuş veya geçersizTOKEN_EXPIRED, INVALID_CREDENTIALS
403ForbiddenYetki yetersiz (rol/izin kontrolü)PERMISSION_DENIED
404Not Foundİstenen kaynak bulunamadıRESOURCE_NOT_FOUND
409ConflictKaynak çakışması (duplicate, state conflict)DUPLICATE_ENTRY, STATE_CONFLICT
422Unprocessable EntitySözdizimi doğru fakat iş mantığı kuralı ihlaliBUSINESS_RULE_VIOLATION
429Too Many RequestsRate limit aşıldıRATE_LIMITED
500Internal Server ErrorSunucu tarafı beklenmeyen hataINTERNAL_ERROR
503Service UnavailableGerekli 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

ParametreTipVarsayılanAçıklama
skipint0Atlanacak kayıt sayısı (offset)
limitint50Döndürülecek maksimum kayıt (max: 200)
sort_bystringcreated_atSıralama alanı
sort_orderstringdescSı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 GrubuLimitAçıklama
/api/auth/login, /api/auth/refresh5 istek/dakikaBrute-force koruması
/api/* (genel)30 istek/saniyeGenel 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ğerDavranış
Field hiç gönderilmemiş (request body'de yok)Mevcut değer korunur (no-op)
"extra_metadata": nullMevcut 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ı:

DurumYanı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 template200 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).