Mahsuplaşma Veri Modeli
Bu sayfa, mahsuplaşma (netting) modülünün veritabanı şemasını (migration'lar
0090–0098 + EPİAŞ Şeffaflık 0102) anlatır: tablolar, amaçları, kritik
constraint'ler (EXCLUDE / UNIQUE / CHECK / FK), bedelli_limit_balances görünümü,
facility_hourly_energy hypertable'ı (domenin ilk hypertable'ı), hesap
çekirdeği tabloları (0098 — 3 yeni hypertable dahil), EPİAŞ Şeffaflık Platformu
tabloları (0102 — epias_api_portal_accounts, epias_fetch_logs, market_prices
hypertable) ve btree_gist önkoşulu. Mimari akış
için Mahsuplaşma Modül Mimarisi sayfasına
bakın.
Zeus'ta katalog tablolarının PK'ları UUID'dir (UUIDMixin). İstisna:
salt-append log/ledger tabloları (mahsuplasma_eligibility_results,
bedelli_limit_movements) BIGSERIAL kullanır — satır sırası = insert sırası,
kompakt index, deterministik sıralama. 0097/0098 hypertable'ları + aylık/kademe
tabloları ise composite PK taşır (partition kolonu ts/hour ilk sırada —
TimescaleDB şartı); 0098'in yüksek hacimli satır tabloları (virtual_meter /
settlement_amount) BIGSERIAL yerine BIGINT Identity kullanır.
Migration → tablo haritası
| Migration | Objeler | Runbook |
|---|---|---|
0090 mahsuplasma_regulatory_foundation | mahsuplasma_scopes, official_organizations, official_facilities | — |
0091 backfill_netting_permissions | RBAC izin backfill (dormant): read/manage:netting, import:settlement_reports | — |
0092 mahsuplasma_metering_inverter | btree_gist, metering_points, subregion_meter_assignments, devices.inverter_type | 0092_mahsuplasma_metering_btree_gist.md |
0094 mahsuplasma_groups_eligibility | mahsuplasma_groups, mahsuplasma_group_members, mahsuplasma_eligibility_results | 0094_mahsuplasma_groups_eligibility.md |
0095 bedelli_limit_ledger | bedelli_limit_accounts, bedelli_limit_movements, bedelli_limit_balances (VIEW) | 0095_0096_limit_ledger_regulated_tariff.md |
0096 regulated_tariff_prices | tariff_price_sets, tariff_prices (ULUSAL — tenant_id yok) | 0095_0096_limit_ledger_regulated_tariff.md |
0097 add_facility_hourly_energy | facility_hourly_energy (domenin İLK hypertable'ı; compression 500g, RETENTION YOK) | 0097_facility_hourly_energy.md |
0098 mahsuplasma_calculation_core | Hesap çekirdeği: mahsuplasma_calculation_runs, mahsuplasma_hourly_group_calc (hypertable, compression'lı), mahsuplasma_virtual_meter_hourly (hypertable), mahsuplasma_settlement_amount_hourly (hypertable), mahsuplasma_monthly_calc, tariff_step_monthly_state + 2 ALTER (bedelli_limit_movements.calculation_run_id FK köprüsü; mahsuplasma_groups.limit_ratio_basis) | 0098_mahsuplasma_calculation_core.md |
0102 epias_transparency_market_prices | EPİAŞ Şeffaflık: epias_api_portal_accounts, epias_fetch_logs, market_prices (hypertable, compression'lı, RETENTION YOK) | 0102_epias_transparency.md |
0111 add_producer_tariff_group | official_facilities.producer_tariff_group ALTER (ADDITIVE nullable VARCHAR(24); SKB fiyat tabanı LÜ-grubu override'ı — PR-C2; DB CHECK yok, Pydantic-only whitelist) | 0111_producer_tariff_group.md |
Not:
0093mahsuplaşmaya ait değildir (charging-profile). PR-3 rechain'i nedeniyle plandaki "ledger 0094 / tarife 0095" numaraları gerçekte 0095/0096'ya kaydı; plan §6.8'in "0097" dediği hesap çekirdeği migration'ı da gerçekte 0098'dir (0097'yifacility_hourly_energyaldı — off-by-one).0098→0102aradaki üç migration (0099widget CHECK genişletme,0100alarms.device_idnullable,0101alarm politika seed'i) yeni tablo eklemez. Benzer şekilde0109(price_purpose7→9),0110(tariff_pricesdoğal anahtar genişletme) ve0111(official_facilities.producer_tariff_groupALTER) yeni tablo eklemez —0111mevcut tesis tablosuna additive nullable kolon ekler. Runbook'lardocs/runbook/altındadır.
Tablolar ve amaçları
| Tablo | Amaç | Kritik constraint'ler |
|---|---|---|
mahsuplasma_scopes | VKN'siz resmî kapsam (Zeus alias) | UNIQUE(tenant_id, name); CHECK declaration_source |
official_organizations | Kurum kataloğu (şebeke/dağıtım/GTŞ/OSB-EB/sayaç okuyan/tedarikçi). tenant_id=NULL = sistem-geneli | UNIQUE NULLS NOT DISTINCT (tenant_id, org_type, name) |
official_facilities | Resmî üretim/tüketim tesisi (tarihçeli) | CHECK role/regime/topology/ESS SOC/ESS tutarlılık |
metering_points | Ölçüm noktası (tarihçeli; OSOS köprüsü opsiyonel) | CHECK connection_level; osos_installation_id → SET NULL |
subregion_meter_assignments | Cihaz → ölçüm rolü (tarihçeli) | EXCLUDE ex_sma_no_overlap_main_meter; CHECK meter_role/energy_semantics/scale_factor |
mahsuplasma_groups | Müşteri tanımlı grup | UNIQUE(tenant_id, name); CHECK group_status/eligibility_status |
mahsuplasma_group_members | Grup üyesi tesis (tarihçeli) | EXCLUDE ex_mgm_facility_single_group_per_period; group_id → CASCADE |
mahsuplasma_eligibility_results | Uygunluk log'u (salt-append, BIGSERIAL) | CHECK severity/status; group_id/facility_id → SET NULL |
bedelli_limit_accounts | Tesis × yıl bedelli limit hesabı | UNIQUE(facility_id, year); CHECK limit_source |
bedelli_limit_movements | Salt-append hareket defteri (BIGSERIAL) | CHECK movement_type/ratio_basis; account_id → NO ACTION, group_id → SET NULL |
bedelli_limit_balances | Kalan limit PLAIN VIEW (canlı) | — (view; ORM'e map edilmez) |
tariff_price_sets | Regüle tarife paketi (ULUSAL, tenant_id yok) | CHECK approval_status |
tariff_prices | Tarife fiyat satırı (immutable-append) | UNIQUE NULLS NOT DISTINCT natural key; price_set_id → CASCADE |
facility_hourly_energy | Saatlik enerji fact (hypertable; resolver yazar, kaynak-başına satır) | Composite PK (hour, facility_id, role, source); CHECK YOK (role/source/data_quality Pydantic-only); FK tenant/facility RESTRICT |
mahsuplasma_calculation_runs | Hesap koşumu (run) kataloğu — fact tablolarının FK kökü | CHECK calculation_type (5) / status (4); parent_run_id SELF-FK RESTRICT |
mahsuplasma_hourly_group_calc | Saatlik grup hesap fact'i (hypertable, compression'lı TEK tablo) | PK (ts, group_id, calculation_run_id); partial-uq uq_current_hourly_group_calc; CHECK YOK (hypertable ailesi) |
mahsuplasma_virtual_meter_hourly | Sanal sayaç saatlik satırları — Md.10 (hypertable) | PK (ts, id); expression-uq uq_vm_hourly_dims (zero-uuid COALESCE); CHECK YOK |
mahsuplasma_settlement_amount_hourly | Saatlik tutar satırları — Md.11-12 (hypertable) | PK (ts, id); expression-uq uq_settlement_amount_dims; CHECK YOK |
mahsuplasma_monthly_calc | Aylık grup özeti (mesken resmî sonuç + roll-up; plain) | PK (period_start, group_id, calculation_run_id); partial-uq uq_current_monthly_calc |
tariff_step_monthly_state | Kademeli tarife aylık tesis durumu (plain) | PK (period_start, facility_id, source) — source PK üyesi (NOT NULL) |
epias_api_portal_accounts | EPİAŞ API Portal hesap kataloğu (Fernet şifreli credential; superadmin) | CHECK whitelist_status (9); tenant_id → RESTRICT (NULL = sistem-geneli) |
epias_fetch_logs | EPİAŞ API çekim denetim günlüğü (salt-append, BIGSERIAL) | CHECK status (4); account_id → NO ACTION (denetim izi kopmaz) |
market_prices | Piyasa fiyat zaman serisi (hypertable; PTF/SMF, RETENTION YOK) | Composite PK (ts, market, price_status); CHECK market (6) / price_status (4) |
facility_hourly_energy — domenin ilk hypertable'ı (0097)
facility_hourly_energy, mahsuplaşma domeninin ilk TimescaleDB hypertable'ıdır.
Bir resmî tesisin bir saat bucket'ındaki üretim / tüketim / iç-tüketim / çekiş /
veriş / batarya değerlerini fatura-kalitesinde (NUMERIC) tutar.
MeasurementResolver (PR-5) yazar; mahsuplaşma hesap run'ları (PR-6+) okur. Ayrıntı:
Mimari — MeasurementResolver.
Şema özeti
| Kolon | Tip | Not |
|---|---|---|
hour | TIMESTAMPTZ | Partition kolonu + PK[1]. Saat başı bucket |
tenant_id | UUID | FK → tenants (RESTRICT) |
facility_id | UUID | FK → official_facilities (RESTRICT); PK[2] |
subregion_id | UUID? | FK → subregions (SET NULL) |
metering_point_id | UUID? | FK → metering_points (SET NULL) |
meter_assignment_id | UUID? | FK → subregion_meter_assignments (SET NULL) |
role | VARCHAR(32) | PK[3]. production / consumption / pcc / osb_eb_consumption |
production_kwh, consumption_kwh, internal_consumption_kwh, import_kwh, export_kwh | NUMERIC(16,6) | Enerji; server_default '0' |
battery_charge_kwh, battery_discharge_kwh | NUMERIC(16,6)? | DEFAULT'suz → NULL = "ölçülmedi" (batarya yok) ayrımı |
avg_battery_soc_pct | NUMERIC(8,3)? | Ortalama SoC |
data_quality | VARCHAR(32)? | complete/partial/missing/corrected/suspect/estimated_profiled/unknown |
completeness_pct | NUMERIC(5,2)? | Ölçüm tamlığı |
source | VARCHAR(32) | PK[4]. 8 kanonik kaynak (default 'zeus_meter') |
calculated_at | TIMESTAMPTZ? | UPSERT'te NOW() |
Birincil anahtar — idempotent upsert
PK (hour, facility_id, role, source). Resolver ON CONFLICT (hour, facility_id, role, source) DO UPDATE ile yazar. TimescaleDB kuralı gereği partition
kolonu (hour) PK'nin parçasıdır. Farklı source (zeus_meter vs
osos_official_meter vs lum_official_report) aynı saat/tesis/rol için ayrı satır
üretir — öncelik seçimi okuma katmanında yapılır, veri kaybı olmaz.
CHECK YOK — role / source / data_quality serbest VARCHAR (bilinçli)
Bu üç kolona kasıtlı olarak DB CHECK eklenmemiştir (migration 0063
osos_meter_readings.data_type deseninin devamı): izin-verilen değerler Pydantic
(application-layer) ile daraltılır; yeni kaynak tipi migration-siz eklenebilsin
diye DB serbest bırakıldı. Sentinel notu: CHECK yokluğu bug değildir; ileride
daraltma gerekirse ayrı migration ile CHECK (NOT VALID → VALIDATE) eklenir.
Compression 500 gün + RETENTION YOK
- Compression: 500 gün sonra sıkıştır (
segmentby = facility_id, role, source;orderby = hour DESC). Sıra kritik: önceALTER ... SET (timescaledb.compress, …)ENABLE, sonraadd_compression_policy— ters sıra hata verir. Chunk aralığı 30 gün. - Neden 500 gün? DUY 133/5 düzeltme ufku ~490 gündür (12 ay itiraz + 3 ay
sonuçlandırma + bildirim payı
<500g); bu pencerede satırlar geç-upsert'e açık kalmalıdır (sıkıştırılmış chunk'ta UPDATE maliyetlidir). - RETENTION YOK (kasıtlı):
add_retention_policyEKLENMEZ — mahsuplaşma fact verisi mutabakat/itiraz kaynağıdır, silinmemelidir. Migration0097bu yokluğu yorumla işaretler ki kimse kazara retention (DROP) eklemesin.
Ek index
ix_facility_hourly_energy_facility_role_hour (facility_id, role, hour DESC) —
resolver/calculator tesis-lider okuma yapar (WHERE facility_id = ? AND role = ? ORDER BY hour DESC); PK hour-lider olduğu için bu sorguyu kapsamaz. tenant_id
tek-kolon index kasıtlı yok (tenant-scoped sorgular pratikte facility_id ile
birlikte gelir).
Hesap çekirdeği tabloları (0098) — 6 tablo, 3'ü hypertable
Migration 0098_mahsuplasma_calculation_core, hesap çekirdeğinin şema
katmanını kurar (yalnız şema — hiç veri yazmaz): 6 yeni tablo (3'ü
hypertable) + 2 düşük riskli ALTER. Operasyon notları:
docs/runbook/0098_mahsuplasma_calculation_core.md.
Şema özeti
| Tablo | Tür | PK | Ana kolon aileleri |
|---|---|---|---|
mahsuplasma_calculation_runs | plain | id UUID | billing_period, calculation_type (CHECK: hourly_estimate/monthly_close/correction/forecast/official_import), status (CHECK: running/success/failed/partial), source, algorithm_version, triggered_by, parent_run_id (SELF-FK), input_hash/output_hash, is_current (bilgilendirici), updated_at |
mahsuplasma_hourly_group_calc | hypertable (chunk 30g) | (ts, group_id, calculation_run_id) | Md.9(2) kWh kolonları (total_production/consumption, mahsuplasan_tuketim, sebekeden_alinan, ihtiyac_fazlasi) + Md.7 limit kolonları (limit_before/used/after, bedelli_ihtiyac_fazlasi, skb_odemeli, bedelsiz), calculation_status, warning_flags JSONB, source, is_current |
mahsuplasma_virtual_meter_hourly | hypertable (chunk 30g) | (ts, id) — id BIGINT Identity | virtual_meter_type (bedelli/skb_odemeli/bedelsiz), boyutlar (grid_operator_id, distribution_region, source_type), tekil-tesis anotasyonları, kwh, reason_code, is_current |
mahsuplasma_settlement_amount_hourly | hypertable (chunk 30g) | (ts, id) — id BIGINT Identity | amount_type (7 kanonik değer), taraf FK'ları (supplier/responsible_supplier/grid_operator/tesisler), quantity_kwh, unit_price_try_per_kwh, amount_try, tariff_price_id (provenance), is_current |
mahsuplasma_monthly_calc | plain | (period_start, group_id, calculation_run_id) | Aylık toplamlar (total_monthly_*, monthly_matched/surplus, bedelli_production, bedelsiz), source, is_current |
tariff_step_monthly_state | plain | (period_start, facility_id, source) | Kademeli tarife durumu: low_step_threshold_kwh, total_consumption_to_date_kwh, low/high_step_used_kwh (kronolojik doldurma — düşük kademe saat sırasıyla dolar) |
NUMERIC kararları (Float hiçbir yerde yok): tüm kWh kolonları
NUMERIC(16,6) (0097 emsali); unit_price_try_per_kwh NUMERIC(16,8) (0096
tariff_prices.price emsali); amount_try NUMERIC(18,6). Bedelli limit
ailesi NUMERIC(16,3) kalır — hourly (16,6) → ledger (16,3) yuvarlaması
serviste tek noktada yapılır (quantize_ledger_kwh, ROUND_HALF_EVEN).
Compression yalnız mahsuplasma_hourly_group_calc
- segmentby =
group_id, source(calculation_run_idgirmez — yüksek kardinalite segment'i parçalar;tenant_idgereksiz — group_id zaten tenant-scoped;is_currentzararlı — flip UPDATE'i segment'i bozar); orderby =ts DESC; compress_after = 500 gün — DUY 133/5 düzeltme ufku ~490 gün:is_currentflip'leri hep sıkışmamış chunk'ta kalır. mahsuplasma_virtual_meter_hourly+mahsuplasma_settlement_amount_hourlyFaz 1'de bilinçli compression'sız: correction cascade'in run bazlıis_currentUPDATE churn'ü sıkıştırılmış chunk'ta çok pahalı olurdu; Faz 2'de değerlendirilir.- RETENTION HİÇBİR TABLODA YOK (kasıtlı): mahsuplaşma fact/denetim verisi
mutabakat-itiraz kaynağıdır ve yasal saklama kapsamındadır —
add_retention_policyyazılmaz; migration yokluğu yorumla işaretler.
is_current / partial-unique modeli
Current'lık veri satırındadır; runs.is_current bilgilendiricidir
(run seviyesinde partial-unique konmaz — çoklu-current run meşrudur:
saatlik beat farklı dilimleri işler; hourly + monthly run'lar birlikte current
olabilir):
| Index | Tablo | Kural |
|---|---|---|
uq_current_hourly_group_calc | hourly_group_calc | (ts, group_id, source) WHERE is_current — saat başına tek current satır |
uq_current_monthly_calc | monthly_calc | (period_start, group_id, source) WHERE is_current — çifte-current mesken tutarı çift sayımını önler (plan'a EK) |
uq_vm_hourly_dims | virtual_meter_hourly | expression-unique: (ts, group_id, run, tip, COALESCE(grid_operator_id, zero-uuid), COALESCE(source_type, '')) |
uq_settlement_amount_dims | settlement_amount_hourly | expression-unique: (ts, group_id, run, amount_type, COALESCE(tesis/bölge FK'ları, zero-uuid)) |
VM/tutar tablolarında is_current unique'e eklenmez — flip run bazlı
topluca yapılır (UPDATE ... WHERE calculation_run_id = :old_run; pencere
genişletmesi eski run kapsamını tam örtmeyi garanti eder). NULL boyutlar
zero-uuid / boş-string sentineliyle tek sınıfa katlanır (taşınabilir COALESCE
deseni — NULLS NOT DISTINCT alternatifi).
FK ondelete matrisi (0098) — istisnasız RESTRICT
Fact katmanının tüm FK'ları RESTRICT'tir (21 explicit ad): tenant_id →
tenants, group_id → mahsuplasma_groups, calculation_run_id → runs,
parent_run_id → runs (SELF), kurum FK'ları → official_organizations, tesis
FK'ları → official_facilities, tariff_price_id → tariff_prices. SET NULL
bilinçli yok: expression-unique'lerdeki COALESCE kolonlarında SET NULL,
mevcut NULL'lu satırla unique-violation üretebilirdi; mali/denetim satırı
hard-delete zinciriyle koparılamaz. Tek istisna:
bedelli_limit_movements.calculation_run_id → NO ACTION (0095 tablo-içi
simetri). Hiçbir FK hypertable'ı hedeflemez (TimescaleDB kuralı — tüm FK
hedefleri plain tablolardır).
group_id FK'ları RESTRICT olduğu için, hesap koşumu/fact satırı oluşmuş bir
mahsuplaşma grubu artık hard-delete edilemez (DB engeller). Doğru akış:
grubu group_status='inactive' yapmak. (Grup silme endpoint'i zaten yoktur;
bu kural DB/operatör düzeyinde geçerlidir — servis katmanı FK hatasını "önce
pasifleştirin" 409'una çevirir.)
ALTER'lar (düşük risk — hot-table değil)
- (b)
bedelli_limit_movements.calculation_run_id→ runs FK köprüsü: kolon 0095'te ileri-referans olarak açılmıştı (tümden NULL) → FK validation anlık,NOT VALIDgereksiz; + partial indexix_blm_calculation_run_id. PR-6 ilemovement_type'ın son 4 değeri (correction_reversal/correction_reapply+ run bağlı doğal hareketler) fiilen yazılmaya başladı. - (c)
mahsuplasma_groups.limit_ratio_basisVARCHAR(16) NULL + CHECK (remaining/initial, NULL-toleranslı). DB default bilinçli yok — NULL →'remaining'mirası serviste çözülür (resolve_ratio_basis).
EPİAŞ Şeffaflık Platformu tabloları (0102) — 3 tablo, 1 hypertable
Migration 0102_epias_transparency_market_prices, EPİAŞ (Enerji Piyasaları
İşletme A.Ş.) Şeffaflık Platformu entegrasyonunun temel şema katmanını kurar
(yalnız şema — hiç veri yazmaz). Piyasa fiyatı (PTF/SMF), API Portal hesap
yönetimi ve çekim denetim günlüğü için 3 yeni tablo (1'i hypertable) ekler; hiçbir
mevcut tabloya dokunmaz (additive DDL). Kullanıcı-yönlü anlatım için
EPİAŞ Piyasa Fiyatları
sayfasına bakın. Operasyon notları: docs/runbook/0102_epias_transparency.md.
EPİAŞ (Enerji Piyasaları İşletme A.Ş.), Türkiye elektrik piyasasını işleten
kurumdur; Şeffaflık Platformu üzerinden PTF (Piyasa Takas Fiyatı) ve
SMF (Sistem Marjinal Fiyatı) gibi saatlik fiyat serilerini yayımlar. Bu fiyatlar
mahsuplaşma tutarının doğrudan ana girdisi DEĞİLDİR (mevzuat Md.4-l gereği
yalnızca bilgilendirici/advisory'dir); bu yüzden EPİAŞ şeması mahsuplaşma hesap
çekirdeğinden (0098) ayrık tutulur. market_prices.source='epias_transparency'
ayrı bir kaynak ailesidir ve mahsuplaşma fact'lerinin CALCULATION_SOURCES
kümesine dahil değildir.
Şema özeti
| Tablo | Tür | PK | Ana kolon aileleri |
|---|---|---|---|
epias_api_portal_accounts | plain | id UUID (gen_random_uuid()) | tenant_id (NULL = sistem-geneli), name, portal_username, portal_password_encrypted (Fernet TEXT), application_title, subscription_key_encrypted (Fernet TEXT), api_product (default seffaflik_platformu_1_0_0), config_version, whitelist_status (CHECK 9), whitelisted_ip (INET), last_auth_at, last_auth_status, is_active, created_at, updated_at |
epias_fetch_logs | plain | id BIGSERIAL | account_id (FK → accounts, NO ACTION, nullable), api_name, endpoint, request_hash, period_start/period_end, status (CHECK 4, NOT NULL), http_status, error_message, fetched_at |
market_prices | hypertable (chunk 90g) | (ts, market, price_status) | ts (TIMESTAMPTZ, partition), market (CHECK 6), price_try_per_mwh NUMERIC(16,6), price_try_per_kwh NUMERIC(16,8), price_status (CHECK 4, default final), source (default epias_transparency), fetched_at |
portal_password_encrypted ve subscription_key_encrypted kolonları hiçbir zaman
düz metin (plaintext) tutmaz. Şifreleme servis katmanında (Fernet) yapılır;
migration yalnızca TEXT kolonu açar (0052 email_encrypted emsali). Değerler API
yanıtına geri gösterilmez (write-only). Bu, Kepmark v1'in "MQTT credential
plaintext" tuzağının tekrarlanmaması için tasarım gereğidir.
epias_api_portal_accounts — 9 adımlı IP-beyaz-liste durum makinesi
EPİAŞ Şeffaflık Platformu'ndan canlı fiyat çekmek, bir dizi operasyonel adımın
(spec indir → EPİAŞ'a gönder → IP whitelist → portal hesabı → başvuru → abonelik →
aktif) tamamlanmasını gerektirir. whitelist_status kolonu bu sürecin hangi
adımda olduğunu tutar. Yalnız active durumda gerçek çekim yapılır; blocked
her durumdan girilebilir (EPİAŞ reddi / askıya alma).
DB CHECK'i (ck_epias_api_portal_accounts_whitelist_status) 9 değeri whitelist
eder: not_started, spec_downloaded, spec_sent, ip_whitelisted,
portal_account_created, application_created, subscribed, active, blocked.
Geçiş sırası (state-machine) servis katmanında (api_portal.py) zorlanır; DB
CHECK yalnız geçerli değer kümesini garanti eder.
tenant_id NULL = sistem-geneli hesapEPİAŞ tek platform hesabı tenant-bağımsız olabilir (tenant_id=NULL). FK ondelete
RESTRICT'tir: bir tenant'a bağlı hesap varsa o tenant hard-delete edilemez
(credential/denetim sessizce kaybolmasın). tenant_id üzerinde partial index
(WHERE tenant_id IS NOT NULL) — NULL-ağırlıklı katalogda tenant-scoped lookup için
yeterli (0090 FIX-1 deseni).
epias_fetch_logs — salt-append denetim günlüğü (BIGSERIAL)
Her API çekimi (başarılı / hatalı / atlanmış) bir satır üretir; forensic
izlenebilirlik için SİLİNMEZ (retention yok). PK BIGSERIAL'dir (satır
sırası = insert sırası; bedelli_limit_movements / mahsuplasma_eligibility_results
salt-append ledger emsali). account_id → epias_api_portal_accounts FK'sı
NO ACTION'dır (ondelete verilmez): bir hesabın log satırı varsa hesap
silinemez (denetim izi kopmaz). account_id nullable'dır — hesapsız /
sistem-tetikli çekimler de loglanabilir. status CHECK'i
(ck_epias_fetch_logs_status) 4 değeri whitelist eder:
status | TR etiket | Ne zaman |
|---|---|---|
success | Başarılı | Çekim tamamlandı, veri yazıldı |
error | Hatalı | HTTP/auth/parse hatası (error_message + http_status dolu) |
skipped | Atlandı | Ön koşul yok (ör. EPIAS_ENABLED=false dark-launch) |
partial | Kısmî | Bazı saatler geldi, bazıları eksik |
(account_id, fetched_at DESC) index'i "bir hesabın son çekimleri" sorgusunu
karşılar.
market_prices — EPİAŞ fiyat hypertable'ı (0102)
market_prices, mahsuplaşma domeninin ikinci ailesindeki (EPİAŞ) tek
hypertable'ıdır. Bir saat bucket'ındaki bir piyasanın (PTF/SMF/…) fiyatını
fatura-kalitesinde (NUMERIC) tutar. EPİAŞ client servisi (PR-8a) yazar; tenant
GET /api/energy-market/prices (read:netting) okur. TimescaleDB hypertable
kavramı için TimescaleDB sayfasına bakın.
Şema özeti
| Kolon | Tip | Not |
|---|---|---|
ts | TIMESTAMPTZ | Partition kolonu + PK[1]. Saat başı bucket (offset-aware) |
market | VARCHAR(32) | PK[2]. CHECK 6 değer (aşağıda) |
price_try_per_mwh | NUMERIC(16,6)? | Kaynak birim (EPİAŞ TL/MWh yayımlar) |
price_try_per_kwh | NUMERIC(16,8)? | = mwh / 1000 (Decimal aritmetiği; servis hesaplar) |
price_status | VARCHAR(24) | PK[3]. CHECK 4 değer; server_default 'final' |
source | VARCHAR(32)? | server_default 'epias_transparency' (ayrı kaynak ailesi) |
fetched_at | TIMESTAMPTZ? | Çekim damgası (server_default NOW()) |
Birincil anahtar — revizyonu koruyan idempotent upsert
PK (ts, market, price_status). TimescaleDB kuralı gereği partition kolonu
(ts) PK'nin ilk parçasıdır. Aynı saat/piyasa için farklı price_status
(tentative → final → corrected) ayrı satır üretir — böylece K.PTF /
kesin / revize fiyat geçmişi kaybolmadan izlenir (revizyon izlenebilirliği).
Client ON CONFLICT (ts, market, price_status) DO UPDATE ile idempotent yazar.
market CHECK — 6 piyasa kodu
ck_market_prices_market şu 6 değeri whitelist eder:
market | TR etiket | Durum |
|---|---|---|
ptf | Piyasa Takas Fiyatı | Aktif (Seçenek B client PTF üretir) |
smf | Sistem Marjinal Fiyatı | Aktif (SMF, ~4 saat gecikmeli) |
aof | Ağırlıklı Ortalama Fiyat | İleride (Faz 2+) |
grf | Gaz eşdeğeri fiyat kodu | İleride (gaz eşdeğeri, lisanslı segment) |
gaf | Gaz eşdeğeri fiyat kodu | İleride (gaz eşdeğeri) |
gsf | Gaz eşdeğeri fiyat kodu | İleride (gaz eşdeğeri) |
price_status CHECK — 4 durum
ck_market_prices_price_status şu 4 değeri whitelist eder:
price_status | TR etiket | Anlam |
|---|---|---|
tentative | Kesinleşmemiş | K.PTF / geçici (interim) fiyat |
final | Kesin | Nihai yayımlanmış fiyat (default) |
corrected | Revize | Sonradan düzeltilmiş fiyat |
unknown | Bilinmiyor | Kaynak durum bildirmedi |
Compression 180 gün + RETENTION YOK
- Compression: 180 gün sonra sıkıştır (
segmentby = market;orderby = ts DESC). Sıra kritik (0098 dersi): önceALTER … SET (timescaledb.compress, …)ENABLE, sonraadd_compression_policy— ters sıra hata verir.segmentby=marketdüşük kardinalitelidir (~6 değer → segment parçalanmaz). Chunk aralığı 90 gün (fiyat serisi 0097/0098'in 30 gününden daha seyrek yazılır → daha geniş chunk verimli). - Neden 180 gün? Fiyat revizyonu (
tentative→final→corrected) tipik olarak fatura dönemi içinde tamamlanır; 180 gün sonrası satır "donmuş" kabul edilir (sıkıştırılmış chunk'ta UPDATE maliyetlidir). - RETENTION YOK (kasıtlı):
add_retention_policyEKLENMEZ —market_pricesfiyat serisi mutabakat/itiraz kaynağıdır, silinmemelidir (0097/0098 ile aynı ilke). Migration0102bu yokluğu yorumla işaretler ki kimse kazara retention (DROP) eklemesin.epias_fetch_logsdenetim günlüğü de aynı gerekçeyle silinmez.
TimescaleDB extension yoksa market_prices düz tablo olarak doğar (CHECK + PK
korunur; yalnız hypertable/compression atlanır). Böylece hypertable'sız ortamlarda da
migration çalışır. Prod + CI'da aynı timescaledb pg16 imajı kullanılır.
(market, ts DESC) ek index
PK ts-lider olduğundan "bir piyasanın en yeni fiyatı"
(WHERE market = ? ORDER BY ts DESC) sorgusunu kapsamaz; bu yüzden
ix_market_prices_market_ts (market, ts DESC) eklenir.
FK ondelete — market_prices bağımsız, hiçbir FK vermez/almaz
market_prices hiçbir FK vermez ve almaz (bağımsız zaman serisi; TimescaleDB
kuralı gereği hypertable'a FK verilemez zaten). epias_fetch_logs.account_id → epias_api_portal_accounts tek FK'dır ve NO ACTION'dır (hesap silme, log varsa
bloklanır). epias_api_portal_accounts.tenant_id → tenants RESTRICT'tir.
epias_fetch_logs.account_id FK'sı NO ACTION olduğu için, çekim logu oluşmuş bir
API Portal hesabı hard-delete edilemez (DB IntegrityError fırlatır). Doğru
akış: hesabı is_active=false yapmak. Bu, mali/operasyonel denetim izinin (kim,
ne zaman, hangi endpoint'ten çekti) korunması içindir.
İlgili sistem alarmları (4 EPİAŞ alarmı)
EPİAŞ çekim/whitelist sorunları mahsuplaşma sistem alarmlarına yansır (33 tipin 4'ü
EPİAŞ): epias_api_portal_not_whitelisted, epias_api_subscription_missing,
epias_api_fetch_failed, epias_api_auth_failed. Ayrıntı için
Sistem Alarmları sayfasına bakın.
btree_gist önkoşulu
Migration 0092, repo'da ilk kez btree_gist extension'ını kullanır. Bu,
EXCLUDE ... USING gist constraint'inde skaler tiplerin (uuid/text) =
operatörüyle çalışabilmesi için zorunludur. Migration:
- extension'ı guard + fail-loud ile kurar (sessiz skip yasak),
- kurulamazsa (yetki yoksa) net çözüm-yolu ile
RuntimeErrorfırlatır (CREATE EXTENSION IF NOT EXISTS btree_gist— DevOps tek-seferlik + re-run), - downgrade'de DROP EDİLMEZ (paylaşımlı obje; hem 0092 hem 0094 EXCLUDE'u kullanır).
EXCLUDE constraint'ler
Zaman-aralığı tekilliği klasik UNIQUE ile ifade edilemez; PostgreSQL'in doğru
aracı EXCLUDE'dur.
ex_sma_no_overlap_main_meter (0092)
Kural: Aynı Subregion'da aynı ANA ölçüm rolü, aynı dönemde yalnız bir cihaza atanabilir.
EXCLUDE USING gist (
subregion_id WITH =,
meter_role WITH =,
tstzrange(valid_from, COALESCE(valid_to, 'infinity'::timestamptz), '[)') WITH &&
) WHERE (meter_role IN ('main_production','main_consumption',
'pcc_import_export','osb_eb_consumption'))
TIMESTAMPTZdaterange (tstzrange) — ölçüm ataması zaman-noktası hassasiyeti gerektirir.- PARTIAL WHERE: yalnız 4 ANA rol tekilleştirilir; alt/yardımcı roller bilinçli olarak çakışabilir.
'[)'half-open → komşu dönemler (önceki.valid_to == sonraki.valid_from) çakışmaz.- Ne zaman 409: çakışan bir dönem eklenmeye çalışıldığında.
ex_mgm_facility_single_group_per_period (0094)
Kural: Bir tesis aynı dönemde yalnız bir mahsuplaşma grubunda yer alır.
EXCLUDE USING gist (
facility_id WITH =,
daterange(valid_from, COALESCE(valid_to, 'infinity'::date), '[)') WITH &&
)
DATEdaterange (daterange) — üyelik fatura dönemidir (gün çözünürlüğü).- PARTIAL WHERE YOK — tüm üyeler tekilleştirilir (rol/ilişki ayrımı gözetilmez; regülasyon gereği bir tesis birden fazla gruba aynı anda giremez).
- Ne zaman 409: aynı tesis çakışan bir dönemde ikinci gruba eklenince.
UNIQUE constraint'ler (NULLS NOT DISTINCT dahil)
| Constraint | Tablo | Not |
|---|---|---|
UNIQUE(tenant_id, name) | scopes, groups | Tenant içi ad tekil |
UNIQUE NULLS NOT DISTINCT (tenant_id, org_type, name) | organizations | tenant_id=NULL (sistem-geneli) satırlar da tekil |
uq_bedelli_limit_accounts_facility_year | limit accounts | Tesis × yıl tekil hesap |
uq_tariff_prices_natural_key (NULLS NOT DISTINCT) | tariff_prices | (price_set_id, abone_grubu, price_purpose, time_segment, step_no); NULL boyutlar (tek-zamanlı/kademesiz) da tekil |
Klasik UNIQUE'de NULL'lar birbirinden farklı sayılır → (set, grup, amaç, NULL, NULL) satırı sınırsız çoğaltılabilirdi (CSV re-import duplicate kirlenmesi).
NULLS NOT DISTINCT (PG15+), NULL'lu satırlarda da tekilliği zorlar. Bu, prod +
CI'da aynı timescaledb pg15 imajı ile garanti edilir; PG<15 ortamda migration
fail-loud olur (sessiz fallback yok).
CHECK whitelist'leri
DB CHECK'leri (whitelist string'leri literal — kod sabitleri import edilmez, migration'lar yeniden-adlandırmadan etkilenmez):
official_facilities: role / regime / topology / ESS SOC (min<max) / ESS tutarlılık (has_ess=falseikeness_*NULL).metering_points.connection_level:transmission|distribution|osb_eb|unknown.subregion_meter_assignments:meter_role(9 değer) /energy_semantics(interval_delta|cumulative_counter|power_integrated|unknown) /scale_factor > 0.mahsuplasma_groups:group_status(6) /eligibility_status(5).mahsuplasma_group_members:member_role(3) /relationship_type(6).mahsuplasma_eligibility_results:severity(4) /status(4).bedelli_limit_accounts.limit_source(5);bedelli_limit_movements:movement_type(9) /ratio_basis(nullable-toleranslı). 9movement_typedeğeri (kod-doğrulanmış —netting_deductiondiye bir değer YOKTUR):initial,mahsuplasan_consumption,bedelli_ihtiyac_fazlasi,manual_adjustment,lum_adjustment,group_transfer_in,group_transfer_out,correction_reversal,correction_reapply.tariff_price_sets.approval_status(3);tariff_prices:price_purpose(7) /time_segment(4, nullable-toleranslı).- EPİAŞ (0102):
epias_api_portal_accounts.whitelist_status(9),epias_fetch_logs.status(4),market_prices.market(6) /price_status(4). Hypertable ailesinin (facility_hourly_energy, 0098 hesap fact'leri) "CHECK YOK" kararından bilinçli sapma: EPİAŞ enum'ları kapalı-domain (operasyonel durum makinesi + sabit piyasa kodları) olduğundan DB CHECK ile korunur.market_pricesbir hypertable olsa da CHECK'ler hypertable dönüşümünden önce boş tabloda anlık eklenir.
Bazı alanlar bilinçli olarak DB CHECK'siz (Pydantic-only — EPDK/mevzuat
genişletebilir): abone_grubu, facility_type_code, rule_code,
responsible_supplier_basis, metering_point_type,
producer_tariff_group (PR-C2 — source_type/abone_grubu kalıbı; EPDK
üretici grubu genişletirse DB migration'ı gerekmez) vb.
FK ondelete matrisi
| FK | ondelete | Gerekçe |
|---|---|---|
metering_points.osos_installation_id | SET NULL | OSOS köprüsü opsiyonel; köprü silinince nokta korunur |
subregion_meter_assignments.* | NO ACTION | Billing-relevant; cihaz silmeyle sessizce silinemez (önce valid_to kapat) |
mahsuplasma_group_members.group_id | CASCADE | Üyeler gruba aittir; grup silinince üyelik anlamsızlaşır |
mahsuplasma_eligibility_results.group_id/facility_id | SET NULL | Salt-append denetim logu; grup silinse bile log korunur (izlenebilirlik) |
bedelli_limit_movements.account_id | NO ACTION | Ledger denetim izi; hesap silme hareket varsa FK ile bloklanır |
bedelli_limit_movements.group_id | SET NULL | Hareket (denetim izi) korunur; grup silinince yalnız group_id NULL'lanır |
tariff_prices.price_set_id | CASCADE | Draft paket temizliği (approved silme servis guard'ıyla 409) |
tariff_price_sets.created_by/approved_by | SET NULL | Kullanıcı silinse bile onay izi korunur |
facility_hourly_energy.tenant_id/facility_id | RESTRICT | Fact varsa tenant/tesis hard-delete BLOKLANIR (fact sessizce kaybolamaz) |
facility_hourly_energy.subregion_id/metering_point_id/meter_assignment_id | SET NULL | Denormalize referans silinse fact KORUNUR |
epias_api_portal_accounts.tenant_id (0102) | RESTRICT | Hesap (credential) varsa tenant hard-delete BLOKLANIR |
epias_fetch_logs.account_id (0102) | NO ACTION | Denetim günlüğü; hesap silme log varsa FK ile bloklanır (önce is_active=false) |
market_prices (0102) | FK YOK | Bağımsız zaman serisi; hypertable'a FK verilemez, kimseye FK vermez |
| Diğer regülasyon/kullanıcı FK'ları | NO ACTION (default) | Hard-delete FK ile bloklanmalı (fail-safe felsefesi) |
Tesis hard-delete guard'ı (FK matrisi → 409)
DELETE /api/mahsuplasma/facilities/{id} (superadmin-only; kullanıcı-yönlü anlatım
için Kurulum Akışı — Tesis silme)
bir tesisi kalıcı siler. Silmeden önce servis (services/facilities.py)
tesise facility_id ile bağlı 5 tabloyu SELECT COUNT ile sayar (sabit sıra
→ deterministik yanıt); herhangi biri > 0 ise silme 409 facility_has_dependencies
ile reddedilir ve gövde tam bağımlılık tablosunu (dependencies) döner.
| Sayılan tablo | FK ondelete | Neden servis-içi sayılır? |
|---|---|---|
metering_points.facility_id | NO ACTION (default) | Cihaz/nokta sessizce silinemez; önce nokta kaldırılmalı |
subregion_meter_assignments.facility_id | NO ACTION | Billing-relevant atama; önce valid_to kapat/kaldır |
facility_hourly_energy.facility_id | RESTRICT | DB son savunma hattı — fact varsa hard-delete DB'de de bloklanır |
mahsuplasma_group_members.facility_id | NO ACTION (default) | group_id CASCADE'dir ama facility_id DEĞİL — üye önce kaldırılmalı |
bedelli_limit_accounts.facility_id | NO ACTION (default) | Ledger hesabı; hesap silinmeden tesis silinemez |
Neden hem servis sayımı hem FK guard? Servis-içi sayım kullanıcıya 500
(ham IntegrityError) yerine anlamlı 409 + bağımlılık sayıları verir. FK
ondelete (özellikle facility_hourly_energy RESTRICT) ise say→sil arasındaki
yarışta (eş-zamanlı alt kayıt eklenmesi) son savunma hattıdır: IntegrityError
yakalanır → rollback → güncel sayımla aynı 409'a çevrilir. İkisi birlikte
"yetim veri / sessiz kayıp yok" garantisini verir.
valid_to semantiği CorrectionEngine'e (PR-6) aittirBu işlem satırı tamamen kaldırır (hard-delete); official_facilities.valid_to
/ archived_at ile dönem kapatma yapılmaz. Tesisin tarihçesini koruyarak
"kapatma" (dönem sonlandırma / geriye-dönük düzeltme) semantiği ayrı bir sürümde
(PR-6 CorrectionEngine) ele alınmaktadır — mevzuat (DUY düzeltme ufku) ile
tutarlı düzeltme akışı orada tanımlanır. Bu yüzden hard-delete yalnız
tamamen bağımsız (yetim) tesisler için pratiktir; kullanımda olan bir tesis
için doğru yol bağımlılıkları kaldırmak veya (ileride) dönem kapatmaktır.
bedelli_limit_balances — neden matview DEĞİL?
Kalan bedelli limit, gerçek-zamanlı SKB (Md.7(5)) kararının girdisidir. Bayat bir kalan-limit değeri yanlış karar doğururdu (izlenebilirlik kırmızı çizgisi). Bu yüzden PLAIN VIEW kullanılır:
remaining_limit_kwh = initial_limit_kwh + COALESCE(SUM(movements.amount_kwh), 0)
Sorgu anında hesaplanır (LEFT JOIN + COALESCE → hareketsiz hesapta
remaining = initial doğru). View ORM'e map edilmez (autogenerate view'i
tablo sanıp drift üretirdi); backend ham sa.text() sorgusuyla + Pydantic
BalanceRead ile okur. initial hareketin amount_kwh'si 0'dır (başlangıç
limiti zaten initial_limit_kwh'de; double-count önleme).
Salt-append (immutable) tablolar
Aşağıdaki tablolar UPDATE/DELETE edilmez (yeni satır = düzeltme):
mahsuplasma_eligibility_results— herevaluaterun'ı yeni satırlar;run_idyerine transaction-sabitevaluated_at(aynı run birebir eşit damga).bedelli_limit_movements— düzeltmecorrection_reversal/correction_reapplyveyamanual_adjustmentyeni satırlarıyla (Md.15).updated_atkolonu yok.tariff_prices— immutable-append; düzeltme yenitariff_price_setsversiyonu (eskisiarchived).epias_fetch_logs(0102) — her API çekimi yeni satır (forensic denetim günlüğü); silinmez, güncellenmez.
İlgili sayfalar
- Mahsuplaşma Modül Mimarisi
- Kullanım Kılavuzu — Limit Hesapları
- Kullanım Kılavuzu — Sayaç Atama ve Kanal Eşleme
- API Referansı — Mahsuplaşma
- Kullanım Kılavuzu — Hesap Çekirdeği
- Kullanım Kılavuzu — EPİAŞ Piyasa Fiyatları
- Kullanım Kılavuzu — Sistem Alarmları
- TimescaleDB Hypertable'ları
- Operasyonel runbook'lar:
docs/runbook/0092_*,0094_*,0095_0096_*,0097_*,0098_*,0102_epias_transparency.md.