magaza-mcp 0.3.0 → 0.4.0

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.
package/AGENTS.md CHANGED
@@ -157,8 +157,8 @@ npx -y magaza-mcp kaydet --magazalar appstore,play --salt-okunur
157
157
  ```
158
158
 
159
159
  Bu modda yazma yapan araçlar (`appstore__yorum_yanitla`,
160
- `appstore__metin_guncelle`, fiyat/abonelik/yaş derecelendirme araçları,
161
- `play__yorum_yanitla`) listeye hiç girmez; 32 araç 24'e düşer. `magaza__cagir` listede kalır — katalogdaki okuma uçlarına da o
160
+ `appstore__metin_guncelle`, fiyat/abonelik/yaş derecelendirme araçları, Play yayın araçları,
161
+ `play__yorum_yanitla`) listeye hiç girmez; 39 araç 26'ya düşer. `magaza__cagir` listede kalır — katalogdaki okuma uçlarına da o
162
162
  araçtan gidiliyor — ama veri değiştiren bir operasyon istendiğinde reddeder.
163
163
 
164
164
  Komut mevcut ayarlara dokunmaz: yalnızca `magaza-mcp` girdisini ekler veya
@@ -227,15 +227,23 @@ npx magaza-mcp kur
227
227
 
228
228
  ### Yüklü araçlar günlük işi görür
229
229
 
230
- İki mağaza da açıkken 32 araç yüklenir: 16 tane `appstore__`, 10 tane `play__`,
230
+ İki mağaza da açıkken 39 araç yüklenir: 17 tane `appstore__`, 16 tane `play__`,
231
231
  3 tane iki mağazayı birden sorgulayan `magaza__`, 3 tane de aşağıdaki erişim
232
232
  aracı. Tek mağazalı kurulumda çapraz araçlar hiç oluşmaz: yalnızca App Store'da
233
- 19, yalnızca Play'de 13 araç yüklenir. Araç tanımlarının tamamı bağlamda
234
- yaklaşık **6.000 token** yer kaplar.
233
+ 20, yalnızca Play'de 19 araç yüklenir. Araç tanımlarının tamamı bağlamda
234
+ yaklaşık **8.500 token** yer kaplar.
235
235
 
236
236
  Abonelik ve IAP fiyatı, abonelik kurulumu, satış ülkeleri ve yaş
237
237
  derecelendirmesi için hazır `appstore__` araçları var. Bunları
238
238
  `magaza__cagir` ile ülke ülke yapma: hazır araç 175 ülkeyi tek istekte yazar.
239
+ Kullanıcı "ülkelere göre fiyatlandır" derse önce `appstore__fiyat_analizi` ile
240
+ öneri al, kullanıcıya göster, onaylarsa çıktıdaki `ozel_fiyatlar`'ı
241
+ `appstore__abonelik_fiyat_ayarla`'ya ver. Play'de aynısı `play__fiyat_analizi` →
242
+ `play__abonelik_fiyat_ayarla`.
243
+
244
+ Play'de sürüm yayınlama (AAB yükleme dahil), rollout yüzdesi, kanallar arası
245
+ taşıma, mağaza metni ve görsel için hazır `play__` araçları var; bunları
246
+ `magaza__cagir` ile edit oturumu açıp kapatarak yapma.
239
247
 
240
248
  Hangi araçların yüklü olduğunu `npx -y magaza-mcp araclar` ile görebilirsin.
241
249
 
package/README.md CHANGED
@@ -62,8 +62,8 @@ directly to Apple and Google, and your keys never leave your Keychain.
62
62
  | [🧑‍💻 Manual setup](#-manual-setup) | If you're not using an assistant |
63
63
  | [🧩 Do the two stores mix up?](#-do-the-two-stores-mix-up) | No — here's why |
64
64
  | [💬 What you can ask](#-what-you-can-ask) | Example prompts |
65
- | [🧰 Tools](#-tools) | Full list of all 32 tools |
66
- | [🌐 Full API access](#-full-api-access) | 1,440 operations, ~6,000 tokens |
65
+ | [🧰 Tools](#-tools) | Full list of all 39 tools |
66
+ | [🌐 Full API access](#-full-api-access) | 1,440 operations, ~8,500 tokens |
67
67
  | [🔑 Required permissions](#-required-permissions) | What Apple and Google require |
68
68
  | [🔒 Security](#-security) | Keys, approval gates, telemetry |
69
69
  | [🩺 Troubleshooting](#-troubleshooting) | Common errors |
@@ -274,10 +274,10 @@ model, and cost zero tokens.
274
274
 
275
275
  | Setup | Tools loaded |
276
276
  | --- | --- |
277
- | Both stores | 32 |
278
- | App Store only | 19 |
279
- | Play only | 13 |
280
- | Read-only (both stores) | 24 |
277
+ | Both stores | 39 |
278
+ | App Store only | 20 |
279
+ | Play only | 19 |
280
+ | Read-only (both stores) | 26 |
281
281
 
282
282
  **3. Cross-store tools only exist when both stores are enabled.** In a
283
283
  single-store setup they're never created at all — they'd have no meaning.
@@ -336,7 +336,7 @@ These tools only load when both stores are connected.
336
336
  </details>
337
337
 
338
338
  <details>
339
- <summary><b>App Store Connect — 16 tools</b> (<code>appstore__</code>)</summary>
339
+ <summary><b>App Store Connect — 17 tools</b> (<code>appstore__</code>)</summary>
340
340
 
341
341
  <br>
342
342
 
@@ -353,7 +353,8 @@ These tools only load when both stores are connected.
353
353
  | `appstore__testflight_gruplari` | Shows TestFlight beta groups, how many testers are in each, public invite links, and quotas. |
354
354
  | `appstore__metin_guncelle` ✎ | Updates the store text of the version in preparation: release notes, description, keywords, promotional text. |
355
355
  | `appstore__satis_raporu` | Downloads the sales/download report as TSV: daily, weekly, monthly, or yearly; sales, pre-order, installs, subscriptions, subscription events, subscribers, and offer code reports. |
356
- | `appstore__abonelik_fiyat_ayarla` ✎ | Sets a subscription's price in **every country at once**: give one price (e.g. USA 9.99), Apple's equalization table fills in all ~175 countries and they're written in a single request. |
356
+ | `appstore__abonelik_fiyat_ayarla` ✎ | Sets a subscription's price in **every country at once**: give one price (e.g. USA 9.99), Apple's equalization table fills in all ~175 countries and they're written in a single request. Add country-specific prices on top (`{"TUR": "349.99"}`), or change only those countries and leave the rest alone. |
357
+ | `appstore__fiyat_analizi` | Regional pricing suggestion, writes nothing: starts from Apple's equalized price and applies a softened discount based on each country's purchasing power (World Bank). Never raises a price, never cuts more than half, leaves rich countries alone. The output plugs straight into `abonelik_fiyat_ayarla`. |
357
358
  | `appstore__iap_fiyat_ayarla` ✎ | Sets a one-time purchase's price from a single base country; Apple fills in the rest. |
358
359
  | `appstore__abonelik_ulkeleri` ✎ | Makes a subscription available in every country (or the ones you list) in one request. |
359
360
  | `appstore__abonelik_kur` ✎ | Creates a subscription from scratch behind one approval: group → subscription → localized names → availability → prices in all countries. |
@@ -362,7 +363,7 @@ These tools only load when both stores are connected.
362
363
  </details>
363
364
 
364
365
  <details>
365
- <summary><b>Google Play — 10 tools</b> (<code>play__</code>)</summary>
366
+ <summary><b>Google Play — 16 tools</b> (<code>play__</code>)</summary>
366
367
 
367
368
  <br>
368
369
 
@@ -377,7 +378,13 @@ These tools only load when both stores are connected.
377
378
  | `play__urunler` | Lists one-time products; tries both the new (`oneTimeProducts`) and legacy (`inappproducts`) models. |
378
379
  | `play__satin_alma_dogrula` | Verifies a purchase token: subscription status, acknowledgment status, expiry date, whether it's a test purchase. |
379
380
  | `play__iade_edilenler` | Lists cancelled, refunded, or charged-back purchases; turns the refund reason and source into something readable. Google only returns the last 30 days. |
380
- | `play__cokme_orani` | Fetches Android vitals crash rate day by day, along with the number of distinct users in the measurement. |
381
+ | `play__cokme_orani` | Android vitals: crash and ANR rates (including the user-perceived rates Play uses for its bad-behavior thresholds), whether a threshold is exceeded, and the most frequent crash clusters with stack traces. Can be narrowed to one version code. |
382
+ | `play__surum_yayinla` ✎ | Uploads an AAB/APK and releases it to a track in one approval: upload → assign to track → release notes → commit. Give a percentage for a staged rollout (e.g. production 10%). If anything fails, nothing is committed. |
383
+ | `play__yayin_yonet` ✎ | Manages a track: change the rollout %, halt, resume, complete, promote a release to another track (internal → production), or update only the release notes. The preview shows the track before and after. |
384
+ | `play__listeleme_guncelle` ✎ | Reads or updates store listing text (title, short and full description, video) for every language in one edit. |
385
+ | `play__gorsel_yukle` ✎ | Uploads screenshots, the icon, or the feature graphic, optionally replacing the existing ones. |
386
+ | `play__abonelik_fiyat_ayarla` ✎ | Sets a base plan's price in every country at once from a USD price (Google's own conversion), with optional country-specific prices on top, or changes only the countries you list. |
387
+ | `play__fiyat_analizi` | Regional pricing suggestion for Play, writes nothing: same purchasing-power logic as the App Store version, rounded to natural-looking local prices. |
381
388
 
382
389
  </details>
383
390
 
@@ -425,7 +432,7 @@ any of them.
425
432
 
426
433
  | | Tools | Paid on every message |
427
434
  | --- | --- | --- |
428
- | **magaza-mcp** | 32 | **~6,000 tokens** |
435
+ | **magaza-mcp** | 39 | **~8,500 tokens** |
429
436
  | If all were separate tools | 1,440 | ~210,000 tokens |
430
437
 
431
438
  > [!NOTE]
@@ -482,7 +489,7 @@ some analytics reports need higher privileges.
482
489
  the client's approval UI.
483
490
  - **Read-only mode.** `--salt-okunur` (or `SALT_OKUNUR=1`) removes every
484
491
  write-capable tool from the list entirely: the model can't see them or call
485
- them. 32 tools become 24. `magaza__cagir` stays in the list because it's
492
+ them. 39 tools become 26. `magaza__cagir` stays in the list because it's
486
493
  also the single gateway for read endpoints in the catalog — but it refuses
487
494
  write operations when read-only mode is on.
488
495
  - **Path parameters are validated.** IDs passed to tools are encoded before
@@ -500,8 +507,10 @@ some analytics reports need higher privileges.
500
507
  - **No telemetry.** No analytics, error reports, or usage data are sent.
501
508
  Network traffic only goes to `api.appstoreconnect.apple.com`,
502
509
  `androidpublisher.googleapis.com`, `playdeveloperreporting.googleapis.com`,
503
- and `oauth2.googleapis.com` for tokens. There's no server in the middle;
504
- data flows directly between your machine and Apple/Google.
510
+ and `oauth2.googleapis.com` for tokens. The one exception is
511
+ the two `fiyat_analizi` tools, which read public purchasing-power figures from
512
+ `api.worldbank.org` (no key, nothing of yours is sent). There's no server in
513
+ the middle; data flows directly between your machine and Apple/Google.
505
514
 
506
515
  ---
507
516
 
package/README.tr.md CHANGED
@@ -62,8 +62,8 @@ Google ile konuşur, anahtarın Anahtar Zinciri'nden dışarı çıkmaz.
62
62
  | [🧑‍💻 Elle kurulum](#-elle-kurulum) | Asistan kullanmıyorsan |
63
63
  | [🧩 İki mağaza karışır mı?](#-iki-mağaza-karışır-mı) | Hayır — nedeni burada |
64
64
  | [💬 Ne sorabilirsin](#-ne-sorabilirsin) | Örnek komutlar |
65
- | [🧰 Araçlar](#-araçlar) | 32 aracın tam listesi |
66
- | [🌐 Tam API erişimi](#-tam-api-erişimi) | 1440 operasyon, ~6.000 token |
65
+ | [🧰 Araçlar](#-araçlar) | 39 aracın tam listesi |
66
+ | [🌐 Tam API erişimi](#-tam-api-erişimi) | 1440 operasyon, ~8.500 token |
67
67
  | [🔑 Gereken izinler](#-gereken-izinler) | Apple ve Google tarafında ne şart |
68
68
  | [🔒 Güvenlik](#-güvenlik) | Anahtarlar, onay kapıları, telemetri |
69
69
  | [🩺 Sorun giderme](#-sorun-giderme) | Sık karşılaşılan hatalar |
@@ -270,10 +270,10 @@ belleğe bile alınmaz, modele gösterilmez, token harcamaz.
270
270
 
271
271
  | Kurulum | Yüklenen araç |
272
272
  | --- | --- |
273
- | İki mağaza | 32 |
274
- | Yalnızca App Store | 19 |
275
- | Yalnızca Play | 13 |
276
- | Salt-okunur (iki mağaza) | 24 |
273
+ | İki mağaza | 39 |
274
+ | Yalnızca App Store | 20 |
275
+ | Yalnızca Play | 19 |
276
+ | Salt-okunur (iki mağaza) | 26 |
277
277
 
278
278
  **3. Çapraz araçlar yalnızca iki mağaza da açıkken var olur.** Tek mağazalı
279
279
  kurulumda hiç oluşturulmazlar — anlamları olmadığı için.
@@ -333,7 +333,7 @@ Bu araçlar yalnızca iki mağaza da bağlıyken yüklenir.
333
333
  </details>
334
334
 
335
335
  <details>
336
- <summary><b>App Store Connect — 16 araç</b> (<code>appstore__</code>)</summary>
336
+ <summary><b>App Store Connect — 17 araç</b> (<code>appstore__</code>)</summary>
337
337
 
338
338
  <br>
339
339
 
@@ -350,7 +350,8 @@ Bu araçlar yalnızca iki mağaza da bağlıyken yüklenir.
350
350
  | `appstore__testflight_gruplari` | TestFlight beta gruplarını, her gruptaki test kullanıcı sayısını, genel davet linklerini ve kotalarını gösterir. |
351
351
  | `appstore__metin_guncelle` ✎ | Hazırlanmakta olan sürümün mağaza metinlerini günceller: sürüm notları, açıklama, anahtar kelimeler, tanıtım metni. |
352
352
  | `appstore__satis_raporu` | Satış/indirme raporunu TSV olarak indirir: günlük, haftalık, aylık veya yıllık; satış, ön sipariş, yükleme, abonelik, abonelik olayı, abone ve teklif kodu raporları. |
353
- | `appstore__abonelik_fiyat_ayarla` ✎ | Aboneliğin fiyatını **bütün ülkelerde tek seferde** ayarlar: bir fiyat verirsin (örn. USA 9.99), Apple'ın eşitleme tablosu ~175 ülkenin karşılığını çıkarır, hepsi tek istekte yazılır. |
353
+ | `appstore__abonelik_fiyat_ayarla` ✎ | Aboneliğin fiyatını **bütün ülkelerde tek seferde** ayarlar: bir fiyat verirsin (örn. USA 9.99), Apple'ın eşitleme tablosu ~175 ülkenin karşılığını çıkarır, hepsi tek istekte yazılır. Üstüne ülkeye özel fiyat ekleyebilirsin (`{"TUR": "349.99"}`) ya da yalnızca o ülkeleri değiştirip gerisine dokunmazsın. |
354
+ | `appstore__fiyat_analizi` | Bölgesel fiyat önerir, hiçbir şey yazmaz: Apple'ın eşit fiyatından yola çıkıp ülkenin alım gücüne (Dünya Bankası) göre yumuşatılmış indirim uygular. Fiyat asla artmaz, yarıdan fazla düşmez, zengin ülkelere dokunulmaz. Çıktısı doğrudan `abonelik_fiyat_ayarla`'ya verilir. |
354
355
  | `appstore__iap_fiyat_ayarla` ✎ | Tek seferlik satın almanın fiyatını tek bir taban ülkeden ayarlar; gerisini Apple doldurur. |
355
356
  | `appstore__abonelik_ulkeleri` ✎ | Aboneliği bütün ülkelerde (ya da verdiklerinde) tek istekte satışa açar. |
356
357
  | `appstore__abonelik_kur` ✎ | Sıfırdan abonelik kurar, tek onayla: grup → abonelik → dil metinleri → satış ülkeleri → bütün ülkelerde fiyat. |
@@ -359,7 +360,7 @@ Bu araçlar yalnızca iki mağaza da bağlıyken yüklenir.
359
360
  </details>
360
361
 
361
362
  <details>
362
- <summary><b>Google Play — 10 araç</b> (<code>play__</code>)</summary>
363
+ <summary><b>Google Play — 16 araç</b> (<code>play__</code>)</summary>
363
364
 
364
365
  <br>
365
366
 
@@ -374,7 +375,13 @@ Bu araçlar yalnızca iki mağaza da bağlıyken yüklenir.
374
375
  | `play__urunler` | Tek seferlik ürünleri listeler; yeni (`oneTimeProducts`) ve eski (`inappproducts`) modelin ikisini de dener. |
375
376
  | `play__satin_alma_dogrula` | Bir satın alma token'ını doğrular: abonelik durumu, onay durumu, bitiş tarihi, test satın alması mı. |
376
377
  | `play__iade_edilenler` | İptal edilmiş, iade edilmiş veya geri alınmış satın almaları listeler; iade sebebini ve kaynağını okunur hale getirir. Google yalnızca son 30 günü verir. |
377
- | `play__cokme_orani` | Android vitals çökme oranını, ölçüme giren farklı kullanıcı sayısıyla birlikte günlük olarak getirir. |
378
+ | `play__cokme_orani` | Android vitals: çökme ve ANR oranları (Play'in kötü davranış eşiğinde baktığı "algılanan" oranlar dahil), eşik aşıldı mı, en sık çökme kümeleri ve stack trace'leri. Tek bir sürüm koduna daraltılabilir. |
379
+ | `play__surum_yayinla` ✎ | AAB/APK'yı yükleyip bir kanala yayınlar, tek onayla: yükle → kanala ata → sürüm notları → commit. Yüzde verirsen kademeli yayın (örn. production %10). Bir adım düşerse hiçbir şey commit edilmez. |
380
+ | `play__yayin_yonet` ✎ | Kanalı yönetir: rollout yüzdesini değiştir, durdur, sürdür, tamamla, sürümü başka kanala taşı (internal → production) ya da yalnızca sürüm notlarını güncelle. Önizlemede kanalın önceki ve sonraki hali görünür. |
381
+ | `play__listeleme_guncelle` ✎ | Mağaza metinlerini (başlık, kısa ve tam açıklama, video) okur ya da bütün dillerde tek oturumda günceller. |
382
+ | `play__gorsel_yukle` ✎ | Ekran görüntüsü, ikon veya öne çıkan görsel yükler; istersen eskilerini önce siler. |
383
+ | `play__abonelik_fiyat_ayarla` ✎ | Bir temel planın fiyatını USD fiyattan bütün ülkelerde tek seferde ayarlar (Google'ın kendi çevirisiyle); üstüne ülkeye özel fiyat eklenebilir ya da yalnızca verdiğin ülkeler değişir. |
384
+ | `play__fiyat_analizi` | Play için bölgesel fiyat önerir, hiçbir şey yazmaz: App Store'dakiyle aynı alım gücü mantığı, mağazada doğal görünen yerel fiyatlara yuvarlanır. |
378
385
 
379
386
  </details>
380
387
 
@@ -420,7 +427,7 @@ hiçbirini kullanmasan bile her mesajda yeniden ödenir.
420
427
 
421
428
  | | Araç | Her mesajda ödenen |
422
429
  | --- | --- | --- |
423
- | **magaza-mcp** | 32 | **~6.000 token** |
430
+ | **magaza-mcp** | 39 | **~8.500 token** |
424
431
  | Hepsi ayrı araç olsaydı | 1440 | ~210.000 token |
425
432
 
426
433
  > [!NOTE]
@@ -472,8 +479,8 @@ analitik raporlar daha yüksek yetki ister.
472
479
  onayladıktan sonra `onayla=true` ile tekrarlandığında yürür. Bu **sunucu
473
480
  tarafında** uygulanır — istemcinin onay arayüzüne bağlı değildir.
474
481
  - **Salt-okunur mod.** `--salt-okunur` (veya `SALT_OKUNUR=1`) yazma yapan
475
- araçları listeden tamamen çıkarır: model onları göremez, çağıramaz. 32 araç
476
- 24'e düşer. `magaza__cagir` listede kalır çünkü okuma uçlarının da tek
482
+ araçları listeden tamamen çıkarır: model onları göremez, çağıramaz. 39 araç
483
+ 26'ya düşer. `magaza__cagir` listede kalır çünkü okuma uçlarının da tek
477
484
  kapısıdır — ama yazma operasyonu istendiğinde reddeder.
478
485
  - **Yol parametreleri doğrulanır.** Araçlara verilen kimlikler URL'e girmeden
479
486
  önce kodlanır; `.` ve `..` gibi yol gezinme denemeleri reddedilir. Mutlak
@@ -490,8 +497,11 @@ analitik raporlar daha yüksek yetki ister.
490
497
  - **Telemetri yok.** Hiçbir analitik, hata raporu veya kullanım verisi
491
498
  gönderilmez. Ağ trafiği yalnızca `api.appstoreconnect.apple.com`,
492
499
  `androidpublisher.googleapis.com`, `playdeveloperreporting.googleapis.com` ve
493
- token için `oauth2.googleapis.com` adreslerine gider. Araya giren bir sunucu
494
- yoktur; veri doğrudan senin makinenle Apple ve Google arasında akar.
500
+ token için `oauth2.googleapis.com` adreslerine gider. Tek istisna iki
501
+ `fiyat_analizi` aracı: herkese açık alım gücü verisini
502
+ `api.worldbank.org`'dan okur (anahtarsız, senden hiçbir veri gitmez). Araya
503
+ giren bir sunucu yoktur; veri doğrudan senin makinenle Apple ve Google
504
+ arasında akar.
495
505
 
496
506
  ---
497
507
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Ülkelerin fiyat seviyesi — bölgesel fiyat önerileri için.
3
+ *
4
+ * App Store ve Play analiz araçları ortak kullanır.
5
+ */
6
+ let _alimGucu;
7
+ /**
8
+ * Dünya Bankası'ndan ülkelerin fiyat seviyesi verisini çeker.
9
+ *
10
+ * Hane tüketimi PPP çevrim katsayısı / resmi kur: ABD = 1, Türkiye ≈ 0.45,
11
+ * Hindistan ≈ 0.23. Anahtarsız, herkese açık veri; bizden hiçbir şey gitmez.
12
+ */
13
+ export function alimGucuVerisi() {
14
+ const getir = async (gosterge) => {
15
+ const yanit = await fetch(`https://api.worldbank.org/v2/country/all/indicator/${gosterge}?format=json&mrnev=1&per_page=500`, { signal: AbortSignal.timeout(10_000) });
16
+ const [, veri] = (await yanit.json());
17
+ if (!Array.isArray(veri))
18
+ throw new Error(`Dünya Bankası ${gosterge} verisi gelmedi.`);
19
+ return new Map(veri
20
+ .filter((x) => x?.value != null && x?.countryiso3code)
21
+ // Apple ülkeleri 3 harfle (TUR), Play 2 harfle (TR) yazıyor; ikisini de anahtar yapıyoruz.
22
+ .flatMap((x) => {
23
+ const kayit = { deger: Number(x.value), yil: String(x.date) };
24
+ return [[x.countryiso3code, kayit], [x.country?.id, kayit]];
25
+ })
26
+ .filter(([u]) => !!u));
27
+ };
28
+ _alimGucu ??= Promise.all([getir("PA.NUS.PRVT.PP"), getir("PA.NUS.FCRF")])
29
+ .then(([ppp, kur]) => ({ ppp, kur }))
30
+ .catch((hata) => {
31
+ _alimGucu = undefined;
32
+ throw new Error(`Alım gücü verisi alınamadı (api.worldbank.org): ${hata.message}`);
33
+ });
34
+ return _alimGucu;
35
+ }
36
+ /**
37
+ * Bir ülkenin fiyat seviyesi (ABD = 1).
38
+ *
39
+ * Mağaza avroyla satıyorsa avro kuru kullanılır: avroya yeni geçen ülkelerde
40
+ * (Bulgaristan 2026) PPP avroya çevrilmiş ama kur serisi eski parada kalmış,
41
+ * oran yarıya düşüyordu. İki seri farklı yıldansa (Arjantin'de PPP 2021, kur
42
+ * 2025) oran anlamsız; o ülke atlanır.
43
+ */
44
+ export function fiyatSeviyesi(v, ulke, para) {
45
+ const p = v.ppp.get(ulke);
46
+ const k = para === "EUR" ? v.kur.get("DEU") : v.kur.get(ulke);
47
+ if (!p || !k || k.yil !== p.yil || !(k.deger > 0))
48
+ return undefined;
49
+ return p.deger / k.deger;
50
+ }
@@ -6,10 +6,14 @@
6
6
  * 175 ülke × (önizleme + onay) ≈ 350 araç çağrısı. Buradaki araçlar döngüyü
