@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.
@@ -8,7 +8,7 @@ import { accessPolicyRules } from './access.js';
8
8
  import { isGuid } from '../platform/refs.js';
9
9
  import { themePolicyRules } from './themeRules.js';
10
10
  import { processPolicyRules } from './process.js';
11
- import { mobileStorePolicyRules } from './mobileStore.js';
11
+ import { PRODUCT_KEY_PATTERN, mobileStorePolicyRules, workflowBalanceFindings } from './mobileStore.js';
12
12
  const terminalStepTypes = new Set(['response', 'convert', 'revert']);
13
13
  const DESCRIPTION_EXEMPT_STEPS = new Set(['start']);
14
14
  const stepKindMatrix = {
@@ -30,7 +30,8 @@ const stepKindMatrix = {
30
30
  redirect: ['middleware'],
31
31
  response: ['workflow', 'middleware'],
32
32
  convert: ['workflow'],
33
- revert: ['automation']
33
+ revert: ['automation'],
34
+ balance: ['workflow', 'automation', 'middleware']
34
35
  };
35
36
  function asArray(value) {
36
37
  return Array.isArray(value) ? value : [];
@@ -177,6 +178,9 @@ export const flowBranchNames = {
177
178
  'biometric-auth': ['always', 'error'],
178
179
  back: ['always'],
179
180
  'scan-code': ['always', 'error'],
181
+ purchase: ['always', 'error'],
182
+ 'restore-purchases': ['always', 'error'],
183
+ 'balance-spend': ['always', 'error'],
180
184
  end: []
181
185
  };
182
186
  /**
@@ -189,7 +193,7 @@ export const knownFlowStepTypes = new Set([
189
193
  'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
190
194
  'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share',
191
195
  'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'bpm',
192
- 'permission', 'open-settings', 'ad-show', 'back', 'biometric-auth', 'scan-code'
196
+ 'permission', 'open-settings', 'ad-show', 'back', 'biometric-auth', 'scan-code', 'purchase', 'restore-purchases', 'balance-spend'
193
197
  ]);
194
198
  /**
195
199
  * Cihaz yeteneği adımlarının kapalı kümeleri — `@ng-apps/flow` `tasks/device/device.functions.ts`
@@ -215,6 +219,22 @@ export const biometricFallbacks = new Set(['device', 'none']);
215
219
  * `upc_a` bilerek yok: UPC-A etiketi iki platformda `ean13` ve on üç haneyle döner.
216
220
  */
217
221
  export const scanCodeTypes = new Set(['aztec', 'codabar', 'code39', 'code93', 'code128', 'datamatrix', 'ean8', 'ean13', 'itf14', 'pdf417', 'qr', 'upc_e']);
222
+ /**
223
+ * `purchase` ve `restore-purchases` adımlarının hata sözlüğü (`Steps.<ad>.error.status`) ve `purchase`'ın tek argümanı —
224
+ * `@ng-apps/flow` `purchase-golden.json` `errors` / `argument`'ın aynası; backend `TestData/flows/purchase-golden.json` aynı
225
+ * dosyanın bayt bayt kopyasıdır (`test/flow-step-parity.test.ts` üçünü bağlar). Kök ADR 0027 madde 8.
226
+ */
227
+ export const purchaseErrorStatuses = new Set(['cancelled', 'pending', 'failed', 'unavailable', 'unsupported', 'invalid']);
228
+ export const purchaseStepArguments = new Set(['value']);
229
+ /**
230
+ * `balance-spend` adımının hata sözlüğü (`Steps.<ad>.error.status`) ve argümanları (`value` = bakiye adı, `title` = miktar; ikisi de
231
+ * metin şablonu) — `@ng-apps/flow` `balance-spend-golden.json` `errors` / `argument`'ın aynası; backend
232
+ * `TestData/flows/balance-spend-golden.json` aynı iki listeyi taşır (`test/flow-step-parity.test.ts` üçünü bağlar). Kök ADR 0027 bakiye bölümü.
233
+ */
234
+ export const balanceSpendErrorStatuses = new Set(['insufficient', 'unavailable', 'unsupported', 'invalid', 'failed']);
235
+ export const balanceSpendStepArguments = new Set(['value', 'title']);
236
+ /** `purchase.value` statikse bu desende bir katalog anahtarıdır (`knowledge/mobileStore.ts` `PRODUCT_KEY_PATTERN`, backend aynası). */
237
+ export const purchaseProductKeyPattern = PRODUCT_KEY_PATTERN;
218
238
  /** `permission` adımının kipleri — `@ng-apps/flow` `PERMISSION_MODES`'un aynası (kök ADR 0010); tür adını gateway bilmez, backend doğrular. */
219
239
  export const permissionStepModes = new Set(['request', 'check']);
220
240
  /**
@@ -898,7 +918,7 @@ export const policyRules = [
898
918
  return [{
899
919
  ruleId: 'WF-005',
900
920
  severity: 'block',
901
- message: `'${stepLabel(step)}' step'i (${step.type}) ${kind} tipinde kullanılamaz. İzin matrisi: variable/set-user/setcookie/redirect yalnız middleware; convert yalnız workflow; revert yalnız automation; response workflow+middleware; process workflow+automation.`,
921
+ message: `'${stepLabel(step)}' step'i (${step.type}) ${kind} tipinde kullanılamaz. İzin matrisi: variable/set-user/setcookie/redirect yalnız middleware; convert yalnız workflow; revert yalnız automation; response workflow+middleware; process workflow+automation; balance üçünde de serbest.`,
902
922
  guide: 'ngapps://guides/workflow'
903
923
  }];
904
924
  });
@@ -2905,6 +2925,149 @@ export const policyRules = [
2905
2925
  return findings;
2906
2926
  })
2907
2927
  },
2928
+ {
2929
+ id: 'FLOW-045',
2930
+ module: 'flow',
2931
+ appliesTo: ['create', 'update'],
2932
+ sourceRef: "Satın alma adımları (kök docs/adr/0027-uygulama-ici-satin-alma.md madde 8): purchase.value ürünün Zeyrek ANAHTARIDIR ve METİN ŞABLONUDUR (@ng-apps/flow resolveStepText — düz 'premium_yillik' ya da '{{ Components.plan.value }}'; {{ }} dışındaki ifade harfi harfine anahtar sayılır); restore-purchases argümansızdır. Backend FlowDtoValidator HasPurchaseProduct boş değeri 422 FlowStepPurchaseProductMissing ile reddeder, anahtarın katalogda olduğunu denetlemez (iş sahibi kararı b-04, 2026-10-23: Studio → Core bağımlılığı açılmaz; mağaza yayını ön kontrolü P-23 purchases_unknown_product uyarır) — cihaz ve önizleme bilinmeyen anahtarda 'invalid' döner. Katalog denetimi araç katmanındadır ve REDDEDER: {{ }} şablonu olmayan, katalogda olmayan değer block (tools/flows.ts purchaseChecks → knowledge/mobileStore.ts purchaseCatalogFindings; katalog okunamazsa susar). productKey/product/sku gibi anahtarlar StepManagementArgument sözleşmesinde YOK, 200 ile sessizce düşer. İki adım da kapıdır: error dalı → hata akışı → o da yoksa akış adımda BİTER (device.functions.ts settle 'stop')",
2933
+ check: definition => {
2934
+ const errorSteps = flowStepList(definition.errorSteps);
2935
+ const errorFlow = errorSteps.some(step => step.type !== 'start');
2936
+ const statuses = [...purchaseErrorStatuses].join(', ');
2937
+ return flowAllSteps(definition)
2938
+ .filter(step => step.type === 'purchase' || step.type === 'restore-purchases')
2939
+ .flatMap((step) => {
2940
+ const label = flowStepLabel(step);
2941
+ const resultName = step.name || label;
2942
+ const args = (step.args ?? {});
2943
+ const findings = [];
2944
+ if (step.type === 'purchase') {
2945
+ const value = typeof args.value === 'string' ? args.value.trim() : '';
2946
+ const stray = ['productKey', 'product', 'productId', 'sku', 'key'].filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
2947
+ const unquoted = value.replace(/^['"`](.*)['"`]$/, '$1');
2948
+ if (!value) {
2949
+ findings.push({
2950
+ ruleId: 'FLOW-045',
2951
+ severity: 'block',
2952
+ message: `'${label}' purchase adımının ürünü yok: args.value ürünün Zeyrek anahtarını ister (platform 422 FlowStepPurchaseProductMissing döner) — düz anahtar ('premium_yillik') ya da şablon ('{{ Components.plan.value }}'). Anahtarlar: mobile_product_list.`,
2953
+ guide: 'ngapps://guides/mobile-steps'
2954
+ });
2955
+ }
2956
+ else if (!value.includes('{{') && !purchaseProductKeyPattern.test(value)) {
2957
+ findings.push({
2958
+ ruleId: 'FLOW-045',
2959
+ severity: 'warn',
2960
+ message: unquoted !== value && purchaseProductKeyPattern.test(unquoted)
2961
+ ? `'${label}' purchase adımının ürünü tırnaklı yazılmış: ${value}. Değer metin şablonudur, tırnaklar anahtarın parçası sayılır ve cihaz 'invalid' döner — args.value: '${unquoted}' yazın.`
2962
+ : /[.([]/.test(value)
2963
+ ? `'${label}' purchase adımının ürünü {{ }} dışında ifade: '${value}'. args.value METİN ŞABLONUDUR — ifade harfi harfine anahtar sayılır, cihaz 'invalid' döner. Sarın: '{{ ${value} }}'.`
2964
+ : `'${label}' purchase adımının ürünü bir ürün anahtarı değil: '${value}' (${purchaseProductKeyPattern.source}). Katalogdaki hiçbir anahtar bu biçimde olamaz: gateway katalog denetimi yazımı durdurur, cihaz 'invalid' döner. Anahtarlar: mobile_product_list.`,
2965
+ guide: 'ngapps://guides/mobile-steps'
2966
+ });
2967
+ }
2968
+ if (stray.length > 0) {
2969
+ findings.push({
2970
+ ruleId: 'FLOW-045',
2971
+ severity: value ? 'warn' : 'block',
2972
+ message: `'${label}' purchase adımında args.${stray.join('/args.')} alanı var: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu 200 ile sessizce düşürür. Ürünün anahtarı args.value'ya yazılır — ör. args: { value: 'premium_yillik' }.`,
2973
+ guide: 'ngapps://guides/mobile-steps'
2974
+ });
2975
+ }
2976
+ }
2977
+ const errorBranch = flowBranches(step).some(branch => branch.name === 'error' && Boolean(branch.nextStepId));
2978
+ const insideErrorFlow = errorSteps.includes(step);
2979
+ if (!errorBranch && (insideErrorFlow || !errorFlow)) {
2980
+ findings.push({
2981
+ ruleId: 'FLOW-045',
2982
+ severity: 'warn',
2983
+ message: `'${label}' ${String(step.type)} adımının error dalı da akışın hata adımları da bağlı değil: kullanıcı vazgeçince, ödeme onay beklerken ya da mağaza yokken akış BU ADIMDA BİTER, always'e bağlı adım koşmaz (kapı). Kullanıcıya söylenecek olanı error dalına bağlayın${insideErrorFlow ? ' (adım hata akışının içinde: hata akışının kendi hata akışı yoktur)' : ''} — Steps.${resultName}.error.status: ${statuses}; vazgeçmeye (cancelled) genellikle bir şey gösterilmez, pending 'onay bekleniyor' der.`,
2984
+ guide: 'ngapps://guides/mobile-steps'
2985
+ });
2986
+ }
2987
+ return findings;
2988
+ });
2989
+ }
2990
+ },
2991
+ {
2992
+ id: 'FLOW-046',
2993
+ module: 'flow',
2994
+ appliesTo: ['create', 'update'],
2995
+ sourceRef: "Harca adımı (kök docs/adr/0027-uygulama-ici-satin-alma.md bakiye bölümü, plans/mobil-uygulama-ici-satin-alma b-09/g-02): balance-spend.value bakiyenin adı, balance-spend.title MİKTARDIR (adım başına ikisi de METİN ŞABLONU — @ng-apps/flow resolveStepText; {{ }} dışındaki ifade harfi harfine değer sayılır). Backend FlowDtoValidator HasBalanceSpendArguments value ya da title boşsa 422 FlowStepBalanceSpendArgumentMissing döner; ad ^[a-z][a-z0-9_]{1,63}$ (MobileStoreProductConstants.IdentifierPattern), miktar 1 ve üstü tam sayı — aksi cihazda 'invalid'. Bakiye yetmezse adım error dalına 'insufficient' ile düşer (sunucu 409); çevrimdışıyken 'unavailable' (K12: para değeri taşır, çevrimdışı kuyruğa girmez); web'de 'unsupported'. Katalog ayağı araç katmanındadır ve REDDEDER: {{ }} şablonu olmayan, uygulamanın tüketilen ürünlerinde olmayan bakiye adı block (tools/flows.ts purchaseChecks → knowledge/mobileStore.ts balanceCatalogFindings; katalog okunamazsa susar). Workflow yüzeyindeki balance adımı aynı katalog denetimini workflowStepFindings'te alır. balance/amount/name gibi anahtarlar StepManagementArgument sözleşmesinde YOK, 200 ile sessizce düşer. error dalı bağlı değilse akış adımda BİTER (kapı)",
2996
+ check: definition => {
2997
+ const errorSteps = flowStepList(definition.errorSteps);
2998
+ const errorFlow = errorSteps.some(step => step.type !== 'start');
2999
+ const statuses = [...balanceSpendErrorStatuses].join(', ');
3000
+ return flowAllSteps(definition)
3001
+ .filter(step => step.type === 'balance-spend')
3002
+ .flatMap((step) => {
3003
+ const label = flowStepLabel(step);
3004
+ const resultName = step.name || label;
3005
+ const args = (step.args ?? {});
3006
+ const value = typeof args.value === 'string' ? args.value.trim() : '';
3007
+ const title = typeof args.title === 'string' ? args.title.trim() : '';
3008
+ const stray = ['balance', 'amount', 'name', 'key', 'idempotencyKey'].filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
3009
+ const unquoted = value.replace(/^['"`](.*)['"`]$/, '$1');
3010
+ const findings = [];
3011
+ if (!value) {
3012
+ findings.push({
3013
+ ruleId: 'FLOW-046',
3014
+ severity: 'block',
3015
+ message: `'${label}' balance-spend adımının bakiye adı yok: args.value bakiyenin adını ister (platform 422 FlowStepBalanceSpendArgumentMissing döner) — düz ad ('kredi') ya da şablon ('{{ Components.secim.value }}'). Adlar: mobile_product_list (consumable ürünün balance alanı).`,
3016
+ guide: 'ngapps://guides/mobile-steps'
3017
+ });
3018
+ }
3019
+ else if (!value.includes('{{') && !purchaseProductKeyPattern.test(value)) {
3020
+ findings.push({
3021
+ ruleId: 'FLOW-046',
3022
+ severity: 'warn',
3023
+ message: unquoted !== value && purchaseProductKeyPattern.test(unquoted)
3024
+ ? `'${label}' balance-spend adımının bakiye adı tırnaklı yazılmış: ${value}. Değer metin şablonudur, tırnaklar adın parçası sayılır ve cihaz 'invalid' döner — args.value: '${unquoted}' yazın.`
3025
+ : /[.([]/.test(value)
3026
+ ? `'${label}' balance-spend adımının bakiye adı {{ }} dışında ifade: '${value}'. args.value METİN ŞABLONUDUR — ifade harfi harfine ad sayılır, cihaz 'invalid' döner. Sarın: '{{ ${value} }}'.`
3027
+ : `'${label}' balance-spend adımının bakiye adı geçerli bir ad değil: '${value}' (${purchaseProductKeyPattern.source}). Katalogdaki hiçbir bakiye bu biçimde olamaz: cihaz 'invalid' döner. Adlar: mobile_product_list.`,
3028
+ guide: 'ngapps://guides/mobile-steps'
3029
+ });
3030
+ }
3031
+ if (!title) {
3032
+ findings.push({
3033
+ ruleId: 'FLOW-046',
3034
+ severity: 'block',
3035
+ message: `'${label}' balance-spend adımının miktarı yok: args.title düşülecek miktarı ister (platform 422 FlowStepBalanceSpendArgumentMissing döner) — düz tam sayı ('10') ya da şablon ('{{ Components.adet.value }}'). Miktar args.amount DEĞİL args.title'dır.`,
3036
+ guide: 'ngapps://guides/mobile-steps'
3037
+ });
3038
+ }
3039
+ else if (!title.includes('{{') && !/^[1-9][0-9]*$/.test(title)) {
3040
+ findings.push({
3041
+ ruleId: 'FLOW-046',
3042
+ severity: 'warn',
3043
+ message: /[.([]/.test(title)
3044
+ ? `'${label}' balance-spend adımının miktarı {{ }} dışında ifade: '${title}'. args.title METİN ŞABLONUDUR — ifade harfi harfine metin sayılır ve miktar geçersiz olur ('invalid'). Sarın: '{{ ${title} }}'.`
3045
+ : `'${label}' balance-spend adımının miktarı 1 ve üstü bir tam sayı değil: '${title}'. Cihaz 'invalid' döner; sıfır ve eksi miktar harcama değildir.`,
3046
+ guide: 'ngapps://guides/mobile-steps'
3047
+ });
3048
+ }
3049
+ if (stray.length > 0) {
3050
+ findings.push({
3051
+ ruleId: 'FLOW-046',
3052
+ severity: value && title ? 'warn' : 'block',
3053
+ message: `'${label}' balance-spend adımında args.${stray.join('/args.')} alanı var: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu 200 ile sessizce düşürür. Bakiye adı args.value'ya, miktar args.title'a yazılır — ör. args: { value: 'kredi', title: '10' }. Tekrar güvenliği (idempotencyKey) bu adımda yoktur: sunucu her tıklamada ayrı anahtar üretir.`,
3054
+ guide: 'ngapps://guides/mobile-steps'
3055
+ });
3056
+ }
3057
+ const errorBranch = flowBranches(step).some(branch => branch.name === 'error' && Boolean(branch.nextStepId));
3058
+ const insideErrorFlow = errorSteps.includes(step);
3059
+ if (!errorBranch && (insideErrorFlow || !errorFlow)) {
3060
+ findings.push({
3061
+ ruleId: 'FLOW-046',
3062
+ severity: 'warn',
3063
+ message: `'${label}' balance-spend adımının error dalı da akışın hata adımları da bağlı değil: bakiye yetmediğinde (insufficient), çevrimdışıyken (unavailable) ya da kabuk satın almayı taşımıyorken akış BU ADIMDA BİTER, always'e bağlı adım koşmaz (kapı). Kullanıcıya söylenecek olanı error dalına bağlayın${insideErrorFlow ? ' (adım hata akışının içinde: hata akışının kendi hata akışı yoktur)' : ''} — Steps.${resultName}.error.status: ${statuses}; insufficient genellikle "Kredin yetmiyor" ile satın alma ekranına yönlendirir.`,
3064
+ guide: 'ngapps://guides/mobile-steps'
3065
+ });
3066
+ }
3067
+ return findings;
3068
+ });
3069
+ }
3070
+ },
2908
3071
  {
2909
3072
  id: 'FLOW-041',
2910
3073
  module: 'flow',
@@ -3939,6 +4102,35 @@ export async function microfunctionArchiveEntrypointFindings(platform, project,
3939
4102
  }
3940
4103
  /** `MicrofunctionLanguageProfile.ModuleExtension` — dil adı bilgi katmanında taşınır. */
3941
4104
  const microfunctionModuleExtensions = { node: '.js', python: '.py' };
4105
+ /**
4106
+ * `balance` adımının (StepType.Balance, kök ADR 0027 bakiye bölümü) FLOW-046 denetimi: statik bakiye/hak adı uygulamanın ürün
4107
+ * kataloğunda olmalı (`knowledge/mobileStore.ts` `workflowBalanceFindings`). Katalog uygulamanın mobil mağaza kaydındadır; adım
4108
+ * `applicationId` ifadesi verirse o uygulama, vermezse akışın uygulaması okunur. Katalog okunamazsa (mobil olmayan uygulama
4109
+ * 404, ağ) ya da uygulama bir ifadeyle/slug'la anılıyorsa yalnız idempotencyKey uyarısı kalır — bilmediğimizi ret diye söylemeyiz.
4110
+ */
4111
+ async function workflowBalanceStepFindings(platform, application, sites) {
4112
+ if (sites.length === 0)
4113
+ return [];
4114
+ const targets = new Map();
4115
+ for (const site of sites) {
4116
+ const expression = typeof site.rule.expression === 'string' ? site.rule.expression : '';
4117
+ const named = /(?:^|[{,\s])applicationId\s*:\s*(['"`])([^'"`]*)\1/.exec(expression)?.[2];
4118
+ const target = named === undefined ? application : isGuid(named) ? named : undefined;
4119
+ if (target !== undefined)
4120
+ targets.set(target, [...(targets.get(target) ?? []), site]);
4121
+ }
4122
+ const found = [];
4123
+ for (const [target, group] of targets) {
4124
+ const products = await platform.org('GET', `/mobile-store/${target}/products`)
4125
+ .then(payload => {
4126
+ const data = payload && typeof payload === 'object' && 'data' in payload ? payload.data : payload;
4127
+ return Array.isArray(data) ? data.filter((entry) => Boolean(entry) && typeof entry === 'object') : undefined;
4128
+ }, () => undefined);
4129
+ const all = workflowBalanceFindings(group.map(({ label, rule }) => ({ label, rule })), products ?? []);
4130
+ found.push(...(products ? all : all.filter(finding => finding.severity !== 'block')));
4131
+ }
4132
+ return found;
4133
+ }
3942
4134
  /**
3943
4135
  * WF-032'nin ikinci yarısı. `rule.expression`'ın işe yarayıp yaramayacağı gövdeden görülemez:
3944
4136
  * `WorkflowStep` ifadeyi YALNIZ çağrılan akışın parametre DTO'su varken değerlendirir
@@ -3958,13 +4150,14 @@ export async function workflowStepFindings(platform, project, environment, appli
3958
4150
  : hasText(definition.applicationId) ? String(definition.applicationId) : undefined;
3959
4151
  if (application === undefined)
3960
4152
  return [];
4153
+ const balanceFindings = await workflowBalanceStepFindings(platform, application, stepCallSites(workflowBody(definition, kind), 'balance'));
3961
4154
  const sites = stepCallSites(workflowBody(definition, kind), 'workflow')
3962
4155
  .filter(site => GUID_PATTERN.test(String(site.rule.workflowId ?? '')));
3963
4156
  if (sites.length === 0)
3964
- return [];
4157
+ return applyPolicyMode(platform.config, balanceFindings, policyOverride);
3965
4158
  const guide = 'ngapps://guides/workflow';
3966
4159
  const records = new Map();
3967
- const findings = [];
4160
+ const findings = [...balanceFindings];
3968
4161
  for (const { label, rule } of sites) {
3969
4162
  const workflowId = String(rule.workflowId);
3970
4163
  if (!records.has(workflowId)) {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Mağazadan taşımanın (kök ADR 0028, `plans/mobil-magazadan-tasima` g-01) saf yanıt özetleri. Backend kayıtları büyük:
3
+ * içe aktarma raporu her dil ve görsel için satır, fark her alan için satır taşır; ajan yalnız karar vereceği satırı görür
4
+ * (`docs/41-arac-tasarim-standardi.md` §5 yanıt bütçesi).
5
+ */
6
+ /** Backend `MobileStoreImportConstants.Section*` — rapor bölümleri */
7
+ export const STORE_IMPORT_SECTIONS = ['identity', 'texts', 'urls', 'appInfo', 'assets', 'declarations'];
8
+ /** Backend `MobileStoreDiffConstants.Choice*` */
9
+ export const STORE_DIFF_CHOICES = ['zeyrek', 'store'];
10
+ /** Backend `PurchaseOptions.ImportMaxBytes` */
11
+ export const PURCHASE_IMPORT_MAX_BYTES = 5_242_880;
12
+ /** Fark satırının seçim isteyen durumları (`MobileStoreDiffConstants.ConflictStates`) */
13
+ export const STORE_DIFF_CONFLICT_STATES = ['store_changed', 'both_changed'];
14
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ const items = (value) => (Array.isArray(value) ? value : []).filter(isObject);
16
+ /** Rapor: varsayılan özet + sorunlu satırlar + ürün önerileri; `section` verilirse o bölümün bütün satırları. */
17
+ export function importReportOutline(record, section) {
18
+ const report = isObject(record.report) ? record.report : {};
19
+ const sections = items(report.sections);
20
+ const issues = sections.flatMap(entry => items(entry.rows)
21
+ .filter(row => Array.isArray(row.issues) && row.issues.length > 0)
22
+ .map(row => ({ section: entry.key, field: row.field, store: row.store, issues: row.issues, value: row.value, alternative: row.alternative })));
23
+ const violations = items(record.violations);
24
+ return {
25
+ id: record.id,
26
+ state: record.state,
27
+ connection: record.connection,
28
+ progress: record.progress,
29
+ summary: report.summary,
30
+ issues,
31
+ products: items(report.products).map((product, index) => ({ index, kind: product.kind, bucket: product.bucket, suggestedKey: product.suggestedKey, suggestedEntitlement: product.suggestedEntitlement, apple: product.apple, play: product.play, storeManaged: product.storeManaged, issues: product.issues })),
32
+ mapping: record.mapping,
33
+ ...(violations.length > 0 ? { violations, next: `Taslak kaydedildi ama ${violations.length} kural ihlali var; mobile_store_import_apply bunlar düzelmeden 422 döner. Satırı düzeltip mobile_store_import_mapping'i yeniden çağırın.` } : {}),
34
+ ...(section ? { rows: items(sections.find(entry => entry.key === section)?.rows) } : {}),
35
+ error: record.error,
36
+ appliedAt: record.appliedAt
37
+ };
38
+ }
39
+ /** Fark: değişmeyen satırlar sayılır, gösterilmez; çakışmalar ve gönderilecek alanlar ayrı listelenir. */
40
+ export function diffOutline(diff) {
41
+ const rows = items(diff.rows);
42
+ const changed = rows.filter(row => row.state !== 'unchanged');
43
+ const unresolved = Array.isArray(diff.unresolved) ? diff.unresolved : [];
44
+ const unreadPlatforms = Array.isArray(diff.unreadPlatforms) ? diff.unreadPlatforms : [];
45
+ return {
46
+ id: diff.id,
47
+ readAt: diff.readAt,
48
+ fresh: diff.fresh,
49
+ unchanged: rows.length - changed.length,
50
+ changed: changed.map(row => ({ key: row.key, state: row.state, base: row.base, zeyrek: row.zeyrek, store: row.store, choice: isObject(diff.choices) ? diff.choices[String(row.key)] : undefined })),
51
+ unresolved,
52
+ send: diff.send,
53
+ storeSendFields: diff.storeSendFields,
54
+ ...(unreadPlatforms.length > 0 ? { unreadPlatforms, note: `${unreadPlatforms.join(', ')} mağazası okunamadı (429/5xx/ağ): son bilinen hâli esas alındı, yalnız Zeyrek'te değişen alanlar gönderilir ve yayın durmaz. Konsoldaki elle değişiklik bu okumada görünmez; mümkünse mobile_store_diff { action: 'read' } ile yeniden okuyun (MOB-041).` } : {}),
55
+ ...(unresolved.length > 0 ? { next: `Yayın bu ${unresolved.length} çakışma seçilmeden başlamaz: mobile_store_diff { action: 'choose', diffId: '${String(diff.id)}', choices: { '<key>': 'zeyrek' | 'store' } } — 'store' mağazadaki değeri Zeyrek'e yazar (MOB-041).` } : {})
56
+ };
57
+ }
58
+ /** Abone listesi işi: ilerleme + sonuç sayfası; e-posta backend'de maskelidir. */
59
+ export function purchaseImportOutline(response) {
60
+ const progress = isObject(response.progress) ? response.progress : response;
61
+ return {
62
+ progress,
63
+ results: items(response.results),
64
+ page: response.page,
65
+ hasMore: response.hasMore,
66
+ ...(progress.state === 'queued' || progress.state === 'running' ? { next: `İş sürüyor: mobile_purchase_import_status { importId: '${String(progress.id)}' } ile yeniden okuyun.` } : {}),
67
+ ...(progress.error === 'iap_key_missing' ? { fix: 'Apple satırları için satın alma anahtarı (.p8, App Store Server API) Studio › Yayınla › Gizli bilgiler’de yüklenmeli — MCP’den yazılmaz.' } : {})
68
+ };
69
+ }
@@ -0,0 +1,233 @@
1
+ {
2
+ "_meta": {
3
+ "synthetic": true,
4
+ "note": "Mobil kredi harcama ekranı (kök docs/adr/0027-uygulama-ici-satin-alma.md bakiye bölümü, plans/mobil-uygulama-ici-satin-alma g-02): bakiye Balances.kredi'den okunur, düğme balance-spend ile 10 kredi düşer; bakiye yetmezse (insufficient) kullanıcıya söylenir. Akış FLOW-046'dan temiz geçer (test/coverage.test.ts). Kredi satın almak ayrı bir teklif ekranıdır: consumable ürün (kredi_100) purchase adımıyla alınır (flow-purchase-paywall).",
5
+ "pairsWith": "flow-purchase-paywall (kredi satışı); önce mobile_product_upsert { key: 'kredi_100', kind: 'consumable', balance: 'kredi', grantsAmount: 100, priceTry, texts }",
6
+ "sourceRef": "platform_guide('mobile-steps') §Kredi harcama tarifi; platform_guide('mobile-publish') §Satın almalar (tüketilen ürün); backend 04-bakiye-defteri.md §Harcama (409 insufficient, çevrimdışı unavailable)"
7
+ },
8
+ "_rules": {
9
+ "order": "Sıra: (1) mobile_product_upsert ile consumable ürün (balance: 'kredi'); (2) page_create + schemaPatchOps (kök önce safe-area); (3) flowCreateArgs; (4) bindOps ile akış kimliğini düğmeye bağla. Bakiye adı katalogdaki consumable ürünün balance alanıdır: katalogda olmayan statik ad FLOW-046 ile yazımda durur.",
10
+ "args": "balance-spend.value bakiye adı, balance-spend.title MİKTARDIR (args.amount/args.balance sessizce düşer). İkisi de metin şablonudur: düz '10' ya da seçimden '{{ adet.value }}'; {{ }} dışındaki ifade harfi harfine metin sayılır.",
11
+ "branches": "Adım KAPIDIR: error dalı → hata akışı → ikisi de yoksa akış adımda biter. Steps.<ad>.error.status: insufficient (bakiye yetmiyor, sunucu 409) · unavailable (çevrimdışı ya da sunucuya ulaşılamadı — çevrimdışı kuyruğa girmez) · unsupported (web uygulaması, production /view, eski kabuk) · invalid (ad ya da miktar geçersiz) · failed.",
12
+ "balance": "Harcamadan sonra Balances kendiliğinden güncellenir; sonucu ayrıca okumayın. Bakiye yalnız sunucudan gelir, istemcide hesaplanmaz.",
13
+ "signIn": "Girişsiz uygulamada bakiye kuruluma yazılır ve tüketilen ürün mağazada geri yüklenemez: telefon değişince bakiye gider (MOB-036). Girişli uygulamada hesaba taşınır."
14
+ },
15
+ "pageCreateArgs": {
16
+ "name": "Kredi",
17
+ "slug": "kredi"
18
+ },
19
+ "schemaPatchOps": [
20
+ {
21
+ "op": "add_component",
22
+ "parent": "page_container",
23
+ "component": {
24
+ "elementId": "guvenli_alan",
25
+ "name": "guvenli_alan",
26
+ "type": "safe-area",
27
+ "kind": "component",
28
+ "props": {},
29
+ "appearance": {
30
+ "marginType": "none",
31
+ "widthType": "fill"
32
+ },
33
+ "blocks": [],
34
+ "events": [],
35
+ "validations": [],
36
+ "stateBindings": [],
37
+ "children": [
38
+ {
39
+ "elementId": "kredi_ekrani",
40
+ "name": "kredi_ekrani",
41
+ "type": "container",
42
+ "kind": "component",
43
+ "props": {
44
+ "direction": {
45
+ "type": "static",
46
+ "ref": null,
47
+ "data": "column"
48
+ },
49
+ "gap": {
50
+ "type": "static",
51
+ "ref": null,
52
+ "data": "medium"
53
+ },
54
+ "horizontalAlign": {
55
+ "type": "static",
56
+ "ref": null,
57
+ "data": "stretch"
58
+ }
59
+ },
60
+ "appearance": {
61
+ "marginType": "none",
62
+ "widthType": "fill"
63
+ },
64
+ "blocks": [],
65
+ "events": [],
66
+ "validations": [],
67
+ "stateBindings": [],
68
+ "children": [
69
+ {
70
+ "elementId": "txt_kredi_baslik",
71
+ "name": "txt_kredi_baslik",
72
+ "type": "text",
73
+ "kind": "component",
74
+ "props": {
75
+ "content": {
76
+ "type": "static",
77
+ "ref": null,
78
+ "data": "Rapor üret"
79
+ },
80
+ "type": {
81
+ "type": "static",
82
+ "ref": null,
83
+ "data": "h2"
84
+ }
85
+ },
86
+ "appearance": {
87
+ "marginType": "none",
88
+ "widthType": "fill"
89
+ },
90
+ "blocks": [],
91
+ "events": [],
92
+ "validations": [],
93
+ "stateBindings": [],
94
+ "children": []
95
+ },
96
+ {
97
+ "elementId": "txt_bakiye",
98
+ "name": "txt_bakiye",
99
+ "type": "text",
100
+ "kind": "component",
101
+ "props": {
102
+ "content": {
103
+ "type": "code",
104
+ "ref": "{{ 'Kredin: ' + (Balances.kredi ?? 0) }}",
105
+ "data": null
106
+ }
107
+ },
108
+ "appearance": {
109
+ "marginType": "none",
110
+ "widthType": "fill"
111
+ },
112
+ "blocks": [],
113
+ "events": [],
114
+ "validations": [],
115
+ "stateBindings": [],
116
+ "children": []
117
+ },
118
+ {
119
+ "elementId": "btn_uret",
120
+ "name": "btn_uret",
121
+ "type": "button",
122
+ "kind": "component",
123
+ "props": {
124
+ "text": {
125
+ "type": "static",
126
+ "ref": null,
127
+ "data": "Rapor üret (10 kredi)"
128
+ },
129
+ "kind": {
130
+ "type": "static",
131
+ "ref": null,
132
+ "data": "primary"
133
+ }
134
+ },
135
+ "appearance": {
136
+ "marginType": "none",
137
+ "widthType": "fill"
138
+ },
139
+ "blocks": [],
140
+ "events": [],
141
+ "validations": [],
142
+ "stateBindings": [],
143
+ "children": []
144
+ }
145
+ ]
146
+ }
147
+ ]
148
+ }
149
+ }
150
+ ],
151
+ "flowCreateArgs": {
152
+ "host": "page",
153
+ "hostId": "<kredi sayfasının id'si>",
154
+ "name": "rapor-uret",
155
+ "slug": "rapor-uret",
156
+ "steps": [
157
+ {
158
+ "stepId": "start",
159
+ "name": "start",
160
+ "type": "start",
161
+ "kind": "normal",
162
+ "args": {},
163
+ "branches": [
164
+ {
165
+ "name": "always",
166
+ "nextStepId": "harca"
167
+ }
168
+ ]
169
+ },
170
+ {
171
+ "stepId": "harca",
172
+ "name": "harca",
173
+ "type": "balance-spend",
174
+ "kind": "normal",
175
+ "args": {
176
+ "value": "kredi",
177
+ "title": "10"
178
+ },
179
+ "branches": [
180
+ {
181
+ "name": "always",
182
+ "nextStepId": "hazir"
183
+ },
184
+ {
185
+ "name": "error",
186
+ "nextStepId": "olmadi"
187
+ }
188
+ ]
189
+ },
190
+ {
191
+ "stepId": "hazir",
192
+ "name": "hazir",
193
+ "type": "alert",
194
+ "kind": "normal",
195
+ "args": {
196
+ "message": "Raporun hazır. {{ Balances.kredi }} kredin kaldı.",
197
+ "alertType": "success"
198
+ },
199
+ "branches": [
200
+ {
201
+ "name": "always",
202
+ "nextStepId": null
203
+ }
204
+ ]
205
+ },
206
+ {
207
+ "stepId": "olmadi",
208
+ "name": "olmadi",
209
+ "type": "alert",
210
+ "kind": "normal",
211
+ "args": {
212
+ "message": "{{ Steps.harca.error.status === 'insufficient' ? 'Krediniz yetmiyor: önce kredi satın alın.' : 'Harcama yapılamadı: ' + Steps.harca.error.message }}",
213
+ "alertType": "error"
214
+ },
215
+ "branches": [
216
+ {
217
+ "name": "always",
218
+ "nextStepId": null
219
+ }
220
+ ]
221
+ }
222
+ ],
223
+ "errorSteps": []
224
+ },
225
+ "bindOps": [
226
+ {
227
+ "op": "set_event",
228
+ "target": "btn_uret",
229
+ "event": "click",
230
+ "eventValue": "<rapor-uret akış id>"
231
+ }
232
+ ]
233
+ }