@appaflytech/zeyrek-ai-mcp 0.0.50 → 0.0.52

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,236 @@
1
+ import { z } from 'zod';
2
+ import { destructive, environmentParam, guid, projectParam, readOnly, updates, writes } from './common.js';
3
+ import { applicationId } from './mobileStore.js';
4
+ import { unwrap } from './screens.js';
5
+ import { PlatformApiError } from '../platform/errors.js';
6
+ import { handle } from '../platform/responses.js';
7
+ import { GRANTS_AMOUNT_MAX, PRODUCT_KINDS, PRODUCT_PERIODS, PRODUCT_TRIALS, PURCHASE_READINESS_HINTS, PURCHASES_SHELL_VERSION, changedProductFields, mergeProduct, productFormatErrors, productLockedChanges } from '../knowledge/mobileStore.js';
8
+ /**
9
+ * Uygulama içi satın almanın ajan yolu (kök `docs/adr/0027-uygulama-ici-satin-alma.md`, `plans/mobil-uygulama-ici-satin-alma`
10
+ * g-01): ürün kataloğu (Core `mobile-store/{appId}/products`, ortam yok), iki mağazaya gönderim işi, mağaza hazırlığı ve
11
+ * Studio'nun satış listesi (Tenant `{project}/{appId}/purchases`, ortamlı, salt okunur). Gizli bilgi (Apple satın alma
12
+ * anahtarı `.p8`) ve cihazın anonim uçları (verify / entitlements / restore / bildirimler) bilinçli olarak araç DEĞİLDİR —
13
+ * ilki Studio'da insanın, ikincisi kabuğun işidir (`platform_guide('mobile-publish')` §Satın almalar).
14
+ */
15
+ /** Backend'in yalnız gönderimden sonra doğrulayabildiği hazırlık satırı (Android'de ilk ürün mağazada açılınca `ready`) */
16
+ const AFTER_FIRST_SEND_ROW = 'billingBuild';
17
+ const productKey = z.string().min(1).describe("Ürünün Zeyrek anahtarı ^[a-z][a-z0-9_]{1,63}$ ('premium_yillik'); purchase adımı ürünü bu anahtarla anar");
18
+ const sendId = guid.describe("Gönderim işinin id'si (mobile_product_send ya da mobile_product_send_status { applicationId } çıktısı)");
19
+ const productText = z.object({
20
+ name: z.string().optional().describe('Mağazadaki ad, 1–30'),
21
+ description: z.string().optional().describe('Mağazadaki açıklama, 1–45')
22
+ }).strict();
23
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ const list = (payload) => {
25
+ const data = isObject(payload) && 'data' in payload ? payload.data : payload;
26
+ return Array.isArray(data) ? data.filter(isObject) : [];
27
+ };
28
+ const storeState = (store) => isObject(store)
29
+ ? { state: store.state, storeId: store.storeId, ...(Array.isArray(store.pendingChanges) && store.pendingChanges.length > 0 ? { pendingChanges: store.pendingChanges } : {}), error: store.error, storePrice: store.storePrice, sentPriceTry: store.sentPriceTry, lastNotificationAt: store.lastNotificationAt }
30
+ : undefined;
31
+ const productSummary = (product) => ({
32
+ id: product.id,
33
+ key: product.key,
34
+ kind: product.kind,
35
+ entitlement: product.entitlement,
36
+ balance: product.balance,
37
+ grantsAmount: product.grantsAmount,
38
+ priceTry: product.priceTry,
39
+ period: product.period,
40
+ trial: product.trial,
41
+ texts: product.texts,
42
+ reviewAssetId: product.reviewAssetId,
43
+ ios: storeState(product.ios),
44
+ android: storeState(product.android),
45
+ updatedAt: product.updatedAt ?? product.createdAt
46
+ });
47
+ const steps = (value) => (Array.isArray(value) ? value : []).filter(isObject);
48
+ /**
49
+ * Düşen gönderim adımı → mağaza, adım türü, ürün, kod ve nereye bakılacağı (hata kodları b-03 `MobileStoreProductConstants.Error*`).
50
+ * Adımın `key`'i `<tür>:<ürün ya da hak>` birleşimidir (`MobileStoreProductSend.Step`); tür `kind`'dadır.
51
+ */
52
+ const failedSteps = (send) => ['ios', 'android'].flatMap(platform => steps(send[platform])
53
+ .filter(step => step.state === 'failed')
54
+ .map(step => `${platform} ${String(step.kind ?? step.key)} ${String(step.productKey ?? step.entitlement ?? '')}: ${String(step.error ?? 'store_error')}${step.detail ? ` (${String(step.detail)})` : ''} → mobile_purchase_readiness`));
55
+ function sendSummary(send, appId) {
56
+ const failed = failedSteps(send);
57
+ const active = send.state === 'queued' || send.state === 'running';
58
+ return {
59
+ id: send.id,
60
+ state: send.state,
61
+ applyToExisting: send.applyToExisting,
62
+ ios: steps(send.ios),
63
+ android: steps(send.android),
64
+ createdAt: send.createdAt,
65
+ finishedAt: send.finishedAt,
66
+ ...(failed.length > 0 ? { failedSteps: failed } : {}),
67
+ ...(active ? { next: `Gönderim sürüyor: mobile_product_send_status { applicationId: '${appId}', sendId: '${String(send.id)}' } ile yeniden okuyun (mağaza çağrıları saniyeler–dakikalar sürer).` } : {}),
68
+ ...(!active && failed.length > 0 ? { next: `Düşen adımın nedeni giderilince mobile_product_send { applicationId: '${appId}', retrySendId: '${String(send.id)}' } — yalnız bitmemiş adımlar yeniden koşar. Kodlar: paid_agreement (Apple sözleşmesi), play_permission (Play izni), play_billing_build (BILLING'li derleme yok), apple_connection / app_missing (hesap ya da mağaza uygulaması), store_error (ayrıntı detail).` } : {})
69
+ };
70
+ }
71
+ export function registerMobilePurchaseTools(server, platform) {
72
+ const config = platform.config;
73
+ server.registerTool('mobile_product_list', {
74
+ title: 'Satın alma ürünlerini listele',
75
+ description: `Mobil uygulamanın uygulama içi satın alma ürünlerini döner (GET mobile-store/{appId}/products; kök ADR 0027). Her ürün: key (Zeyrek anahtarı — purchase adımının args.value'su), kind (subscription | non_consumable | consumable), entitlement (abonelik ve tek seferlikte verdiği hak — ekranlar Entitlements.<hak> okur), balance + grantsAmount (yalnız consumable: bakiyenin adı — ekranlar Balances.<ad> okur, balance-spend adımı bu adla harcar — ve her satın almada eklenen miktar), priceTry, period, trial, texts (dil → { name, description }) ve mağaza başına durum: ios / android { state (draft · sending · ready · in_review · approved · active · failed), storeId (mağazaya gittiyse), pendingChanges (mağazaya gitmemiş değişiklik: name, description, price, period, trial), error (son gönderimin kodu), storePrice }. storeId'si olan ürünün anahtarı, türü, hakkı ve bakiye adı KİLİTLİDİR (miktar değişebilir). Katalog Core'dadır, ortam yoktur. Satın alma kabuk ${PURCHASES_SHELL_VERSION}+ ister.`,
76
+ inputSchema: { applicationId },
77
+ annotations: readOnly
78
+ }, handle(config, async ({ applicationId: appId }) => {
79
+ const products = list(await platform.org('GET', `/mobile-store/${appId}/products`));
80
+ return {
81
+ applicationId: appId,
82
+ count: products.length,
83
+ products: products.map(productSummary),
84
+ ...(products.length === 0 ? { note: 'Bu uygulamada satın alma ürünü yok — çağrı BAŞARILI, sonuç boş. Eklemek için mobile_product_upsert.' } : {})
85
+ };
86
+ }));
87
+ server.registerTool('mobile_product_upsert', {
88
+ title: 'Satın alma ürünü yaz',
89
+ description: `Ürünü anahtarıyla yazar (kök ADR 0027): katalogda yoksa açar (POST mobile-store/{appId}/products), varsa okunan ürünün üstüne birleştirip yazar (PUT …/products/{id} tam modeli yazdığı için verilmeyen alan korunur; texts dil dil ve alan alan birleşir, dil: null o dili kaldırır). Yeni üründe kind, priceTry, texts zorunlu; abonelik ve tek seferlikte entitlement, abonelikte period, tüketilende (consumable — kredi, jeton) balance ve grantsAmount da. Değişiklik yoksa yazmaz (outcome 'unchanged'). Gateway backend MobileStoreProductValidator'ın 422 kurallarını istek gitmeden uygular: anahtar ve hak ^[a-z][a-z0-9_]{1,63}$, anahtar uygulamada tekil, bir hak tek türden ürünle verilir (abonelik grubu = hak; aylık + yıllık aynı hakkı paylaşır), fiyat > 0 en çok iki ondalık TRY, abonelikte period ${PRODUCT_PERIODS.join(' | ')} ve trial ${PRODUCT_TRIALS.join(' | ')} ya da boş, tek seferlikte (non_consumable) ve tüketilende ikisi de boş, tüketilende entitlement boş, balance ^[a-z][a-z0-9_]{1,63}$ ve grantsAmount 1–${GRANTS_AMOUNT_MAX} tam sayı (öbür türlerde ikisi de boş), texts en az bir dil, ad 1–30, açıklama 1–45. Tüketilen ürün mağazada \"kredi paketi\"dir: aynı balance'ı birden çok ürün paylaşabilir (kredi_100, kredi_500 → Balances.kredi); satın alma bakiyeyi grantsAmount kadar artırır, harcama balance-spend adımıyla olur (platform_guide('mobile-steps') §Kredi harcama) ve iade o satırın miktarını geri alır; bakiye negatife düşebilir. K9: tüketilen ürün mağazada Geri yükle ile geri gelmez — kullanıcı girişi olmayan uygulamada telefon değişince bakiye gider (MOB-036, flow yazımında uyarır). Tuzak: mağazaya gitmiş ürünün (ios/android storeId) key, kind, entitlement ve balance'ı KİLİTLİDİR (backend 422 MobileStoreProductLockedException) — gateway durdurur; farklı kimlik gerekiyorsa yeni anahtarla yeni ürün açın. Yazmak mağazaya göndermez: gönderilmiş üründeki ad/fiyat/süre değişikliği pendingChanges'e düşer ve mobile_product_send ile gider. Fiyat mağazada TRY basamağına yuvarlanır (storePrice). Ürün katalogda olunca purchase adımının statik anahtarı FLOW-045'ten, balance-spend adımının statik bakiye adı FLOW-046'dan geçer.`,
90
+ inputSchema: {
91
+ applicationId,
92
+ key: productKey,
93
+ kind: z.enum(PRODUCT_KINDS).optional().describe('subscription (yenilenen abonelik) | non_consumable (tek seferlik, kalıcı hak) | consumable (tüketilen: kredi, jeton — hak değil bakiye verir; balance + grantsAmount zorunlu)'),
94
+ entitlement: z.string().min(1).optional().describe("Ürünün açtığı hak ('premium'); ekranlar Entitlements.premium okur. Aynı hakkı veren abonelikler mağazada bir grup olur. consumable'da verilmez"),
95
+ balance: z.string().min(1).optional().describe("Yalnız consumable: bakiyenin adı ^[a-z][a-z0-9_]{1,63}$ ('kredi'); ekranlar Balances.kredi okur, balance-spend adımı args.value'da bu adı kullanır. Aynı adı birden çok ürün paylaşabilir. Mağazaya gidince kilitlenir"),
96
+ grantsAmount: z.number().optional().describe(`Yalnız consumable: her satın almada bakiyeye eklenen miktar, 1–${GRANTS_AMOUNT_MAX} tam sayı (100). Gönderildikten sonra da değişebilir; eski satışlar eski miktarla kalır`),
97
+ priceTry: z.number().optional().describe('Tek TRY fiyatı (949.99); öteki ülkeler mağazanın eşlemesiyle'),
98
+ period: z.enum(PRODUCT_PERIODS).nullable().optional().describe('Yalnız abonelikte, zorunlu'),
99
+ trial: z.enum(PRODUCT_TRIALS).nullable().optional().describe('Yalnız abonelikte: ücretsiz deneme süresi; boş ya da none = deneme yok'),
100
+ texts: z.record(productText.nullable()).optional().describe("Dil kodu → { name, description } (ar, en, ru, tr mağazanın diline eşlenir); null o dili kaldırır. Ör. { tr: { name: 'Premium yıllık', description: 'Bütün özellikler' } }"),
101
+ reviewAssetId: guid.nullable().optional().describe("App Review ekran görüntüsü (mobile_asset_create kind 'reviewAttachment' id'si); Apple'a gönderimde yüklenmez, insan App Store Connect'te ekler")
102
+ },
103
+ // Anahtarla idempotent: aynı çağrının ikincisi 'unchanged' döner (organization_component_upsert emsali, 41 §6).
104
+ annotations: updates
105
+ }, handle(config, async ({ applicationId: appId, ...patch }) => {
106
+ const products = list(await platform.org('GET', `/mobile-store/${appId}/products`));
107
+ const current = products.find(product => product.key === patch.key);
108
+ const body = mergeProduct(current, patch);
109
+ const errors = productFormatErrors(body, products.filter(product => product !== current));
110
+ const locked = current ? productLockedChanges(current, body) : [];
111
+ if (locked.length > 0) {
112
+ throw new PlatformApiError(`mobile_product_upsert: '${String(patch.key)}' mağazaya gitmiş (storeId var) — ${locked.join(', ')} KİLİTLİ: mağaza ürünü ve abonelik grubunu bu kimlikle tanır. Hiçbir şey yazılmadı. Alanı yamadan çıkarın; farklı tür ya da hak gerekiyorsa yeni anahtarla yeni ürün açın.`, 422, 'MobileStoreProductLockedException');
113
+ }
114
+ if (errors.length > 0) {
115
+ throw new PlatformApiError(`mobile_product_upsert: ürün biçimsiz — backend 422 ile reddederdi, hiçbir şey yazılmadı: ${errors.map(error => `${error.field}: ${error.message}`).join(' · ')}. Var olan ürünler: mobile_product_list.`, 400, 'product_invalid');
116
+ }
117
+ const changed = current ? changedProductFields(current, body) : [];
118
+ if (current && changed.length === 0)
119
+ return { outcome: 'unchanged', product: productSummary(current), note: 'Yama kayıttaki değerlerle aynı — hiçbir şey yazılmadı.' };
120
+ const saved = unwrap(current
121
+ ? await platform.org('PUT', `/mobile-store/${appId}/products/${String(current.id)}`, { body })
122
+ : await platform.org('POST', `/mobile-store/${appId}/products`, { body }));
123
+ const summary = productSummary(saved);
124
+ const pending = ['ios', 'android'].filter(store => isObject(summary[store]) && Array.isArray(summary[store].pendingChanges));
125
+ return {
126
+ outcome: current ? 'updated' : 'created',
127
+ product: summary,
128
+ ...(current ? { changed } : {}),
129
+ note: pending.length > 0
130
+ ? `Değişiklik ${pending.join(' ve ')} mağazasına gitmedi (pendingChanges) — mobile_product_send ile gönderin; fiyat değişiminde mevcut aboneler varsayılan olarak eski fiyatta kalır (applyToExisting).`
131
+ : 'Ürün katalogda; mağazalarda açılması için mobile_purchase_readiness ile hazırlığa bakıp mobile_product_send ile gönderin.'
132
+ };
133
+ }));
134
+ server.registerTool('mobile_product_delete', {
135
+ title: 'Satın alma ürününü sil',
136
+ description: "Ürünü anahtarıyla siler (DELETE mobile-store/{appId}/products/{id}; gateway anahtarı id'ye çözer). Yalnız mağazaya hiç gitmemiş ürün silinir: storeId'si olan ürün 409 MobileStoreProductSentException — mağazadaki ürün API'den silinemez (Apple ürün kimliğini bir daha kullandırmaz); satıştan kaldırmak App Store Connect / Play Console'da insanın işidir. Silinen ürünü anan purchase adımı cihazda 'invalid' döner: o akış bir sonraki yazımda FLOW-045 ile durur (tüketilen ürünün bakiyesini anan balance-spend adımı FLOW-046 ile), mağaza ön kontrolü P-23 (purchases_unknown_product) uyarır.",
137
+ inputSchema: { applicationId, key: productKey },
138
+ annotations: destructive
139
+ }, handle(config, async ({ applicationId: appId, key }) => {
140
+ const products = list(await platform.org('GET', `/mobile-store/${appId}/products`));
141
+ const product = products.find(entry => entry.key === key);
142
+ if (!product) {
143
+ throw new PlatformApiError(`mobile_product_delete: '${key}' anahtarlı ürün bu uygulamada yok — hiçbir şey silinmedi. Ürünler: ${products.map(entry => String(entry.key)).join(', ') || '(hiç ürün yok)'}.`, 404);
144
+ }
145
+ await platform.org('DELETE', `/mobile-store/${appId}/products/${String(product.id)}`);
146
+ return { applicationId: appId, deleted: key, productId: product.id };
147
+ }));
148
+ server.registerTool('mobile_product_send', {
149
+ title: 'Ürünleri mağazalara gönder',
150
+ description: `Katalogdaki ürünleri App Store ve Google Play'e gönderen işi başlatır (POST mobile-store/{appId}/products/send → 202; kök ADR 0027): mağazada hiç olmayan ürünü açar (Apple: abonelik grubu = hak, abonelik ya da tek seferlik ürün, metin, TUR fiyat basamağı, deneme; Google: abonelik = hak, temel plan = ürün anahtarı '_'→'-', deneme teklifi, tek seferlik ürün), pendingChanges taşıyan ürünü günceller, son gönderimde düşmüş ürünü yeniden dener. Hesabı bağlı olmayan mağaza atlanır. İş asenkrondur: mobile_product_send_status ile izleyin. applyToExisting: fiyat değişiminde mevcut abonelere de uygula (varsayılan false — mevcut aboneler eski fiyatta kalır, K3). retrySendId yarıda kalan ya da düşen gönderimi yeniden dener (POST …/sends/{id}/retry; yalnız bitmemiş adımlar koşar, ilk gönderimin applyToExisting seçimiyle); applyToExisting: true ile birlikte verilmez. Hatalar: 409 MobileStoreProductSendRunningException (etkin gönderim var — 15 dakikadan eski asılı iş yeniden denenebilir), 422 MobileStoreProductNothingToSendException (gönderilecek ürün yok). GERİ ALINAMAZ: mağazada açılan ürün API'den silinemez ve Apple ürün kimliğini yeniden kullandırmaz — göndermeden önce anahtarları ve fiyatları kullanıcıya onaylatın; önce mobile_purchase_readiness. Apple ilk satın alma ürününü uygulamanın yeni sürümüyle inceler (durum ready → in_review); Google ürün açmayı faturalandırma izinli derleme yüklenmeden reddeder (play_billing_build). Cihazda satın alma kabuk ${PURCHASES_SHELL_VERSION}+ ile mağaza derlemesi ister.`,
151
+ inputSchema: {
152
+ applicationId,
153
+ applyToExisting: z.boolean().optional().describe('true: fiyat değişimi mevcut abonelere de uygulanır (mağaza kullanıcıyı bilgilendirir). Varsayılan false'),
154
+ retrySendId: sendId.optional().describe('Bu gönderimi yeniden dener; verilince yeni gönderim açılmaz (applyToExisting: true ile birlikte verilmez)')
155
+ },
156
+ annotations: { ...writes, destructiveHint: true }
157
+ }, handle(config, async ({ applicationId: appId, applyToExisting, retrySendId }) => {
158
+ if (retrySendId !== undefined && applyToExisting === true) {
159
+ throw new PlatformApiError("mobile_product_send: retrySendId ile applyToExisting birlikte verilmez — yeniden deneme gönderimin ilk seçimini kullanır. Birini bırakın; hiçbir şey gönderilmedi.", 400);
160
+ }
161
+ const send = unwrap(retrySendId
162
+ ? await platform.org('POST', `/mobile-store/${appId}/products/sends/${retrySendId}/retry`)
163
+ : await platform.org('POST', `/mobile-store/${appId}/products/send`, { body: { applyToExisting: applyToExisting ?? false } }));
164
+ return { ...sendSummary(send, appId), next: `Gönderim kuyrukta: mobile_product_send_status { applicationId: '${appId}', sendId: '${String(send.id)}' } ile izleyin.` };
165
+ }));
166
+ server.registerTool('mobile_product_send_status', {
167
+ title: 'Ürün gönderiminin durumu',
168
+ description: "sendId verilirse o gönderimin durumunu döner (GET mobile-store/{appId}/products/sends/{id}): state (queued | running | completed | failed), applyToExisting, mağaza başına adımlar ios[] / android[] { key ('<kind>:<ürün ya da hak>'), kind (notification_url · group · product · texts · price · trial · base_plan · activate), productKey, entitlement, state (pending | done | failed | skipped), storeRef, error, detail }; düşen adımlar failedSteps'te kod ve öneriyle ('android product premium_yillik: play_permission (403) → mobile_purchase_readiness'), next sıradaki çağrıyı söyler. sendId verilmezse son 5 gönderimin özetini döner (GET …/sends). Ürünün kendi mağaza durumu: mobile_product_list.",
169
+ inputSchema: { applicationId, sendId: sendId.optional() },
170
+ annotations: readOnly
171
+ }, handle(config, async ({ applicationId: appId, sendId: id }) => {
172
+ if (id)
173
+ return sendSummary(unwrap(await platform.org('GET', `/mobile-store/${appId}/products/sends/${id}`)), appId);
174
+ const sends = list(await platform.org('GET', `/mobile-store/${appId}/products/sends`));
175
+ return {
176
+ applicationId: appId,
177
+ count: sends.length,
178
+ sends: sends.map(send => ({ id: send.id, state: send.state, applyToExisting: send.applyToExisting, failedSteps: failedSteps(send).length, createdAt: send.createdAt, finishedAt: send.finishedAt })),
179
+ ...(sends.length === 0 ? { note: 'Bu uygulamada hiç ürün gönderimi yok — mobile_product_send.' } : {})
180
+ };
181
+ }));
182
+ server.registerTool('mobile_purchase_readiness', {
183
+ title: 'Satın alma mağaza hazırlığı',
184
+ description: `İki mağazanın satın almaya hazırlığını satır satır döner (GET mobile-store/{appId}/purchases/readiness; Studio › Satın almalar › Mağaza hazırlığı ile aynı liste). ios: connection (Apple hesabı) · paidAgreement (ücretli uygulama sözleşmesi, elle) · iapKey (App Store Server API anahtarı .p8) · notificationUrl (App Store Server Notifications V2 adresi, copyValue); android: connection (Play hizmet hesabı) · merchant (ödeme profili, elle) · billingBuild (BILLING izinli derleme yüklendi mi) · permissions (hizmet hesabının ürün izni) · rtdnTopic (gerçek zamanlı bildirim Pub/Sub, copyValue). state: ready · missing · fix · manual (konsolda insanın yaptığı, otomatik doğrulanamaz). Hazır olmayan her satırda hint ne yapılacağını ve kimin yaptığını söyler. ready mağaza başınadır ({ ios, android } — hesabı bağlı olmayan mağaza gönderimde atlanır, öbürünü bekletmez); attention eksik/düzeltilecek satırları sayar. android.billingBuild ilk Android ürünü mağazada açılınca ready olur — ondan önce afterFirstSend'de durur, gönderimi engellemez. Gizli bilgi (hesaplar, .p8) MCP'den YAZILMAZ — Studio'da insan yükler. Mağaza dışı ön koşul: kabuk ${PURCHASES_SHELL_VERSION}+ (ön kontrol P-23).`,
185
+ inputSchema: { applicationId },
186
+ annotations: readOnly
187
+ }, handle(config, async ({ applicationId: appId }) => {
188
+ const readiness = unwrap(await platform.org('GET', `/mobile-store/${appId}/purchases/readiness`));
189
+ // Öneri yalnız hazır olmayan satıra eklenir: hazır satırda "bağlı değil" metni yanlış okunur (lokal ölçüm 2026-10-23).
190
+ const rows = (store) => steps(readiness[store]).map(row => row.state === 'ready' ? row : { ...row, hint: PURCHASE_READINESS_HINTS[`${store}.${String(row.key)}`] });
191
+ // Backend `billingBuild`'i ancak bir Android ürünü mağazada açılınca `ready` yapar (`MobileStorePurchaseReadinessService`):
192
+ // ilk gönderimden önce kapı sayılırsa ajan "önce hazırlık" ile gönderim arasında döner. Ayrı listelenir, kapıya girmez.
193
+ const pending = (store, row) => store === 'android' && row.key === AFTER_FIRST_SEND_ROW && row.state === 'missing';
194
+ const open = (store) => rows(store).filter(row => (row.state === 'missing' || row.state === 'fix') && !pending(store, row));
195
+ const attention = ['ios', 'android'].flatMap(store => open(store).map(row => `${store}.${String(row.key)} ${String(row.state)}`));
196
+ const afterFirstSend = rows('android').filter(row => pending('android', row)).map(row => `android.${String(row.key)}`);
197
+ return {
198
+ applicationId: appId,
199
+ ready: { ios: open('ios').length === 0, android: open('android').length === 0 },
200
+ attention,
201
+ ...(afterFirstSend.length > 0 ? { afterFirstSend, afterFirstSendNote: "Bu satır ilk Android ürünü mağazada açılınca ready olur; gönderimi engellemez. Google ürün açmayı BILLING izinli derleme yüklenmeden reddeder (play_billing_build): derleme yüklendiyse gönderin, düşerse derleme yükleyip retrySendId." } : {}),
202
+ ios: rows('ios'),
203
+ android: rows('android')
204
+ };
205
+ }));
206
+ server.registerTool('mobile_purchase_list', {
207
+ title: 'Satışları listele',
208
+ description: "Uygulamanın satın almalarını ve sayaçları döner (Studio › Satın almalar › Satışlar; Tenant GET {project}/{appId}/purchases + /purchases/summary, salt okunur). Kayıt ortamın şemasındadır: mağaza uygulaması production'a yazar (gerçek satışlar environment: 'production'); development'ta yalnız deneme/yerel doğrulama satırları vardır. items[] { id, userEmail (oturumlu satın almada), installation (kurulum kimliğinin ilk 8 karakteri), entitlement, productKey, productName, productKind, store (ios | android), state (active · trial · grace · cancelled_active · owned açık; expired · refunded kapalı), environment (real | test — TestFlight / Play iç test), nextKind (renews · trial_ends · ends · grace_until · closed_at) + nextAt, purchasedAt }; sayfa 50, nextCursor bir sonraki sayfanın cursor'ı (null = son). summary { activeSubscriptions, inTrial, oneTimeThisMonth (ay sınırı UTC), ownerless (açık sahipsiz satın alma — mağazadan taşınan eski abone) }; satır owner (ownerless · installation · account). Test satın almaları includeTest: true olmadan görünmez. Apple yenileme zinciri tek satırdır. Hak elle verilmez, iade başlatılmaz.",
209
+ inputSchema: {
210
+ applicationId,
211
+ project: projectParam,
212
+ environment: environmentParam.describe("Satışların okunacağı ortam; gerçek mağaza satışları 'production'. Boşsa DEFAULT_ENVIRONMENT"),
213
+ type: z.enum(['all', ...PRODUCT_KINDS]).optional().describe('Süzgeç; varsayılan all'),
214
+ owner: z.enum(['all', 'ownerless', 'installation', 'account']).optional().describe('Sahiplik süzgeci: ownerless (mağazadan taşınan eski abone, cihaz geri yükleyene kadar kimse görmez) · installation · account; varsayılan all'),
215
+ includeTest: z.boolean().optional().describe('true: TestFlight / Play iç test satın almaları da gelir'),
216
+ cursor: z.string().min(1).optional().describe('Önceki yanıtın nextCursor değeri')
217
+ },
218
+ annotations: readOnly
219
+ }, handle(config, async ({ applicationId: appId, project, environment, type, owner, includeTest, cursor }) => {
220
+ const [page, summary] = await Promise.all([
221
+ platform.tenant('GET', project, `/${appId}/purchases`, { environment, query: { ...(type ? { type } : {}), ...(owner ? { owner } : {}), ...(includeTest !== undefined ? { includeTest } : {}), ...(cursor ? { cursor } : {}) } }),
222
+ platform.tenant('GET', project, `/${appId}/purchases/summary`, { environment, query: { ...(includeTest !== undefined ? { includeTest } : {}) } })
223
+ ]);
224
+ const data = unwrap(page);
225
+ const items = steps(data.items);
226
+ return {
227
+ applicationId: appId,
228
+ environment: platform.resolveEnvironment(environment),
229
+ summary: unwrap(summary),
230
+ count: items.length,
231
+ items,
232
+ nextCursor: data.nextCursor ?? null,
233
+ ...(items.length === 0 ? { note: "Bu ortamda (süzgeçle) satış yok — çağrı BAŞARILI. Gerçek satışlar production'dadır; test satın almaları includeTest: true ister." } : {})
234
+ };
235
+ }));
236
+ }
@@ -20,7 +20,7 @@ import { APPLE_RELEASE_TYPES, ASSET_KINDS, BUILD_ASSET_KINDS, COLOR_PATTERN, DEC
20
20
  * Yayın'dan insan yapar (`docs/adr/0002-mobil-yayin-gorsel-ve-beyan.md`). Uçlar Core DB'dedir (ortam
21
21
  * kavramı yok); derlenen uygulama işin ortamını okur — boşsa production (backend ADR 0032).
22
22
  */
23
- const applicationId = guid.describe("Mobil uygulamanın id'si (application_list / mobile_app_create çıktısı). Slug verilirse DEFAULT_PROJECT içinde çözülür");
23
+ export const applicationId = guid.describe("Mobil uygulamanın id'si (application_list / mobile_app_create çıktısı). Slug verilirse DEFAULT_PROJECT içinde çözülür");
24
24
  const buildId = guid.describe("Derleme işinin id'si (mobile_build_start ya da mobile_build_status { applicationId } çıktısı)");
25
25
  const mobilePlatform = z.enum(['ios', 'android']);
26
26
  /**
@@ -0,0 +1,173 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename, extname, resolve } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { destructive, environmentParam, guid, projectParam, readOnly, writes } from './common.js';
5
+ import { applicationId } from './mobileStore.js';
6
+ import { unwrap } from './screens.js';
7
+ import { PlatformApiError } from '../platform/errors.js';
8
+ import { handle } from '../platform/responses.js';
9
+ import { PURCHASE_IMPORT_MAX_BYTES, STORE_DIFF_CHOICES, STORE_IMPORT_SECTIONS, diffOutline, importReportOutline, purchaseImportOutline } from '../knowledge/storeImport.js';
10
+ /**
11
+ * Yayındaki uygulamayı mağazalardan Zeyrek'e taşımanın ajan yolu (kök `docs/adr/0028-magazadan-tasima-saklanan-magaza-kimligi.md`,
12
+ * `plans/mobil-magazadan-tasima` g-01): içe aktarma (Core `mobile-store/{appId}/imports`), eski abone listesinin toplu
13
+ * doğrulaması (Tenant `{project}/{appId}/purchases/imports`, ortamlı), Apple bildirim adresinin Zeyrek'e çevrilip eski
14
+ * sunucuya iletilmesi ve yayın öncesi mağaza farkı. Kurallar MOB-037…MOB-041 (`docs/08-kural-envanteri.md`).
15
+ */
16
+ const importId = guid.describe("İçe aktarma kaydının id'si (mobile_store_import_start ya da mobile_store_import_report çıktısı)");
17
+ export function registerMobileStoreImportTools(server, platform) {
18
+ const config = platform.config;
19
+ server.registerTool('mobile_store_import_start', {
20
+ title: 'Mağazadan içe aktarmayı başlat',
21
+ description: "Yayındaki uygulamanın App Store ve Google Play kaydını OKUMAYA başlar (POST mobile-store/{appId}/imports; ADR 0028). Önce bağlantı eşzamanlı doğrulanır (connection { ios: found | not_found | no_access, android: … }, upload anahtarı uyarısı), sonra arka plan işi iki mağazayı okur ve üç kovalı rapor yazar (Gelir · Sen yüklersin · Mağazada kalır). Okuma mağazaya HİÇBİR ŞEY yazmaz, Zeyrek'te de hiçbir şey değişmez — değişiklik yalnız mobile_store_import_apply ile olur. Yalnız ilk derlemeden ÖNCE çalışır: ilk derleme kimliği ve sayaçları kilitler (409 MobileStoreImportLockedException → MOB-037: alanları elle doldurun). Yeni okuma açık eski kaydı vazgeçilmiş sayar. Sonra mobile_store_import_report ile durumu okuyun (state queued → reading → ready).",
22
+ inputSchema: {
23
+ applicationId,
24
+ bundleId: z.string().min(1).optional().describe("App Store'daki Bundle ID ('com.anatoli.app'); yalnız Android ise verilmez"),
25
+ packageName: z.string().min(1).optional().describe("Google Play paket adı ('com.anatoli.app'); yalnız iOS ise verilmez")
26
+ },
27
+ annotations: writes
28
+ }, handle(config, async ({ applicationId: appId, bundleId, packageName }) => {
29
+ if (!bundleId && !packageName) {
30
+ throw new PlatformApiError('mobile_store_import_start: bundleId ya da packageName verin — en az bir mağaza okunur. İstek gönderilmedi.', 400, 'store_identity_required');
31
+ }
32
+ try {
33
+ return unwrap(await platform.org('POST', `/mobile-store/${appId}/imports`, { body: { bundleId, packageName } }));
34
+ }
35
+ catch (error) {
36
+ if (error instanceof PlatformApiError && error.status === 409) {
37
+ throw new PlatformApiError(`${error.message} — MOB-037: ilk derleme Bundle ID / paket adı ve sayaçları kilitledi; içe aktarma artık yapılamaz. Mağaza bilgilerini mobile_store_update ile elle doldurun.`, 409, error.code);
38
+ }
39
+ throw error;
40
+ }
41
+ }));
42
+ server.registerTool('mobile_store_import_report', {
43
+ title: 'İçe aktarma raporunu oku',
44
+ description: `İçe aktarma kaydını okur (GET mobile-store/{appId}/imports/current ya da …/{importId}). Varsayılan yanıt yalnız özettir: state, okuma ilerlemesi, kova sayaçları (auto · upload · kept · issues), sorunlu satırlar (too_long, store_conflict, locale_unmapped, size_invalid, device_class_unmapped, store_managed…) ve ürün eşleme önerileri. section verilirse o bölümün bütün satırları gelir (${STORE_IMPORT_SECTIONS.join(' · ')}). Rapor kişisel veri taşımaz. Seçimler mobile_store_import_mapping ile yazılır.`,
45
+ inputSchema: {
46
+ applicationId,
47
+ importId: importId.optional(),
48
+ section: z.enum(STORE_IMPORT_SECTIONS).optional().describe('Bütün satırları istenen bölüm; verilmezse yalnız özet')
49
+ },
50
+ annotations: readOnly
51
+ }, handle(config, async ({ applicationId: appId, importId: id, section }) => {
52
+ const record = unwrap(await platform.org('GET', id ? `/mobile-store/${appId}/imports/${id}` : `/mobile-store/${appId}/imports/current`));
53
+ return importReportOutline(record, section);
54
+ }));
55
+ server.registerTool('mobile_store_import_mapping', {
56
+ title: 'İçe aktarma seçimlerini kaydet',
57
+ description: "Raporun Zeyrek'e nasıl yazılacağını kaydeder (PUT mobile-store/{appId}/imports/{importId}/mapping; seçimler birleşmez, verilen sözlük kaydı değiştirir — önce mobile_store_import_report ile okuyun). Anahtarlar: scheme (ZORUNLU — URL şeması mağazadan okunamaz, eski uygulamanınki girilir; MOB-041/P-31) · choice.<satır> = apple | play | custom ve custom.<satır> (iki mağazada farklı metin, K6) · skip.<satır> / skip.language.<dil> = true · target.language.<mağaza>.<dil> = <Zeyrek dili> (eşlenemeyen dilin metnini o dile yazar; eşlenmiş satırı ezmez) · product.<i>.{key, entitlement, period, trial, price, skip} (i = rapordaki ürün sırası; anahtar ve hak ^[a-z][a-z0-9_]{1,63}$) · declaration.<anahtar> = true. Taslak kural ihlaliyle de saklanır: ihlaller yanıtın violations listesinde [{ row, field, code }] döner (choice_required, period_required, price_required, key_taken…) ve mobile_store_import_apply onlar düzelmeden 422 verir. İçe aktarılan ürünün MAĞAZA KİMLİĞİ seçilmez — mağazadan gelir ve kilitlidir (MOB-038).",
58
+ inputSchema: {
59
+ applicationId,
60
+ importId,
61
+ mapping: z.record(z.string()).describe("Seçim sözlüğü — ör. { scheme: 'anatoli', 'choice.listing.tr.title': 'apple', 'product.0.key': 'premium_yillik', 'product.0.entitlement': 'premium' }")
62
+ },
63
+ annotations: writes
64
+ }, handle(config, async ({ applicationId: appId, importId: id, mapping }) => {
65
+ if (!mapping.scheme?.trim()) {
66
+ throw new PlatformApiError("mobile_store_import_mapping: 'scheme' zorunlu — URL şeması mağazadan okunamaz; eski uygulamanın şemasını girin (MOB-041). İstek gönderilmedi.", 400, 'scheme_required');
67
+ }
68
+ return importReportOutline(unwrap(await platform.org('PUT', `/mobile-store/${appId}/imports/${id}/mapping`, { body: { mapping } })));
69
+ }));
70
+ server.registerTool('mobile_store_import_apply', {
71
+ title: 'İçe aktarmayı uygula',
72
+ description: "Onaylanan raporu TEK İŞLEMDE Zeyrek'e yazar (POST mobile-store/{appId}/imports/{importId}/apply): mağaza ayarı, beyanlar (kaynak import), görseller, ürün kataloğu (saklanan mağaza kimlikleriyle, değişiklik beklemez) ve kimlik kilidi; mağaza tabanı farka kaydedilir. Mağazaya HİÇBİR ŞEY gönderilmez. Uygulanmış kayıt yeniden uygulanmaz (409). Sonra: mobile_product_list (içe aktarılan ürünler), yayından önce mobile_store_diff ve ön kontrol P-24…P-31.",
73
+ inputSchema: { applicationId, importId },
74
+ annotations: writes
75
+ }, handle(config, async ({ applicationId: appId, importId: id }) => importReportOutline(unwrap(await platform.org('POST', `/mobile-store/${appId}/imports/${id}/apply`)))));
76
+ server.registerTool('mobile_purchase_import', {
77
+ title: 'Eski abone listesini doğrula',
78
+ description: `Eski sistemin abone listesini (CSV) Zeyrek'e alır (Tenant POST {project}/{appId}/purchases/imports, multipart). Sütunlar: store (ios | android), transactionId ya da purchaseToken, productId (Google'da zorunlu — mağaza ürün kimliği), isteğe bağlı legacyUserId, email. Sunucu HER satırı mağazaya sorar (Apple App Store Server API, Google Play); dosyanın söylediğine güvenmez. E-posta tek proje kullanıcısıyla eşleşirse satın alma hesaba, yoksa SAHİPSİZ yazılır (MOB-039). dryRun varsayılan TRUE: yalnız sorar, yazmaz — gerçek koşu açık dryRun: false ister. Gerçek satışlar production ortamındadır. Dosya ≤ 5 MB, ≤ 100.000 satır; iş arka planda koşar — mobile_purchase_import_status ile izleyin. Apple satırları satın alma anahtarı (.p8) ister.`,
79
+ inputSchema: {
80
+ applicationId,
81
+ project: projectParam,
82
+ environment: environmentParam.describe("Abonelerin yazılacağı ortam; gerçek aboneler 'production'"),
83
+ file: z.string().min(1).describe('Gateway sürecinin makinesindeki .csv dosyasının yolu'),
84
+ dryRun: z.boolean().optional().describe('Varsayılan true: yalnız mağazaya sorar, satır yazmaz. Gerçek koşu için açıkça false')
85
+ },
86
+ annotations: writes
87
+ }, handle(config, async ({ applicationId: appId, project, environment, file, dryRun }) => {
88
+ const absolute = resolve(file);
89
+ if (extname(absolute).toLowerCase() !== '.csv') {
90
+ throw new PlatformApiError(`mobile_purchase_import: file bir .csv dosyası olmalı (${absolute}). İstek gönderilmedi.`, 400, 'file_invalid');
91
+ }
92
+ const size = (await stat(absolute).catch(() => undefined))?.size;
93
+ if (size === undefined)
94
+ throw new PlatformApiError(`mobile_purchase_import: dosya okunamadı — ${absolute}. Yol gateway sürecinin çalıştığı makineye göre çözülür.`, 400, 'file_invalid');
95
+ if (size > PURCHASE_IMPORT_MAX_BYTES)
96
+ throw new PlatformApiError(`mobile_purchase_import: dosya ${size} B, 5 MB sınırının üstünde. Listeyi bölün. İstek gönderilmedi.`, 400, 'file_too_large');
97
+ const effectiveDryRun = dryRun !== false;
98
+ const response = unwrap(await platform.tenant('POST', project, `/${appId}/purchases/imports`, {
99
+ environment,
100
+ multipart: [
101
+ { name: 'File', filename: basename(absolute), contentType: 'text/csv', content: await readFile(absolute) },
102
+ { name: 'DryRun', value: effectiveDryRun ? 'true' : 'false' }
103
+ ]
104
+ }));
105
+ return {
106
+ ...purchaseImportOutline(response),
107
+ dryRun: effectiveDryRun,
108
+ note: effectiveDryRun
109
+ ? 'dryRun: hiçbir satın alma yazılmayacak. Sonuçları eski sistemle karşılaştırın; doğruysa aynı dosyayla dryRun: false (MOB-039).'
110
+ : 'Gerçek koşu: satın almalar yazılıyor. Aynı dosyanın ikinci koşusu already_known döner.'
111
+ };
112
+ }));
113
+ server.registerTool('mobile_purchase_import_status', {
114
+ title: 'Abone listesi işinin durumu',
115
+ description: "importId verilirse işin ilerlemesini ve sonuç sayfasını döner (GET {project}/{appId}/purchases/imports/{id}?page=): progress { state (queued · running · completed · failed), total, processed, outcomes (created_ownerless · created_account · already_known · expired · refunded · product_unknown · not_found · pending · invalid_signature · invalid_row), error (iap_key_missing | iap_key_invalid) } ve 100'lük results[] { line, store, id, outcome, state, productKey, owner, email (maskeli), note }. importId verilmezse son 10 iş. Yüklenen dosya iş bitince silinir.",
116
+ inputSchema: {
117
+ applicationId,
118
+ project: projectParam,
119
+ environment: environmentParam,
120
+ importId: guid.optional().describe('İşin id’si (mobile_purchase_import çıktısı)'),
121
+ page: z.number().int().min(0).optional().describe('Sonuç sayfası (100 satır), 0’dan')
122
+ },
123
+ annotations: readOnly
124
+ }, handle(config, async ({ applicationId: appId, project, environment, importId: id, page }) => {
125
+ if (!id)
126
+ return { imports: unwrap(await platform.tenant('GET', project, `/${appId}/purchases/imports`, { environment })) };
127
+ return purchaseImportOutline(unwrap(await platform.tenant('GET', project, `/${appId}/purchases/imports/${id}`, { environment, query: page ? { page } : undefined })));
128
+ }));
129
+ server.registerTool('mobile_store_notification_forwarding', {
130
+ title: 'Apple bildirim iletimi',
131
+ description: "Apple'ın sunucu bildirim adresini ve eski sunucuya iletimi yönetir (ADR 0028, K1). action: get → durum (Tenant GET {project}/{appId}/purchases/notification-forwarding: active, until, eski adreslerin ana makinesi, delivered · pending · deadLettered, lastError) · switch → Apple'ın üretim + sandbox adresini Zeyrek'e ÇEVİRİR, eski adresleri saklar, forwardDays (varsayılan 90) boyunca her bildirimi aynen eski sunucuya iletir (Core POST mobile-store/{appId}/purchases/notification-url/switch) · revert → saklanan eski adresleri Apple'a GERİ yazar · stop → iletimi durdurur (adresler geri alma için kalır). switch ve revert APPLE'A YAZAR: kullanıcının açık onayı olmadan çağrılmaz, confirm: true ister (MOB-040). Google'da iletim gerekmez: aynı Pub/Sub konusuna Zeyrek için ikinci abonelik (insan, Google Cloud).",
132
+ inputSchema: {
133
+ applicationId,
134
+ project: projectParam,
135
+ environment: environmentParam,
136
+ action: z.enum(['get', 'switch', 'revert', 'stop']).describe('get okur; switch / revert Apple\'a yazar; stop iletimi durdurur'),
137
+ forwardDays: z.number().int().min(0).max(365).optional().describe('switch: iletim süresi (gün), varsayılan 90'),
138
+ confirm: z.boolean().optional().describe('switch / revert / stop için true — kullanıcı onayladı')
139
+ },
140
+ annotations: destructive
141
+ }, handle(config, async ({ applicationId: appId, project, environment, action, forwardDays, confirm }) => {
142
+ if (action === 'get')
143
+ return unwrap(await platform.tenant('GET', project, `/${appId}/purchases/notification-forwarding`, { environment }));
144
+ if (confirm !== true) {
145
+ throw new PlatformApiError(`mobile_store_notification_forwarding: '${action}' Apple bildirim adresini ya da iletimi değiştirir — kullanıcıya eski adresin ${action === 'switch' ? `${forwardDays ?? 90} gün iletileceğini` : 'durumunu'} söyleyip onay alın, sonra confirm: true ile çağırın (MOB-040). İstek gönderilmedi.`, 400, 'confirm_required');
146
+ }
147
+ if (action === 'switch')
148
+ return unwrap(await platform.org('POST', `/mobile-store/${appId}/purchases/notification-url/switch`, { body: { forwardDays: forwardDays ?? 90 } }));
149
+ if (action === 'revert')
150
+ return unwrap(await platform.org('POST', `/mobile-store/${appId}/purchases/notification-url/revert`));
151
+ return unwrap(await platform.org('DELETE', `/mobile-store/${appId}/purchases/notification-forwarding`));
152
+ }));
153
+ server.registerTool('mobile_store_diff', {
154
+ title: 'Yayın öncesi mağaza farkı',
155
+ description: `Yayından hemen önce mağazayı yeniden okur ve üç yollu karşılaştırır: taban (Zeyrek'in mağazada son bıraktığı) · Zeyrek · mağaza (ADR 0028; Core mobile-store/{appId}/store-diff). action: read → yeniden okur (POST) · get → son farkı okur (GET …/current) · choose → çakışmaları seçer (PUT …/{diffId}/choices, choices: { '<platform>:<alan>': ${STORE_DIFF_CHOICES.join(' | ')} }). Satır durumu: unchanged · zeyrek_changed (gönderilir) · store_changed (konsolda elle değişmiş) · both_changed. store_changed / both_changed seçilmeden içe aktarılmış ya da tabanı olan uygulamada mağaza yayını BAŞLAMAZ (409 store_diff_unresolved; fark 30 dk'dan eskiyse ya da ayar değiştiyse store_diff_stale → yeniden read) — MOB-041. 'store' seçimi mağazadaki değeri ZEYREK'E yazar. Varsayılan yanıt yalnız değişen ve çakışan satırlar; send { ios, android } yayının göndereceği alan listesi.`,
156
+ inputSchema: {
157
+ applicationId,
158
+ action: z.enum(['read', 'get', 'choose']),
159
+ diffId: guid.optional().describe('choose için farkın id’si'),
160
+ choices: z.record(z.enum(STORE_DIFF_CHOICES)).optional().describe("choose: '<platform>:<alan>' → zeyrek | store, ör. { 'ios:listing.tr.description': 'store' }")
161
+ },
162
+ annotations: writes
163
+ }, handle(config, async ({ applicationId: appId, action, diffId, choices }) => {
164
+ if (action === 'read')
165
+ return diffOutline(unwrap(await platform.org('POST', `/mobile-store/${appId}/store-diff`)));
166
+ if (action === 'get')
167
+ return diffOutline(unwrap(await platform.org('GET', `/mobile-store/${appId}/store-diff/current`)));
168
+ if (!diffId || !choices || Object.keys(choices).length === 0) {
169
+ throw new PlatformApiError("mobile_store_diff: choose için diffId ve en az bir seçim verin (choices: { 'ios:listing.tr.description': 'zeyrek' | 'store' }). İstek gönderilmedi.", 400, 'choices_required');
170
+ }
171
+ return diffOutline(unwrap(await platform.org('PUT', `/mobile-store/${appId}/store-diff/${diffId}/choices`, { body: { choices } })));
172
+ }));
173
+ }
@@ -60,15 +60,15 @@ export function normalizeGapReport(input) {
60
60
  const guideRouting = {
61
61
  'getting-started': "boş org'da sıfırdan kurulum sırası",
62
62
  'modify-existing': 'mevcut yapıyı bozmadan değiştirme disiplini',
63
- workflow: 'DTO-önce akışı, branch bağlama, tip×step matrisi',
63
+ workflow: 'DTO-önce akışı, branch bağlama, tip×step matrisi, balance adımı (bakiye/hak: krediyi düş, işi yap, düşerse geri ekle; FLOW-046)',
64
64
  microservice: 'kaynak seçimi (table > query), selectFields disiplini',
65
65
  microfunction: 'kendi runtime\'ında koşan Node/Python fonksiyonu: protokol, yayın döngüsü, step kullanımı',
66
66
  entity: "alan tipleri, ilişkiler, hazır entity'ler",
67
67
  page: 'sayfa/flow işleri; prop\'u veri kaynağına bağlama (connectData, Kural 1c-3); flex düzen prop grubu ve boyut modları (Kural 1f-2)',
68
68
  layout: 'yerleşim kabuğu: tek slot, views, dar ekranda yığılma, widthType fill/fixed/auto',
69
69
  mobile: "mobil UYGULAMA (type:'mobile'): kurulum sırası (mobile_app_create), flex-only yerleşim, tipli stil, sekmeler (properties.mobile.tabs) ve çekmece, sayfa mount/focus olayları, MOB-* bulguları, page_screenshot device",
70
- 'mobile-steps': "mobil uygulamada cihaza dokunan akış adımları (redirect, alert, confirm, download, storage, setcookie, share, haptic, location, notification-*, permission, open-settings, back, ad-show, scan-code, biometric-auth): mobildeki davranış, args sözleşmesi, Steps.<adım> sonucu; bildirim, izin, reklam, biyometri ve konum tarifleri; FLOW-029/030, MOB-024/026/027",
71
- 'mobile-publish': "mobil uygulamanın mağaza yayını: altı adımlı yolculuk, mobile_store_* / mobile_build_* akışı, ön kontrol P-01…P-22 (push kimlikleri, izinler, reklam, kod tarama, App Review), yayın hedefleri (yalnız dryRun), iki tür güncelleme, derleme hata kodları, insanın Studio'da yaptıkları (test kullanıcıları dahil)",
70
+ 'mobile-steps': "mobil uygulamada cihaza dokunan akış adımları (redirect, alert, confirm, download, storage, setcookie, share, haptic, location, notification-*, permission, open-settings, back, ad-show, scan-code, biometric-auth, purchase, restore-purchases, balance-spend): mobildeki davranış, args sözleşmesi, Steps.<adım> sonucu; bildirim, izin, reklam, biyometri, satın alma (Entitlements/Products, teklif ekranı), kredi harcama (Balances, balance-spend) ve konum tarifleri; FLOW-029/030/045/046, MOB-024/026/027/034/035/036",
71
+ 'mobile-publish': "mobil uygulamanın mağaza yayını: altı adımlı yolculuk, mobile_store_* / mobile_build_* akışı, ön kontrol P-01…P-23 (push kimlikleri, izinler, reklam, kod tarama, App Review, satın alma kabuğu), yayın hedefleri (yalnız dryRun), uygulama içi satın alma (mobile_product_* / mobile_purchase_*: ürün — abonelik, tek seferlik, tüketilen kredi/jeton → hazırlık → faturalandırmalı derleme → gönder; elle kalanlar), iki tür güncelleme, derleme hata kodları, insanın Studio'da yaptıkları (test kullanıcıları dahil)",
72
72
  'mobile-connect-data': "mobil uygulamada veri bağlamanın telefon farkı: alt sayfa / ↗ tam ekran seçim (lookup.mobileMode, mobileRow), sonsuz kaydırma (behavior.mobile), datatable kartı (mobileCard), gruplu liste grouping, autocomplete araması, CD-004/019/020",
73
73
  'mobile-layout': 'mobil ekranın mizanpajı — tek dil: güvenli alan, tek kaydırıcı, 16 dp kenar, gap ritmi, saydam/yüzeyli grup, genişlik, yarıçap/yazı ölçeği; MOB-015…021 reçeteleri ve layoutReport hiza ölçümü',
74
74
  component: 'komponent modeli, olay sözleşmeleri, appearance boyut sözlüğü (widthString/heightString)',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appaflytech/zeyrek-ai-mcp",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "type": "module",
5
5
  "description": "Zeyrek (NgApps) platform APIs as MCP tools — data, logic and Studio design surface in one gateway",
6
6
  "license": "UNLICENSED",