codedna 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,259 @@
1
+ """GitHub PR'larına otomatik CodeDNA analiz yorumu bırakır."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import urllib.error
8
+ import urllib.request
9
+ from typing import Optional
10
+
11
+ # GitHub API temel URL'i
12
+ _GH_API = "https://api.github.com"
13
+
14
+ # Yorumda kullanılan gizli marker — mevcut CodeDNA yorumunu bulmak için
15
+ _YORUM_MARKERI = "<!-- codedna-bot-comment -->"
16
+
17
+ # Güvenlik: token asla loglanmaz veya hata mesajlarında görünmez
18
+ GITHUB_TOKEN_ENV = "GITHUB_TOKEN"
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Yorum formatlama
23
+ # ---------------------------------------------------------------------------
24
+
25
+ def format_pr_comment(
26
+ scan_sonuclari: list,
27
+ debt_ozeti: Optional[dict] = None,
28
+ ) -> str:
29
+ """
30
+ Analiz sonuçlarını GitHub Markdown yorumuna dönüştür.
31
+
32
+ Okunabilirlik için 2000 karakter altında tutulur.
33
+ ℹ️ teknik borç disclaimer'ı dahil.
34
+
35
+ Args:
36
+ scan_sonuclari: FileAnalysisResult listesi
37
+ debt_ozeti: RepoBorcu özeti (opsiyonel)
38
+
39
+ Returns:
40
+ Markdown formatında yorum metni
41
+ """
42
+ if not scan_sonuclari:
43
+ return f"{_YORUM_MARKERI}\n## 🧬 CodeDNA Analizi\n\nDeğişen dosya bulunamadı.\n"
44
+
45
+ toplam = len(scan_sonuclari)
46
+ ort_ai = sum(s.ai_probability for s in scan_sonuclari) / toplam
47
+ yuksek_risk = [s for s in scan_sonuclari if s.ai_probability >= 0.7]
48
+
49
+ # Risk seviyesi
50
+ if ort_ai >= 0.7:
51
+ risk_emoji = "🔴"
52
+ risk_label = "YÜKSEK"
53
+ elif ort_ai >= 0.4:
54
+ risk_emoji = "🟡"
55
+ risk_label = "ORTA"
56
+ else:
57
+ risk_emoji = "🟢"
58
+ risk_label = "DÜŞÜK"
59
+
60
+ satirlar = [
61
+ _YORUM_MARKERI,
62
+ "## 🧬 CodeDNA Analizi",
63
+ "",
64
+ f"| Metrik | Değer |",
65
+ f"|--------|-------|",
66
+ f"| Analiz edilen dosya | {toplam} |",
67
+ f"| Ort. AI olasılığı | %{ort_ai * 100:.0f} |",
68
+ f"| Risk seviyesi | {risk_emoji} {risk_label} |",
69
+ f"| Yüksek riskli dosya (≥%70) | {len(yuksek_risk)} |",
70
+ "",
71
+ ]
72
+
73
+ # En riskli 3 dosya
74
+ en_riskli = sorted(scan_sonuclari, key=lambda s: s.ai_probability, reverse=True)[:3]
75
+ if en_riskli:
76
+ satirlar.append("### ⚠️ Dikkat Gerektiren Dosyalar")
77
+ satirlar.append("")
78
+ satirlar.append("| Dosya | AI Olasılığı | Karmaşıklık |")
79
+ satirlar.append("|-------|-------------|------------|")
80
+ for s in en_riskli:
81
+ try:
82
+ from pathlib import Path
83
+ kisa_yol = "/".join(Path(s.file_path).parts[-2:])
84
+ except Exception:
85
+ kisa_yol = s.file_path[-40:]
86
+ satirlar.append(
87
+ f"| `{kisa_yol}` | %{s.ai_probability * 100:.0f} | {s.complexity_label} |"
88
+ )
89
+ satirlar.append("")
90
+
91
+ # Teknik borç (varsa)
92
+ if debt_ozeti:
93
+ toplam_saat = debt_ozeti.get("toplam_debt_saatleri", 0)
94
+ satirlar.append(f"### 💰 Teknik Borç Tahmini")
95
+ satirlar.append(f"")
96
+ satirlar.append(f"Tahmini borç: **{toplam_saat:.1f} saat**")
97
+ satirlar.append(f"")
98
+ satirlar.append(
99
+ "> ℹ️ *Bu bir tahmin modelidir — anlama skoru, AI olasılığı ve "
100
+ "karmaşıklık ağırlıklarına dayanır. Kesin muhasebe verisi değildir.*"
101
+ )
102
+ satirlar.append("")
103
+
104
+ # Footer
105
+ satirlar += [
106
+ "---",
107
+ "<sub>🧬 [CodeDNA](https://github.com/codedna/codedna) · "
108
+ "Detaylı analiz için `codedna dashboard` çalıştırın.</sub>",
109
+ ]
110
+
111
+ yorum = "\n".join(satirlar)
112
+
113
+ # 2000 karakter limitini aş
114
+ if len(yorum) > 2000:
115
+ ozet = "\n".join(satirlar[:20])
116
+ yorum = (
117
+ ozet + "\n\n"
118
+ "*... (kısaltıldı — tam rapor için CodeDNA dashboard'a bakın)*\n\n"
119
+ f"---\n<sub>🧬 CodeDNA</sub>\n{_YORUM_MARKERI}"
120
+ )
121
+
122
+ return yorum
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # GitHub API işlemleri
127
+ # ---------------------------------------------------------------------------
128
+
129
+ def _gh_istek(
130
+ method: str,
131
+ url: str,
132
+ token: str,
133
+ veri: Optional[dict] = None,
134
+ ) -> dict:
135
+ """
136
+ GitHub API'ye kimlik doğrulamalı istek gönder.
137
+
138
+ Güvenlik: token sadece Authorization header'ında kullanılır,
139
+ asla loglanmaz veya hata mesajına eklenmez.
140
+ """
141
+ govde = json.dumps(veri).encode("utf-8") if veri else None
142
+ istek = urllib.request.Request(
143
+ url,
144
+ data=govde,
145
+ method=method,
146
+ headers={
147
+ "Authorization": f"Bearer {token}",
148
+ "Accept": "application/vnd.github+json",
149
+ "X-GitHub-Api-Version": "2022-11-28",
150
+ "Content-Type": "application/json",
151
+ },
152
+ )
153
+ try:
154
+ with urllib.request.urlopen(istek, timeout=15) as yanit:
155
+ return json.loads(yanit.read())
156
+ except urllib.error.HTTPError as e:
157
+ govde_str = e.read().decode("utf-8", errors="replace")
158
+ # Token'ı hata mesajına ASLA ekleme
159
+ raise RuntimeError(f"GitHub API hatası ({e.code}): {govde_str[:300]}")
160
+
161
+
162
+ def find_existing_comment(
163
+ repo: str,
164
+ pr_number: int,
165
+ token: str,
166
+ ) -> Optional[int]:
167
+ """
168
+ Mevcut CodeDNA bot yorumunun ID'sini bul (marker ile).
169
+
170
+ Args:
171
+ repo: "owner/repo" formatında repo adı
172
+ pr_number: PR numarası
173
+ token: GitHub token
174
+
175
+ Returns:
176
+ Yorum ID'si veya yoksa None
177
+ """
178
+ url = f"{_GH_API}/repos/{repo}/issues/{pr_number}/comments?per_page=100"
179
+ try:
180
+ yorumlar = _gh_istek("GET", url, token)
181
+ except Exception:
182
+ return None
183
+
184
+ if not isinstance(yorumlar, list):
185
+ return None
186
+
187
+ for yorum in yorumlar:
188
+ body = yorum.get("body", "")
189
+ if _YORUM_MARKERI in body:
190
+ return yorum.get("id")
191
+
192
+ return None
193
+
194
+
195
+ def post_or_update_comment(
196
+ repo: str,
197
+ pr_number: int,
198
+ body: str,
199
+ token: str,
200
+ ) -> dict:
201
+ """
202
+ PR'a CodeDNA yorumu gönder veya mevcut yorumu güncelle.
203
+
204
+ Spam önleme: Her push'ta yeni yorum açılmaz — mevcut CodeDNA
205
+ yorumu bulunur ve PATCH ile güncellenir. Yoksa yeni POST yapılır.
206
+
207
+ Args:
208
+ repo: "owner/repo" formatında repo adı
209
+ pr_number: PR numarası
210
+ body: Yorum metni (Markdown)
211
+ token: GitHub token
212
+
213
+ Returns:
214
+ GitHub API yanıtı
215
+ """
216
+ mevcut_id = find_existing_comment(repo, pr_number, token)
217
+
218
+ if mevcut_id:
219
+ # Mevcut yorumu güncelle — spam yaratma
220
+ url = f"{_GH_API}/repos/{repo}/issues/comments/{mevcut_id}"
221
+ return _gh_istek("PATCH", url, token, {"body": body})
222
+ else:
223
+ # İlk defa yorum bırak
224
+ url = f"{_GH_API}/repos/{repo}/issues/{pr_number}/comments"
225
+ return _gh_istek("POST", url, token, {"body": body})
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # GitHub Actions ortamından PR bilgisi otomatik algıla
230
+ # ---------------------------------------------------------------------------
231
+
232
+ def github_actions_pr_bilgisi() -> Optional[tuple[str, int]]:
233
+ """
234
+ GitHub Actions ortamından repo adı ve PR numarasını otomatik algıla.
235
+
236
+ GITHUB_REPOSITORY ve GITHUB_EVENT_PATH ortam değişkenlerini kullanır.
237
+
238
+ Returns:
239
+ (repo, pr_number) çifti veya algılanamıyorsa None
240
+ """
241
+ repo = os.environ.get("GITHUB_REPOSITORY")
242
+ event_path = os.environ.get("GITHUB_EVENT_PATH")
243
+
244
+ if not repo or not event_path:
245
+ return None
246
+
247
+ try:
248
+ with open(event_path, encoding="utf-8") as f:
249
+ event_veri = json.load(f)
250
+ pr_number = (
251
+ event_veri.get("pull_request", {}).get("number")
252
+ or event_veri.get("number")
253
+ )
254
+ if pr_number:
255
+ return repo, int(pr_number)
256
+ except Exception:
257
+ pass
258
+
259
+ return None
@@ -0,0 +1,166 @@
1
+ """Jira sprint verilerini CodeDNA sprint kayıtlarıyla eşleştirir."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import secrets
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ # Webhook secret dosyası
14
+ _SECRET_DOSYASI = Path.home() / ".codedna" / "jira_secret.txt"
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Webhook Secret Yönetimi
19
+ # ---------------------------------------------------------------------------
20
+
21
+ def get_or_create_secret() -> str:
22
+ """
23
+ Jira webhook secret'ını oku veya yoksa yeni oluştur.
24
+
25
+ Returns:
26
+ Hex formatında 32-byte secret
27
+ """
28
+ if _SECRET_DOSYASI.exists():
29
+ return _SECRET_DOSYASI.read_text().strip()
30
+ secret = secrets.token_hex(32)
31
+ _SECRET_DOSYASI.parent.mkdir(parents=True, exist_ok=True)
32
+ _SECRET_DOSYASI.write_text(secret)
33
+ return secret
34
+
35
+
36
+ def rotate_secret() -> str:
37
+ """Webhook secret'ı yenile ve yeni değeri döndür."""
38
+ yeni = secrets.token_hex(32)
39
+ _SECRET_DOSYASI.parent.mkdir(parents=True, exist_ok=True)
40
+ _SECRET_DOSYASI.write_text(yeni)
41
+ return yeni
42
+
43
+
44
+ def verify_signature(payload_bytes: bytes, signature_header: str, secret: str) -> bool:
45
+ """
46
+ Jira webhook imzasını HMAC-SHA256 ile doğrula.
47
+
48
+ Jira imzası formatı: "sha256=<hex_digest>"
49
+
50
+ Args:
51
+ payload_bytes: Ham HTTP body
52
+ signature_header: X-Hub-Signature-256 başlık değeri
53
+ secret: Webhook secret
54
+
55
+ Returns:
56
+ İmza geçerliyse True
57
+ """
58
+ if not signature_header.startswith("sha256="):
59
+ return False
60
+ beklenen_imza = signature_header[7:]
61
+ hesaplanan = hmac.new(
62
+ secret.encode("utf-8"),
63
+ payload_bytes,
64
+ hashlib.sha256,
65
+ ).hexdigest()
66
+ return hmac.compare_digest(hesaplanan, beklenen_imza)
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Webhook İşleme
71
+ # ---------------------------------------------------------------------------
72
+
73
+ def handle_jira_webhook(payload: dict[str, Any], db_path: Path) -> dict[str, Any]:
74
+ """
75
+ Jira webhook payload'unu işle, sprint kaydı oluştur/güncelle.
76
+
77
+ Desteklenen event türleri:
78
+ - sprint_started: Yeni sprint başladı → kayıt oluştur
79
+ - sprint_closed: Sprint kapandı → sağlık skoru hesapla + güncelle
80
+
81
+ Args:
82
+ payload: Jira webhook JSON payload'u
83
+ db_path: SQLite veritabanı yolu
84
+
85
+ Returns:
86
+ İşlem sonucu sözlüğü
87
+ """
88
+ event_turu = payload.get("webhookEvent", "")
89
+ sprint_verisi = payload.get("sprint", {})
90
+
91
+ if not sprint_verisi:
92
+ return {"durum": "atlandı", "neden": "sprint verisi yok"}
93
+
94
+ sprint_adi = sprint_verisi.get("name", "Jira Sprint")
95
+ baslangic_str = sprint_verisi.get("startDate") or sprint_verisi.get("activatedDate")
96
+ bitis_str = sprint_verisi.get("endDate") or sprint_verisi.get("completeDate")
97
+
98
+ # Tarihleri parse et
99
+ baslangic = _tarih_parse(baslangic_str)
100
+ bitis = _tarih_parse(bitis_str)
101
+
102
+ if not baslangic or not bitis:
103
+ return {"durum": "hata", "neden": "tarih bilgisi eksik"}
104
+
105
+ if event_turu in ("sprint_started", "jira:sprint_created"):
106
+ # Sprint başladı — kayıt oluştur (sağlık skoru hesaplanmadı henüz)
107
+ from codedna.db import save_sprint
108
+ sprint_id = save_sprint(
109
+ sprint_name=sprint_adi,
110
+ start_date=int(baslangic.timestamp()),
111
+ end_date=int(bitis.timestamp()),
112
+ total_lines_ai=0,
113
+ total_lines_human=0,
114
+ avg_understanding=None,
115
+ debt_delta_hours=None,
116
+ health_score=None,
117
+ db_path=db_path,
118
+ )
119
+ return {
120
+ "durum": "oluşturuldu",
121
+ "sprint_id": sprint_id,
122
+ "sprint_adi": sprint_adi,
123
+ }
124
+
125
+ elif event_turu in ("sprint_closed", "jira:sprint_completed"):
126
+ # Sprint kapandı — sağlık skoru hesapla
127
+ from codedna.sprint_health import calculate_sprint_health, save_sprint_result
128
+ from codedna.git_hook import find_git_root
129
+
130
+ repo_koku = find_git_root() or Path.cwd()
131
+ try:
132
+ sonuc = calculate_sprint_health(repo_koku, db_path, baslangic, bitis, sprint_adi)
133
+ sprint_id = save_sprint_result(sonuc, db_path)
134
+ return {
135
+ "durum": "tamamlandı",
136
+ "sprint_id": sprint_id,
137
+ "sprint_adi": sprint_adi,
138
+ "health_score": sonuc.health_score,
139
+ "durum_etiketi": sonuc.durum,
140
+ }
141
+ except Exception as e:
142
+ return {"durum": "hata", "neden": str(e)}
143
+
144
+ return {"durum": "atlandı", "neden": f"desteklenmeyen event: {event_turu}"}
145
+
146
+
147
+ def _tarih_parse(tarih_str: Optional[str]) -> Optional[datetime]:
148
+ """
149
+ Jira tarih string'lerini datetime'a çevir.
150
+ Desteklenen formatlar: ISO 8601 ile çeşitli varyantlar.
151
+ """
152
+ if not tarih_str:
153
+ return None
154
+ formatlar = [
155
+ "%Y-%m-%dT%H:%M:%S.%f%z",
156
+ "%Y-%m-%dT%H:%M:%S%z",
157
+ "%Y-%m-%dT%H:%M:%S.%f",
158
+ "%Y-%m-%dT%H:%M:%S",
159
+ "%Y-%m-%d",
160
+ ]
161
+ for fmt in formatlar:
162
+ try:
163
+ return datetime.strptime(tarih_str[:len(fmt) + 5], fmt)
164
+ except ValueError:
165
+ continue
166
+ return None
@@ -0,0 +1,236 @@
1
+ """Lemon Squeezy checkout ve abonelik webhook entegrasyonu."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Ortam değişkenleri — koda gömülmez
14
+ # ---------------------------------------------------------------------------
15
+
16
+ LEMONSQUEEZY_API_KEY = os.environ.get("LEMONSQUEEZY_API_KEY")
17
+ LEMONSQUEEZY_WEBHOOK_SECRET = os.environ.get("LEMONSQUEEZY_WEBHOOK_SECRET")
18
+ LEMONSQUEEZY_STORE_ID = os.environ.get("LEMONSQUEEZY_STORE_ID")
19
+
20
+ # Plan → Lemon Squeezy variant ID eşlemesi (gerçek ID'ler .env'den)
21
+ PLAN_VARIANT_MAP: dict[str, Optional[str]] = {
22
+ "pro": os.environ.get("LS_VARIANT_PRO"),
23
+ "team": os.environ.get("LS_VARIANT_TEAM"),
24
+ "enterprise": os.environ.get("LS_VARIANT_ENTERPRISE"),
25
+ }
26
+
27
+ # Lemon Squeezy API temel URL'i
28
+ _LS_API_BASE = "https://api.lemonsqueezy.com/v1"
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Webhook imza doğrulama
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def verify_webhook_signature(payload: bytes, signature: str) -> bool:
36
+ """
37
+ Lemon Squeezy webhook imzasını HMAC-SHA256 ile doğrula.
38
+
39
+ Lemon Squeezy, X-Signature header'ında hex digest gönderir.
40
+ hmac.compare_digest ile timing-safe karşılaştırma yapılır.
41
+
42
+ Args:
43
+ payload: Ham HTTP body (bytes)
44
+ signature: X-Signature header değeri
45
+
46
+ Returns:
47
+ İmza geçerliyse True
48
+ """
49
+ secret = LEMONSQUEEZY_WEBHOOK_SECRET
50
+ if not secret:
51
+ return False
52
+
53
+ hesaplanan = hmac.new(
54
+ secret.encode("utf-8"),
55
+ payload,
56
+ hashlib.sha256,
57
+ ).hexdigest()
58
+
59
+ return hmac.compare_digest(hesaplanan, signature)
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Checkout URL oluşturma
64
+ # ---------------------------------------------------------------------------
65
+
66
+ def create_checkout_url(
67
+ plan: str,
68
+ user_email: str,
69
+ user_id: int,
70
+ ) -> str:
71
+ """
72
+ Lemon Squeezy Checkout API'sine istek at ve hosted checkout URL'ini döndür.
73
+
74
+ custom_data içine user_id gömülür — webhook'ta hangi kullanıcıya ait
75
+ olduğunu eşleştirmek için kullanılır.
76
+
77
+ Dokümantasyon: https://docs.lemonsqueezy.com/api/checkouts
78
+
79
+ Args:
80
+ plan: "pro" | "team" | "enterprise"
81
+ user_email: Kullanıcının e-posta adresi (checkout'ta ön doldurulur)
82
+ user_id: Kullanıcı ID'si (webhook eşleştirmesi için)
83
+
84
+ Returns:
85
+ Checkout URL string'i
86
+
87
+ Raises:
88
+ ValueError: API anahtarı veya variant ID eksikse
89
+ RuntimeError: API isteği başarısız olursa
90
+ """
91
+ import urllib.request
92
+
93
+ api_key = LEMONSQUEEZY_API_KEY
94
+ store_id = LEMONSQUEEZY_STORE_ID
95
+ variant_id = PLAN_VARIANT_MAP.get(plan)
96
+
97
+ if not api_key:
98
+ raise ValueError(
99
+ "LEMONSQUEEZY_API_KEY ortam değişkeni tanımlanmamış. "
100
+ ".env.example dosyasına bakın."
101
+ )
102
+ if not store_id:
103
+ raise ValueError("LEMONSQUEEZY_STORE_ID ortam değişkeni tanımlanmamış.")
104
+ if not variant_id:
105
+ raise ValueError(
106
+ f"LS_VARIANT_{plan.upper()} ortam değişkeni tanımlanmamış."
107
+ )
108
+
109
+ istek_govdesi = json.dumps({
110
+ "data": {
111
+ "type": "checkouts",
112
+ "attributes": {
113
+ "checkout_data": {
114
+ "email": user_email,
115
+ "custom": {"user_id": str(user_id)},
116
+ },
117
+ },
118
+ "relationships": {
119
+ "store": {
120
+ "data": {"type": "stores", "id": str(store_id)}
121
+ },
122
+ "variant": {
123
+ "data": {"type": "variants", "id": str(variant_id)}
124
+ },
125
+ },
126
+ }
127
+ }).encode("utf-8")
128
+
129
+ istek = urllib.request.Request(
130
+ f"{_LS_API_BASE}/checkouts",
131
+ data=istek_govdesi,
132
+ headers={
133
+ "Authorization": f"Bearer {api_key}",
134
+ "Content-Type": "application/vnd.api+json",
135
+ "Accept": "application/vnd.api+json",
136
+ },
137
+ method="POST",
138
+ )
139
+
140
+ try:
141
+ with urllib.request.urlopen(istek, timeout=10) as yanit:
142
+ veri = json.loads(yanit.read())
143
+ return veri["data"]["attributes"]["url"]
144
+ except urllib.error.HTTPError as e:
145
+ hata_govdesi = e.read().decode("utf-8", errors="replace")
146
+ raise RuntimeError(
147
+ f"Lemon Squeezy API hatası ({e.code}): {hata_govdesi}"
148
+ )
149
+ except Exception as e:
150
+ raise RuntimeError(f"Checkout URL oluşturulamadı: {e}")
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Webhook event işleme
155
+ # ---------------------------------------------------------------------------
156
+
157
+ # LS event → (plan, subscription_status) eşlemesi
158
+ _EVENT_DURUM_MAP: dict[str, tuple[Optional[str], str]] = {
159
+ "subscription_created": (None, "active"), # plan custom_data'dan alınır
160
+ "subscription_updated": (None, "active"),
161
+ "subscription_cancelled": (None, "cancelled"), # plan hemen düşürülmez
162
+ "subscription_expired": ("free", "none"),
163
+ "subscription_payment_failed": (None, "past_due"),
164
+ }
165
+
166
+
167
+ def handle_subscription_webhook(
168
+ payload: dict,
169
+ db_path: Path,
170
+ ) -> dict:
171
+ """
172
+ Lemon Squeezy webhook event'lerini işle ve kullanıcı planını güncelle.
173
+
174
+ Tüm event'lerde custom_data.user_id ile kullanıcı eşleştirilir.
175
+
176
+ Args:
177
+ payload: Webhook JSON payload'u
178
+ db_path: Auth DB yolu
179
+
180
+ Returns:
181
+ İşlem sonucu sözlüğü
182
+ """
183
+ from codedna.auth import update_user_plan, init_auth_db
184
+
185
+ init_auth_db(db_path)
186
+
187
+ event_turu = payload.get("meta", {}).get("event_name", "")
188
+ if event_turu not in _EVENT_DURUM_MAP:
189
+ return {"durum": "atlandı", "event": event_turu}
190
+
191
+ # Kullanıcı ID'sini custom_data'dan al
192
+ custom_data = payload.get("meta", {}).get("custom_data", {})
193
+ user_id_str = custom_data.get("user_id") or custom_data.get("user_id")
194
+ if not user_id_str:
195
+ return {"durum": "hata", "neden": "custom_data.user_id eksik"}
196
+
197
+ try:
198
+ user_id = int(user_id_str)
199
+ except (ValueError, TypeError):
200
+ return {"durum": "hata", "neden": f"Geçersiz user_id: {user_id_str!r}"}
201
+
202
+ # Abonelik verilerini al
203
+ abonelik = payload.get("data", {}).get("attributes", {})
204
+ customer_id = str(abonelik.get("customer_id", "")) or None
205
+ subscription_id = str(payload.get("data", {}).get("id", "")) or None
206
+
207
+ # Planı belirle
208
+ yeni_plan, yeni_durum = _EVENT_DURUM_MAP[event_turu]
209
+
210
+ # subscription_created/updated için plan variant'tan belirle
211
+ if yeni_plan is None:
212
+ variant_id = str(abonelik.get("variant_id", ""))
213
+ # Variant ID → plan eşlemesi (ters yön)
214
+ for plan_adi, vid in PLAN_VARIANT_MAP.items():
215
+ if vid and vid == variant_id:
216
+ yeni_plan = plan_adi
217
+ break
218
+ if yeni_plan is None:
219
+ yeni_plan = "pro" # bilinmeyen variant → pro varsayılan
220
+
221
+ update_user_plan(
222
+ user_id=user_id,
223
+ plan=yeni_plan,
224
+ subscription_status=yeni_durum,
225
+ customer_id=customer_id,
226
+ subscription_id=subscription_id,
227
+ db_path=db_path,
228
+ )
229
+
230
+ return {
231
+ "durum": "güncellendi",
232
+ "user_id": user_id,
233
+ "yeni_plan": yeni_plan,
234
+ "subscription_status": yeni_durum,
235
+ "event": event_turu,
236
+ }