Ana içeriğe geç

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.

Tüm PK'lar UUID (ledger/log hariç BIGSERIAL)

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ı

MigrationObjelerRunbook
0090 mahsuplasma_regulatory_foundationmahsuplasma_scopes, official_organizations, official_facilities
0091 backfill_netting_permissionsRBAC izin backfill (dormant): read/manage:netting, import:settlement_reports
0092 mahsuplasma_metering_inverterbtree_gist, metering_points, subregion_meter_assignments, devices.inverter_type0092_mahsuplasma_metering_btree_gist.md
0094 mahsuplasma_groups_eligibilitymahsuplasma_groups, mahsuplasma_group_members, mahsuplasma_eligibility_results0094_mahsuplasma_groups_eligibility.md
0095 bedelli_limit_ledgerbedelli_limit_accounts, bedelli_limit_movements, bedelli_limit_balances (VIEW)0095_0096_limit_ledger_regulated_tariff.md
0096 regulated_tariff_pricestariff_price_sets, tariff_prices (ULUSAL — tenant_id yok)0095_0096_limit_ledger_regulated_tariff.md
0097 add_facility_hourly_energyfacility_hourly_energy (domenin İLK hypertable'ı; compression 500g, RETENTION YOK)0097_facility_hourly_energy.md
0098 mahsuplasma_calculation_coreHesap ç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_pricesEPİ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_groupofficial_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: 0093 mahsuplaş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'yi facility_hourly_energy aldı — off-by-one). 00980102 aradaki üç migration (0099 widget CHECK genişletme, 0100 alarms.device_id nullable, 0101 alarm politika seed'i) yeni tablo eklemez. Benzer şekilde 0109 (price_purpose 7→9), 0110 (tariff_prices doğal anahtar genişletme) ve 0111 (official_facilities.producer_tariff_group ALTER) yeni tablo eklemez — 0111 mevcut tesis tablosuna additive nullable kolon ekler. Runbook'lar docs/runbook/ altındadır.


Tablolar ve amaçları