7
7
  * sunucuda koşturur; model yalnızca bir önizleme ve bir onay görür.
8
8
  *
9
+ * appstore__fiyat_analizi da burada: yazmaz, ama ülkeye özel fiyat önerisini
10
+ * abonelik_fiyat_ayarla'nın doğrudan kabul ettiği biçimde üretir.
11
+ *
9
12
  * Yollar ve gövdeler Apple OpenAPI 4.5'e karşı doğrulandı; standart
10
13
  * aboneliğin planType'ı canlı hesapta UPFRONT okundu.
11
14
  */
12
15
  import { cagir, tumunuGetir } from "../istemci/apple.js";
16
+ import { alimGucuVerisi, fiyatSeviyesi } from "./alim-gucu.js";
13
17
  import { kimlik, metin, onayIste, onaySemasi, parca, ulkeKodu } from "./appstore.js";
14
18
  /** Fiyat noktası uçları tek sayfada 8000 kayda izin verir (ABD'de ~800 nokta). */
15
19
  const FIYAT_SAYFASI = 8000;
@@ -328,6 +332,79 @@ async function beyanOku(appInfoId) {
328
332
  return yanit?.data;
329
333
  }
330
334
  /* ------------------------------------------------------------------ */
335
+ /* Ülkeye özel fiyat */
336
+ /* ------------------------------------------------------------------ */
337
+ /**
338
+ * Birden çok ülkenin fiyat basamaklarını getirir.
339
+ *
340
+ * `filter[territory]` liste kabul ediyor; ülke başına ~800-850 basamak var,
341
+ * 9 ülke tek 8000'lik sayfaya sığıyor. 120 ülke = 14 istek, altışar paralel.
342
+ */
343
+ async function ulkeNoktalari(aboneId, ulkeler) {
344
+ const gruplar = [];
345
+ for (let i = 0; i < ulkeler.length; i += 9)
346
+ gruplar.push(ulkeler.slice(i, i + 9));
347
+ const sonuc = new Map();
348
+ for (let i = 0; i < gruplar.length; i += 6) {
349
+ const sayfalar = await Promise.all(gruplar.slice(i, i + 6).map((grup) => tumunuGetir(`/v1/subscriptions/${parca(aboneId, "abonelik_id")}/pricePoints`, {
350
+ "filter[territory]": grup.join(","),
351
+ include: "territory",
352
+ "fields[subscriptionPricePoints]": "customerPrice,proceeds,territory",
353
+ "fields[territories]": "currency",
354
+ }, FIYAT_SAYFASI * 2, FIYAT_SAYFASI)));
355
+ for (const { data, included } of sayfalar) {
356
+ const paralar = new Map();
357
+ for (const t of included)
358
+ if (t?.type === "territories")
359
+ paralar.set(t.id, t.attributes?.currency);
360
+ for (const n of data) {
361
+ const u = n.relationships?.territory?.data?.id;
362
+ if (!u)
363
+ continue;
364
+ if (!sonuc.has(u))
365
+ sonuc.set(u, []);
366
+ sonuc.get(u).push({
367
+ ulke: u,
368
+ nokta: n.id,
369
+ fiyat: n.attributes?.customerPrice,
370
+ gelir: n.attributes?.proceeds,
371
+ para: paralar.get(u),
372
+ });
373
+ }
374
+ }
375
+ }
376
+ return sonuc;
377
+ }
378
+ function enYakin(noktalar, hedef) {
379
+ return noktalar.reduce((a, b) => Math.abs(Number(b.fiyat) - hedef) < Math.abs(Number(a.fiyat) - hedef) ? b : a);
380
+ }
381
+ /**
382
+ * Ülkeye özel fiyatları geçerli basamaklara oturtur.
383
+ *
384
+ * Apple keyfi fiyat kabul etmiyor ("150 TL" yok, 149.99 var); en yakın
385
+ * basamak seçilir ve önizlemede istenenle birlikte gösterilir.
386
+ */
387
+ async function ozelSatirlar(aboneId, ham) {
388
+ if (!ham || typeof ham !== "object" || Array.isArray(ham)) {
389
+ throw new Error("ozel_fiyatlar bir nesne olmalı, örn. {\"TUR\": \"349.99\", \"IND\": \"499\"}.");
390
+ }
391
+ const istenen = new Map();
392
+ for (const [u, f] of Object.entries(ham))
393
+ istenen.set(ulkeKodu(u), fiyatSayisi(f, `${u} fiyatı`));
394
+ const noktalar = await ulkeNoktalari(aboneId, [...istenen.keys()]);
395
+ const satirlar = [];
396
+ const notlar = [];
397
+ for (const [u, f] of istenen) {
398
+ const liste = noktalar.get(u);
399
+ if (!liste?.length)
400
+ throw new Error(`${u} için fiyat basamağı bulunamadı; ülke kodu doğru mu (3 harf, örn. TUR)?`);
401
+ const s = enYakin(liste, f);
402
+ satirlar.push(s);
403
+ notlar.push(`${u}: ${s.fiyat} ${s.para ?? ""}`.trim() + (Number(s.fiyat) !== f ? ` (istenen ${f}, en yakın basamak)` : ""));
404
+ }
405
+ return { satirlar, notlar };
406
+ }
407
+ /* ------------------------------------------------------------------ */
331
408
  /* Araçlar */
332
409
  /* ------------------------------------------------------------------ */
333
410
  // Tip birleşimi (string|number) bazı istemcilerin şema denetiminden geçmiyor;
@@ -342,17 +419,26 @@ export function appstoreYazmaAraclari() {
342
419
  return [
343
420
  {
344
421
  ad: "appstore__abonelik_fiyat_ayarla",
345
- aciklama: "Bir aboneliğin fiyatını TÜM ülkelerde tek seferde ayarlar. Tek bir ülkedeki " +
346
- "fiyatı verirsin (varsayılan USA), Apple'ın eşitleme tablosu ~175 ülkenin " +
347
- "karşılığını çıkarır ve hepsi tek istekte yazılır. Ülke ülke magaza__cagir ile " +
348
- "subscriptionPrices yazma; bu araç onun yerine geçer. Önizlemede ülke sayısı ve " +
349
- "örnek fiyatlar döner.",
422
+ aciklama: "Bir aboneliğin fiyatını ayarlar; döngü sunucuda, tek onayla. Ülke ülke " +
423
+ "magaza__cagir ile subscriptionPrices yazma, bu araç onun yerine geçer.\n" +
424
+ "- fiyat: bir ülkedeki fiyat (varsayılan USA); Apple'ın eşitleme tablosu " +
425
+ "~175 ülkenin karşılığını çıkarır, hepsi yazılır.\n" +
426
+ "- ozel_fiyatlar: {\"TUR\": \"349.99\"} gibi ülkeye özel fiyatlar. fiyat ile " +
427
+ "birlikte verilirse eşitlemenin üstüne yazılır; tek başına verilirse yalnızca " +
428
+ "o ülkeler değişir, diğerlerine dokunulmaz.\n" +
429
+ "Fiyat en yakın geçerli Apple basamağına oturtulur ve önizlemede gösterilir.",
350
430
  girdiSemasi: {
351
431
  type: "object",
352
432
  properties: {
353
433
  abonelik_id: metin("appstore__abonelikler'den gelen abonelik id'si."),
354
434
  fiyat: fiyatSemasi,
355
435
  ulke: ulkeSemasi,
436
+ ozel_fiyatlar: {
437
+ type: "object",
438
+ description: "Ülke kodu → o ülkenin para biriminde fiyat, örn. {\"TUR\": \"349.99\", " +
439
+ "\"IND\": \"499\"}. appstore__fiyat_analizi'nin ozel_fiyatlar çıktısı olduğu gibi verilebilir.",
440
+ additionalProperties: { type: "string" },
441
+ },
356
442
  mevcut_aboneleri_koru: {
357
443
  type: "boolean",
358
444
  description: "Fiyat değişikliğinde mevcut aboneler eski fiyattan devam etsin mi " +
@@ -361,19 +447,34 @@ export function appstoreYazmaAraclari() {
361
447
  baslangic: baslangicSemasi,
362
448
  onayla: onaySemasi,
363
449
  },
364
- required: ["abonelik_id", "fiyat"],
450
+ required: ["abonelik_id"],
365
451
  },
366
452
  yazma: true,
367
453
  async calistir(girdi) {
368
- const fiyat = fiyatSayisi(girdi.fiyat);
454
+ const tabanVar = girdi.fiyat !== undefined && girdi.fiyat !== "";
455
+ if (!tabanVar && girdi.ozel_fiyatlar === undefined) {
456
+ throw new Error("fiyat ya da ozel_fiyatlar vermelisin.");
457
+ }
458
+ const fiyat = tabanVar ? fiyatSayisi(girdi.fiyat) : undefined;
369
459
  const ulke = ulkeKodu(girdi.ulke) ?? "USA";
370
460
  const koru = girdi.mevcut_aboneleri_koru !== false;
371
461
  const baslangic = tarih(girdi.baslangic);
372
- const matris = await abonelikMatrisi(girdi.abonelik_id, fiyat, ulke);
462
+ const [esit, ozel] = await Promise.all([
463
+ fiyat !== undefined ? abonelikMatrisi(girdi.abonelik_id, fiyat, ulke) : [],
464
+ girdi.ozel_fiyatlar !== undefined
465
+ ? ozelSatirlar(girdi.abonelik_id, girdi.ozel_fiyatlar)
466
+ : { satirlar: [], notlar: [] },
467
+ ]);
468
+ const ozelUlkeler = new Set(ozel.satirlar.map((s) => s.ulke));
469
+ const matris = [...esit.filter((s) => !ozelUlkeler.has(s.ulke)), ...ozel.satirlar];
373
470
  if (girdi.onayla !== true) {
374
- return onayIste(`PATCH /v1/subscriptions/${girdi.abonelik_id} (${matris.length} ülke)`, `Aboneliğin fiyatını ${ulke} ${fiyat} baz alınarak ${matris.length} ülkede ayarlar.`, {
471
+ return onayIste(`PATCH /v1/subscriptions/${girdi.abonelik_id} (${matris.length} ülke)`, fiyat !== undefined
472
+ ? `Aboneliğin fiyatını ${ulke} ${fiyat} baz alınarak ${matris.length} ülkede ayarlar` +
473
+ (ozel.satirlar.length ? `; ${ozel.satirlar.length} ülkede özel fiyat.` : ".")
474
+ : `Yalnızca ${matris.length} ülkenin fiyatını değiştirir; diğer ülkelere dokunmaz.`, {
375
475
  ulke_sayisi: matris.length,
376
- ornek_fiyatlar: ornekler(matris),
476
+ ornek_fiyatlar: fiyat !== undefined ? ornekler(esit) : undefined,
477
+ ozel_fiyatlar: ozel.notlar.length ? ozel.notlar : undefined,
377
478
  mevcut_aboneleri_koru: koru,
378
479
  baslangic: baslangic ?? "hemen",
379
480
  });
@@ -381,6 +482,89 @@ export function appstoreYazmaAraclari() {
381
482
  return abonelikFiyatYaz(girdi.abonelik_id, matris, koru, baslangic);
382
483
  },
383
484
  },
485
+ {
486
+ ad: "appstore__fiyat_analizi",
487
+ aciklama: "Ülkelere özel fiyat önerir (bölgesel fiyatlandırma); hiçbir şey yazmaz. Bir " +
488
+ "taban fiyattan (örn. USA 9.99) Apple'ın eşitlediği yerel fiyatı alır, ülkenin " +
489
+ "alım gücüne (Dünya Bankası fiyat seviyesi) göre yumuşatılmış bir indirim " +
490
+ "hesaplar ve en yakın geçerli Apple basamağına oturtur. Fiyatı öldürmemek için " +
491
+ "indirim sınırlı: zengin ülkelere dokunulmaz, hiçbir ülke eşit fiyatın " +
492
+ "en_dusuk_oran'ının altına inmez, hiçbir fiyat artırılmaz. Öneriyi kullanıcıya " +
493
+ "göster, isterse ülke ülke düzelt, sonra appstore__abonelik_fiyat_ayarla'ya " +
494
+ "fiyat + ozel_fiyatlar olarak ver.",
495
+ girdiSemasi: {
496
+ type: "object",
497
+ properties: {
498
+ abonelik_id: metin("appstore__abonelikler'den gelen abonelik id'si."),
499
+ fiyat: fiyatSemasi,
500
+ ulke: ulkeSemasi,
501
+ yansitma: {
502
+ type: "number",
503
+ description: "Alım gücü farkının ne kadarı fiyata yansısın, 0-1 (varsayılan 0.4). " +
504
+ "0 = indirim yok, 1 = farkın tamamı (agresif). 0.4'te Türkiye ~%27, Hindistan ~%44 iner.",
505
+ },
506
+ en_dusuk_oran: {
507
+ type: "number",
508
+ description: "Hiçbir ülke eşit fiyatın bu oranının altına inmesin (varsayılan 0.5 = en fazla %50 indirim).",
509
+ },
510
+ },
511
+ required: ["abonelik_id", "fiyat"],
512
+ },
513
+ async calistir(girdi) {
514
+ const fiyat = fiyatSayisi(girdi.fiyat);
515
+ const ulke = ulkeKodu(girdi.ulke) ?? "USA";
516
+ const yansitma = Math.min(1, Math.max(0, Number(girdi.yansitma ?? 0.4) || 0));
517
+ const taban = Math.min(1, Math.max(0.1, Number(girdi.en_dusuk_oran ?? 0.5) || 0.5));
518
+ const [matris, veri] = await Promise.all([
519
+ abonelikMatrisi(girdi.abonelik_id, fiyat, ulke),
520
+ alimGucuVerisi(),
521
+ ]);
522
+ const cipaOrani = fiyatSeviyesi(veri, ulke, matris[0]?.para) ?? 1;
523
+ // %15'ten küçük indirim gürültü: İtalya'yı %12 indirmek geliri düşürür, satışı
524
+ // pek artırmaz. Batı Avrupa ve Japonya eşit fiyatta kalır.
525
+ const adaylar = matris
526
+ .map((s) => {
527
+ const o = fiyatSeviyesi(veri, s.ulke, s.para);
528
+ if (!o || s.ulke === ulke)
529
+ return undefined;
530
+ const goreli = o / cipaOrani;
531
+ const carpan = Math.min(1, Math.max(taban, goreli ** yansitma));
532
+ return carpan <= 0.85 ? { s, goreli, carpan } : undefined;
533
+ })
534
+ .filter((x) => x !== undefined);
535
+ const noktalar = await ulkeNoktalari(girdi.abonelik_id, adaylar.map((a) => a.s.ulke));
536
+ const oneriler = [];
537
+ for (const { s, goreli, carpan } of adaylar) {
538
+ const liste = noktalar.get(s.ulke);
539
+ if (!liste?.length)
540
+ continue;
541
+ const yeni = enYakin(liste, Number(s.fiyat) * carpan);
542
+ const indirim = Math.round((1 - Number(yeni.fiyat) / Number(s.fiyat)) * 100);
543
+ if (indirim < 10)
544
+ continue;
545
+ oneriler.push({
546
+ ulke: s.ulke,
547
+ fiyat: yeni.fiyat,
548
+ indirim,
549
+ satir: `${s.ulke}: ${s.fiyat} → ${yeni.fiyat} ${s.para ?? ""} ` +
550
+ `(-%${indirim}, alım gücü ${goreli.toFixed(2)})`,
551
+ });
552
+ }
553
+ oneriler.sort((a, b) => b.indirim - a.indirim);
554
+ return {
555
+ taban: `${ulke} ${fiyat}`,
556
+ esit_kalan_ulke: matris.length - oneriler.length,
557
+ indirimli_ulke: oneriler.length,
558
+ oneriler: oneriler.map((o) => o.satir),
559
+ ozel_fiyatlar: Object.fromEntries(oneriler.map((o) => [o.ulke, o.fiyat])),
560
+ ayarlar: { yansitma, en_dusuk_oran: taban },
561
+ kaynak: "Alım gücü: Dünya Bankası, hane tüketimi PPP / resmi kur (en güncel yıl). " +
562
+ "Verisi olmayan ülkeler (ör. Tayvan) eşit fiyatta kalır.",
563
+ not: "Hiçbir şey yazılmadı. Kullanıcıya önerileri göster; onaylarsa " +
564
+ "appstore__abonelik_fiyat_ayarla'ya fiyat ve bu ozel_fiyatlar ile çağır.",
565
+ };
566
+ },
567
+ },
384
568
  {
385
569
  ad: "appstore__iap_fiyat_ayarla",
386
570
  aciklama: "Tek seferlik uygulama içi satın almanın fiyatını ayarlar. Bir ülkedeki fiyatı " +
@@ -121,7 +121,7 @@ const YAZMA_YONTEMLERI = new Set(["POST", "PATCH", "PUT", "DELETE"]);
121
121
  * çalışmaz. `:batchMigratePrices` gibi gerçek toplu yazma uçları bu listeye
122
122
  * girmesin diye ek adı tam olarak eşleştiriliyor.
123
123
  */
124
- const OKUYAN_POST_EKLERI = [":query", ":batchGet", ":search", ":lookup"];
124
+ const OKUYAN_POST_EKLERI = [":query", ":batchGet", ":search", ":lookup", ":convertRegionPrices"];
125
125
  /**
126
126
  * İkinci emniyet: ekin adı kadar operasyonun kendi açıklaması da okuma
127
127
  * demeli. Google ileride `:batchGet` gibi görünen ama yazan bir uç eklerse
@@ -129,7 +129,7 @@ const OKUYAN_POST_EKLERI = [":query", ":batchGet", ":search", ":lookup"];
129
129
  * "Reads / Queries / Searches / Gets / Lists" ile başlamıyorsa yazma sayıp
130
130
  * onay isteriz. Bugünkü 19 ucun 19'u bu kalıba uyuyor.
131
131
  */
132
- const OKUMA_OZETI = /^(read|quer|search|look ?up|get|list|fetch|retriev)/i;
132
+ const OKUMA_OZETI = /^(read|quer|search|look ?up|get|list|fetch|retriev|calculat)/i;
133
133
  function yazmaIslemiMi(op) {
134
134
  if (!YAZMA_YONTEMLERI.has(op.yontem))
135
135
  return false;
@@ -184,7 +184,8 @@ export function dispatchAraclari() {
184
184
  "yöntemini döndürür. Sonucu magaza__cagir ile çalıştırabilirsin. " +
185
185
  "Örnek sorgular: 'subscription price', 'testflight tester', 'crash rate'. " +
186
186
  "Abonelik/IAP fiyatı, abonelik kurulumu ve yaş derecelendirmesi için " +
187
- "hazır appstore__ araçları var; onları tercih et.",
187
+ "hazır appstore__ araçları; Play'de sürüm yayınlama, rollout, mağaza " +
188
+ "metni, görsel ve abonelik fiyatı için hazır play__ araçları var. Onları tercih et.",
188
189
  girdiSemasi: {
189
190
  type: "object",
190
191
  properties: {
@@ -293,6 +294,11 @@ export function dispatchAraclari() {
293
294
  ? `\nBunlar mı demek istedin?\n ${yakin.map((y) => y.ad).join("\n ")}`
294
295
  : "\nmagaza__endpoint_ara ile arayabilirsin."));
295
296
  }
297
+ // Katalogdaki yükleme uçları ham dosya baytı ister; JSON gövdeyle hep düşüyordu.
298
+ if (magaza === "play" && /\.upload[a-z]*$/i.test(op.ad)) {
299
+ throw new Error(`'${op.ad}' dosya yükler; magaza__cagir dosya gönderemez. AAB/APK için ` +
300
+ "play__surum_yayinla, ekran görüntüsü ve ikon için play__gorsel_yukle kullan.");
301
+ }
296
302
  const yazma = yazmaIslemiMi(op);
297
303
  if (yazma && magaza === "appstore")
298
304
  govdeTamamla(op, girdi);
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { cagir as appleCagir } from "../istemci/apple.js";
9
9
  import { cagir as playCagir } from "../istemci/play.js";
10
- import { yolParcasi } from "./play.js";
10
+ import { paraBicimle, yolParcasi } from "./play.js";
11
11
  import { acikMagazalar } from "./tip.js";
12
12
  const metin = (aciklama) => ({ type: "string", description: aciklama });
13
13
  /** Bir isteği çalıştırır; patlarsa hatayı sonuç olarak döndürür. */
@@ -254,11 +254,11 @@ export function ortakAraclar() {
254
254
  plan_id: p.basePlanId,
255
255
  isim: a.listings?.[0]?.title,
256
256
  sure: p.autoRenewingBasePlanType?.billingPeriodDuration ??
257
- p.prepaidBasePlanType?.billingPeriodDuration,
257
+ p.prepaidBasePlanType?.billingPeriodDuration ??
258
+ p.installmentsBasePlanType?.billingPeriodDuration,
258
259
  durum: p.state,
259
- fiyat: bolge?.price
260
- ? `${(Number(bolge.price.units || 0) + Number(bolge.price.nanos || 0) / 1e9).toFixed(2)} ${bolge.price.currencyCode}`
261
- : undefined,
260
+ // toFixed(2) JPY'de kuruş uyduruyor, TND'de (3 hane) yuvarlıyordu.
261
+ fiyat: paraBicimle(bolge?.price),
262
262
  };
263
263
  }));
264
264
  return { uygulama: uygulama.displayName, abonelikler };