TabloAmaçKritik constraint'ler
mahsuplasma_scopesVKN'siz resmî kapsam (Zeus alias)UNIQUE(tenant_id, name); CHECK declaration_source
official_organizationsKurum kataloğu (şebeke/dağıtım/GTŞ/OSB-EB/sayaç okuyan/tedarikçi). tenant_id=NULL = sistem-geneliUNIQUE NULLS NOT DISTINCT (tenant_id, org_type, name)
official_facilitiesResmî ü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_assignmentsCihaz → ölçüm rolü (tarihçeli)EXCLUDE ex_sma_no_overlap_main_meter; CHECK meter_role/energy_semantics/scale_factor
mahsuplasma_groupsMüşteri tanımlı grupUNIQUE(tenant_id, name); CHECK group_status/eligibility_status
mahsuplasma_group_membersGrup üyesi tesis (tarihçeli)EXCLUDE ex_mgm_facility_single_group_per_period; group_id → CASCADE
mahsuplasma_eligibility_resultsUygunluk log'u (salt-append, BIGSERIAL)CHECK severity/status; group_id/facility_id → SET NULL
bedelli_limit_accountsTesis × yıl bedelli limit hesabıUNIQUE(facility_id, year); CHECK limit_source
bedelli_limit_movementsSalt-append hareket defteri (BIGSERIAL)CHECK movement_type/ratio_basis; account_id → NO ACTION, group_id → SET NULL
bedelli_limit_balancesKalan limit PLAIN VIEW (canlı)— (view; ORM'e map edilmez)
tariff_price_setsRegüle tarife paketi (ULUSAL, tenant_id yok)CHECK approval_status
tariff_pricesTarife fiyat satırı (immutable-append)UNIQUE NULLS NOT DISTINCT natural key; price_set_id → CASCADE
facility_hourly_energySaatlik 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_runsHesap 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_calcSaatlik 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_hourlySanal sayaç saatlik satırları — Md.10 (hypertable)PK (ts, id); expression-uq uq_vm_hourly_dims (zero-uuid COALESCE); CHECK YOK
mahsuplasma_settlement_amount_hourlySaatlik tutar satırları — Md.11-12 (hypertable)PK (ts, id); expression-uq uq_settlement_amount_dims; CHECK YOK
mahsuplasma_monthly_calcAylı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_stateKademeli tarife aylık tesis durumu (plain)PK (period_start, facility_id, source)source PK üyesi (NOT NULL)
epias_api_portal_accountsEPİAŞ API Portal hesap kataloğu (Fernet şifreli credential; superadmin)CHECK whitelist_status (9); tenant_id → RESTRICT (NULL = sistem-geneli)
epias_fetch_logsEPİAŞ API çekim denetim günlüğü (salt-append, BIGSERIAL)CHECK status (4); account_id → NO ACTION (denetim izi kopmaz)
market_pricesPiyasa 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

KolonTipNot
hourTIMESTAMPTZPartition kolonu + PK[1]. Saat başı bucket
tenant_idUUIDFK → tenants (RESTRICT)
facility_idUUIDFK → official_facilities (RESTRICT); PK[2]
subregion_idUUID?FK → subregions (SET NULL)
metering_point_idUUID?FK → metering_points (SET NULL)
meter_assignment_idUUID?FK → subregion_meter_assignments (SET NULL)
roleVARCHAR(32)PK[3]. production / consumption / pcc / osb_eb_consumption
production_kwh, consumption_kwh, internal_consumption_kwh, import_kwh, export_kwhNUMERIC(16,6)Enerji; server_default '0'
battery_charge_kwh, battery_discharge_kwhNUMERIC(16,6)?DEFAULT'suzNULL = "ölçülmedi" (batarya yok) ayrımı
avg_battery_soc_pctNUMERIC(8,3)?Ortalama SoC
data_qualityVARCHAR(32)?complete/partial/missing/corrected/suspect/estimated_profiled/unknown
completeness_pctNUMERIC(5,2)?Ölçüm tamlığı
sourceVARCHAR(32)PK[4]. 8 kanonik kaynak (default 'zeus_meter')
calculated_atTIMESTAMPTZ?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: önce ALTER ... SET (timescaledb.compress, …) ENABLE, sonra add_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_policy EKLENMEZ — mahsuplaşma fact verisi mutabakat/itiraz kaynağıdır, silinmemelidir. Migration 0097 bu 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

TabloTürPKAna kolon aileleri
mahsuplasma_calculation_runsplainid UUIDbilling_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_calchypertable (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_hourlyhypertable (chunk 30g)(ts, id)id BIGINT Identityvirtual_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_hourlyhypertable (chunk 30g)(ts, id)id BIGINT Identityamount_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_calcplain(period_start, group_id, calculation_run_id)Aylık toplamlar (total_monthly_*, monthly_matched/surplus, bedelli_production, bedelsiz), source, is_current
tariff_step_monthly_stateplain(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_id girmez — yüksek kardinalite segment'i parçalar; tenant_id gereksiz — group_id zaten tenant-scoped; is_current zararlı — flip UPDATE'i segment'i bozar); orderby = ts DESC; compress_after = 500 gün — DUY 133/5 düzeltme ufku ~490 gün: is_current flip'leri hep sıkışmamış chunk'ta kalır.
  • mahsuplasma_virtual_meter_hourly + mahsuplasma_settlement_amount_hourly Faz 1'de bilinçli compression'sız: correction cascade'in run bazlı is_current UPDATE 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_policy yazı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):

IndexTabloKural
uq_current_hourly_group_calchourly_group_calc(ts, group_id, source) WHERE is_current — saat başına tek current satır
uq_current_monthly_calcmonthly_calc(period_start, group_id, source) WHERE is_current — çifte-current mesken tutarı çift sayımını önler (plan'a EK)
uq_vm_hourly_dimsvirtual_meter_hourlyexpression-unique: (ts, group_id, run, tip, COALESCE(grid_operator_id, zero-uuid), COALESCE(source_type, ''))
uq_settlement_amount_dimssettlement_amount_hourlyexpression-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).

Davranış değişikliği — calc fact'i olan grup silinemez

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 VALID gereksiz; + partial index ix_blm_calculation_run_id. PR-6 ile movement_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_basis VARCHAR(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Ş nedir, mahsuplaşmadaki rolü ne?

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

TabloTürPKAna kolon aileleri
epias_api_portal_accountsplainid 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_logsplainid BIGSERIALaccount_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_priceshypertable (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
Credential'lar Fernet ile şifreli TEXT — plaintext ASLA

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 hesap

EPİ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:

statusTR etiketNe zaman
successBaşarılıÇekim tamamlandı, veri yazıldı
errorHatalıHTTP/auth/parse hatası (error_message + http_status dolu)
skippedAtlandıÖn koşul yok (ör. EPIAS_ENABLED=false dark-launch)
partialKı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

KolonTipNot
tsTIMESTAMPTZPartition kolonu + PK[1]. Saat başı bucket (offset-aware)
marketVARCHAR(32)PK[2]. CHECK 6 değer (aşağıda)
price_try_per_mwhNUMERIC(16,6)?Kaynak birim (EPİAŞ TL/MWh yayımlar)
price_try_per_kwhNUMERIC(16,8)?= mwh / 1000 (Decimal aritmetiği; servis hesaplar)
price_statusVARCHAR(24)PK[3]. CHECK 4 değer; server_default 'final'
sourceVARCHAR(32)?server_default 'epias_transparency' (ayrı kaynak ailesi)
fetched_atTIMESTAMPTZ?Ç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 (tentativefinalcorrected) 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:

marketTR etiketDurum
ptfPiyasa Takas FiyatıAktif (Seçenek B client PTF üretir)
smfSistem Marjinal FiyatıAktif (SMF, ~4 saat gecikmeli)
aofAğırlıklı Ortalama Fiyatİleride (Faz 2+)
grfGaz eşdeğeri fiyat koduİleride (gaz eşdeğeri, lisanslı segment)
gafGaz eşdeğeri fiyat koduİleride (gaz eşdeğeri)
gsfGaz 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_statusTR etiketAnlam
tentativeKesinleşmemişK.PTF / geçici (interim) fiyat
finalKesinNihai yayımlanmış fiyat (default)
correctedRevizeSonradan düzeltilmiş fiyat
unknownBilinmiyorKaynak 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): önce ALTER … SET (timescaledb.compress, …) ENABLE, sonra add_compression_policy — ters sıra hata verir. segmentby=market düşü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 (tentativefinalcorrected) 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_policy EKLENMEZmarket_prices fiyat serisi mutabakat/itiraz kaynağıdır, silinmemelidir (0097/0098 ile aynı ilke). Migration 0102 bu yokluğu yorumla işaretler ki kimse kazara retention (DROP) eklemesin. epias_fetch_logs denetim günlüğü de aynı gerekçeyle silinmez.
TimescaleDB yoksa düz tablo — migration yine geçerli

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.

EPİAŞ hesabı çekim log'u varsa silinemez

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 RuntimeError fı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'))
  • TIMESTAMPTZ daterange (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 &&
)
  • DATE daterange (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)

ConstraintTabloNot
UNIQUE(tenant_id, name)scopes, groupsTenant içi ad tekil
UNIQUE NULLS NOT DISTINCT (tenant_id, org_type, name)organizationstenant_id=NULL (sistem-geneli) satırlar da tekil
uq_bedelli_limit_accounts_facility_yearlimit accountsTesis × 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
NULLS NOT DISTINCT neden gerekli?

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=false iken ess_* 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ı). 9 movement_type değeri (kod-doğrulanmış — netting_deduction diye 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_prices bir 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

FKondeleteGerekçe
metering_points.osos_installation_idSET NULLOSOS köprüsü opsiyonel; köprü silinince nokta korunur
subregion_meter_assignments.*NO ACTIONBilling-relevant; cihaz silmeyle sessizce silinemez (önce valid_to kapat)
mahsuplasma_group_members.group_idCASCADEÜyeler gruba aittir; grup silinince üyelik anlamsızlaşır
mahsuplasma_eligibility_results.group_id/facility_idSET NULLSalt-append denetim logu; grup silinse bile log korunur (izlenebilirlik)
bedelli_limit_movements.account_idNO ACTIONLedger denetim izi; hesap silme hareket varsa FK ile bloklanır
bedelli_limit_movements.group_idSET NULLHareket (denetim izi) korunur; grup silinince yalnız group_id NULL'lanır
tariff_prices.price_set_idCASCADEDraft paket temizliği (approved silme servis guard'ıyla 409)
tariff_price_sets.created_by/approved_bySET NULLKullanıcı silinse bile onay izi korunur
facility_hourly_energy.tenant_id/facility_idRESTRICTFact varsa tenant/tesis hard-delete BLOKLANIR (fact sessizce kaybolamaz)
facility_hourly_energy.subregion_id/metering_point_id/meter_assignment_idSET NULLDenormalize referans silinse fact KORUNUR
epias_api_portal_accounts.tenant_id (0102)RESTRICTHesap (credential) varsa tenant hard-delete BLOKLANIR
epias_fetch_logs.account_id (0102)NO ACTIONDenetim günlüğü; hesap silme log varsa FK ile bloklanır (önce is_active=false)
market_prices (0102)FK YOKBağı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 tabloFK ondeleteNeden servis-içi sayılır?
metering_points.facility_idNO ACTION (default)Cihaz/nokta sessizce silinemez; önce nokta kaldırılmalı
subregion_meter_assignments.facility_idNO ACTIONBilling-relevant atama; önce valid_to kapat/kaldır
facility_hourly_energy.facility_idRESTRICTDB son savunma hattı — fact varsa hard-delete DB'de de bloklanır
mahsuplasma_group_members.facility_idNO ACTION (default)group_id CASCADE'dir ama facility_id DEĞİL — üye önce kaldırılmalı
bedelli_limit_accounts.facility_idNO 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.

Arşivleme DEĞİL — valid_to semantiği CorrectionEngine'e (PR-6) aittir

Bu 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 — her evaluate run'ı yeni satırlar; run_id yerine transaction-sabit evaluated_at (aynı run birebir eşit damga).
  • bedelli_limit_movements — düzeltme correction_reversal/correction_reapply veya manual_adjustment yeni satırlarıyla (Md.15). updated_at kolonu yok.
  • tariff_prices — immutable-append; düzeltme yeni tariff_price_sets versiyonu (eskisi archived).
  • epias_fetch_logs (0102) — her API çekimi yeni satır (forensic denetim günlüğü); silinmez, güncellenmez.

İlgili sayfalar