@stewardhq/sdk 0.1.0-rc.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/README.md +437 -0
- package/dist/_chunks/errors.js +73 -0
- package/dist/_chunks/events.d.ts +537 -0
- package/dist/_chunks/events.js +350 -0
- package/dist/_chunks/index.d.ts +6005 -0
- package/dist/_chunks/locale.d.ts +17 -0
- package/dist/_chunks/locale.js +17 -0
- package/dist/_chunks/src.js +1202 -0
- package/dist/_chunks/steward.d.ts +2588 -0
- package/dist/_chunks/steward.js +578 -0
- package/dist/_chunks/validators.d.ts +23 -0
- package/dist/_chunks/validators.js +106 -0
- package/dist/_chunks/webhook-core.d.ts +32 -0
- package/dist/_chunks/webhook-core.js +64 -0
- package/dist/_chunks/webhook.d.ts +22 -0
- package/dist/_chunks/webhook.js +53 -0
- package/dist/contract.d.ts +4 -0
- package/dist/contract.js +4 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +784 -0
- package/dist/server.d.ts +236 -0
- package/dist/server.js +384 -0
- package/dist/testing/fixtures/events/LOCK.json +27 -0
- package/dist/testing/fixtures/events/account.deleted.json +36 -0
- package/dist/testing/fixtures/events/account.state_changed.json +53 -0
- package/dist/testing/fixtures/events/account.updated.json +37 -0
- package/dist/testing/fixtures/events/checkout.completed.json +36 -0
- package/dist/testing/fixtures/events/checkout.expired.json +26 -0
- package/dist/testing/fixtures/events/checkout.failed.json +27 -0
- package/dist/testing/fixtures/events/invoice.created.json +37 -0
- package/dist/testing/fixtures/events/invoice.issued.json +38 -0
- package/dist/testing/fixtures/events/invoice.voided.json +39 -0
- package/dist/testing/fixtures/events/subscription.activated.json +40 -0
- package/dist/testing/fixtures/events/subscription.cancel_scheduled.json +38 -0
- package/dist/testing/fixtures/events/subscription.canceled.json +29 -0
- package/dist/testing/fixtures/events/subscription.expired.json +27 -0
- package/dist/testing/fixtures/events/subscription.payment_failed.json +38 -0
- package/dist/testing/fixtures/events/subscription.reactivated.json +36 -0
- package/dist/testing/fixtures/events/subscription.renewed.json +39 -0
- package/dist/testing/fixtures/events/subscription.suspended.json +36 -0
- package/dist/testing/fixtures/events/subscription.terminated.json +28 -0
- package/dist/testing.d.ts +629 -0
- package/dist/testing.js +3759 -0
- package/dist/webhook/web.d.ts +17 -0
- package/dist/webhook/web.js +66 -0
- package/dist/webhook.d.ts +3 -0
- package/dist/webhook.js +3 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
import { A as CodeSchema, B as IntervalSchema, C as BillingProfileSchema, F as EntitlementsSchema, M as DateTimeSchema, O as ActorRefSchema, R as ExternalRefSchema, S as BillingProfileKindSchema, T as SubscriptionStatusSchema, a as EVENT_TYPES, j as CurrencySchema, k as AmountMinorSchema, o as EventIdSchema, r as BillingEventSchema, v as AccountSchema, y as AccountStateSchema, z as IdSchema } from "./events.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region ../contract/src/catalog.ts
|
|
4
|
+
const FeatureTypeSchema = z.enum([
|
|
5
|
+
"limit",
|
|
6
|
+
"boolean",
|
|
7
|
+
"string"
|
|
8
|
+
]);
|
|
9
|
+
const FeatureDefSchema = z.object({
|
|
10
|
+
key: CodeSchema,
|
|
11
|
+
type: FeatureTypeSchema,
|
|
12
|
+
description: z.string().max(500).optional()
|
|
13
|
+
});
|
|
14
|
+
const PlanDefSchema = z.object({
|
|
15
|
+
code: CodeSchema,
|
|
16
|
+
name: z.string().min(1).max(200),
|
|
17
|
+
/** Yükseltme/düşürme sırası: büyük olan üst plan. */
|
|
18
|
+
rank: z.number().int().nonnegative(),
|
|
19
|
+
/** Self-servis satın alınabilir mi (false: yalnızca grant ile verilir). */
|
|
20
|
+
sellable: z.boolean(),
|
|
21
|
+
entitlements: EntitlementsSchema
|
|
22
|
+
});
|
|
23
|
+
const PriceDefSchema = z.object({
|
|
24
|
+
planCode: CodeSchema,
|
|
25
|
+
interval: IntervalSchema,
|
|
26
|
+
intervalCount: z.number().int().positive().default(1),
|
|
27
|
+
currency: CurrencySchema,
|
|
28
|
+
amountMinor: AmountMinorSchema,
|
|
29
|
+
/** Tutar vergi dahil mi (faturada vergi bu tutardan ayrıştırılır). */
|
|
30
|
+
taxInclusive: z.boolean(),
|
|
31
|
+
/** Baz puan: %20 = 2000. */
|
|
32
|
+
taxRateBps: z.number().int().min(0).max(1e4)
|
|
33
|
+
});
|
|
34
|
+
const CatalogSyncInputSchema = z.object({
|
|
35
|
+
defaultPlanCode: CodeSchema,
|
|
36
|
+
features: z.array(FeatureDefSchema),
|
|
37
|
+
plans: z.array(PlanDefSchema).min(1),
|
|
38
|
+
prices: z.array(PriceDefSchema)
|
|
39
|
+
}).superRefine((c, ctx) => {
|
|
40
|
+
const features = new Set(c.features.map((f) => f.key));
|
|
41
|
+
const plans = new Set(c.plans.map((p) => p.code));
|
|
42
|
+
if (features.size !== c.features.length) ctx.addIssue({
|
|
43
|
+
code: "custom",
|
|
44
|
+
message: "tekrarlanan feature"
|
|
45
|
+
});
|
|
46
|
+
if (plans.size !== c.plans.length) ctx.addIssue({
|
|
47
|
+
code: "custom",
|
|
48
|
+
message: "tekrarlanan plan"
|
|
49
|
+
});
|
|
50
|
+
if (!plans.has(c.defaultPlanCode)) ctx.addIssue({
|
|
51
|
+
code: "custom",
|
|
52
|
+
path: ["defaultPlanCode"],
|
|
53
|
+
message: "varsayılan plan katalogda yok"
|
|
54
|
+
});
|
|
55
|
+
for (const [i, p] of c.plans.entries()) for (const key of Object.keys(p.entitlements)) if (!features.has(key)) ctx.addIssue({
|
|
56
|
+
code: "custom",
|
|
57
|
+
path: [
|
|
58
|
+
"plans",
|
|
59
|
+
i,
|
|
60
|
+
"entitlements",
|
|
61
|
+
key
|
|
62
|
+
],
|
|
63
|
+
message: "tanımsız feature"
|
|
64
|
+
});
|
|
65
|
+
const seen = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const [i, p] of c.prices.entries()) {
|
|
67
|
+
if (!plans.has(p.planCode)) ctx.addIssue({
|
|
68
|
+
code: "custom",
|
|
69
|
+
path: [
|
|
70
|
+
"prices",
|
|
71
|
+
i,
|
|
72
|
+
"planCode"
|
|
73
|
+
],
|
|
74
|
+
message: "tanımsız plan"
|
|
75
|
+
});
|
|
76
|
+
const key = `${p.planCode}/${p.interval}/${p.intervalCount}/${p.currency}`;
|
|
77
|
+
if (seen.has(key)) ctx.addIssue({
|
|
78
|
+
code: "custom",
|
|
79
|
+
path: ["prices", i],
|
|
80
|
+
message: `aynı fiyat anahtarı iki kez: ${key}`
|
|
81
|
+
});
|
|
82
|
+
seen.add(key);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const PriceSchema = PriceDefSchema.extend({
|
|
86
|
+
id: IdSchema,
|
|
87
|
+
active: z.boolean(),
|
|
88
|
+
createdAt: DateTimeSchema
|
|
89
|
+
});
|
|
90
|
+
const CatalogSchema = z.object({
|
|
91
|
+
product: CodeSchema,
|
|
92
|
+
defaultPlanCode: CodeSchema,
|
|
93
|
+
features: z.array(FeatureDefSchema),
|
|
94
|
+
plans: z.array(PlanDefSchema),
|
|
95
|
+
/** Varsayılan olarak yalnızca aktif fiyatlar. */
|
|
96
|
+
prices: z.array(PriceSchema)
|
|
97
|
+
});
|
|
98
|
+
const CatalogSyncResultSchema = z.object({
|
|
99
|
+
/** Aynı girdiyle tekrar senkron: hiçbir şey değişmedi. */
|
|
100
|
+
unchanged: z.boolean(),
|
|
101
|
+
createdPrices: z.array(IdSchema),
|
|
102
|
+
deactivatedPrices: z.array(IdSchema),
|
|
103
|
+
/** Yeni, yeniden aktifleşen ya da adı/sırası/satılabilirliği/hakları değişen planlar. */
|
|
104
|
+
changedPlans: z.array(CodeSchema),
|
|
105
|
+
/** Katalogdan çıkarılıp pasifleşen planlar. */
|
|
106
|
+
retiredPlans: z.array(CodeSchema),
|
|
107
|
+
removedFeatures: z.array(CodeSchema),
|
|
108
|
+
defaultPlanChanged: z.boolean(),
|
|
109
|
+
/** Sağlayıcıda karşılığı oluşturulamayan fiyatlar (tekrar senkronla düzelir). */
|
|
110
|
+
providerErrors: z.array(z.object({
|
|
111
|
+
priceId: IdSchema,
|
|
112
|
+
provider: z.string(),
|
|
113
|
+
code: z.string()
|
|
114
|
+
}))
|
|
115
|
+
});
|
|
116
|
+
/** `POST /v1/admin/accounts/refresh` — ürünün tüm hesaplarının snapshot'ı yeniden hesaplanır. */
|
|
117
|
+
const AccountsRefreshResultSchema = z.object({
|
|
118
|
+
/** Snapshot'ı değişen (version'ı artıp `account.updated` yayınlayan) hesap sayısı. */
|
|
119
|
+
changed: z.number().int().nonnegative() });
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region ../contract/src/settings.ts
|
|
122
|
+
const LOOPBACK_HOSTS$1 = /* @__PURE__ */ new Set([
|
|
123
|
+
"localhost",
|
|
124
|
+
"127.0.0.1",
|
|
125
|
+
"[::1]"
|
|
126
|
+
]);
|
|
127
|
+
const MAX_URL_LENGTH = 2e3;
|
|
128
|
+
function parseUrl(value) {
|
|
129
|
+
try {
|
|
130
|
+
return new URL(value);
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Kimlik bilgisi taşımayan http(s) adresi. */
|
|
136
|
+
const HttpUrlSchema = z.url({ protocol: /^https?$/ }).max(MAX_URL_LENGTH).refine((value) => {
|
|
137
|
+
const url = parseUrl(value);
|
|
138
|
+
return url !== null && url.username === "" && url.password === "";
|
|
139
|
+
}, "adreste kullanıcı bilgisi olamaz");
|
|
140
|
+
/** https; http yalnızca localhost / 127.0.0.1 / [::1]. */
|
|
141
|
+
const WebUrlSchema = HttpUrlSchema.refine((value) => {
|
|
142
|
+
const url = parseUrl(value);
|
|
143
|
+
return url !== null && (url.protocol === "https:" || LOOPBACK_HOSTS$1.has(url.hostname));
|
|
144
|
+
}, "https olmalı (http yalnızca localhost)");
|
|
145
|
+
/** http(s) adresinin origin'i (ör. `https://app.example.com`). */
|
|
146
|
+
const ReturnOriginSchema = HttpUrlSchema.transform((value) => new URL(value).origin);
|
|
147
|
+
const ReturnOriginsSchema = z.array(ReturnOriginSchema).max(20).transform((origins) => [...new Set(origins)]);
|
|
148
|
+
const SETTINGS_LOCALES = ["tr", "en"];
|
|
149
|
+
const SettingsLocaleSchema = z.enum(SETTINGS_LOCALES);
|
|
150
|
+
const TtlMinutesSchema = z.number().int().min(5).max(1440);
|
|
151
|
+
const DEFAULT_CHECKOUT_TTL_MINUTES = 30;
|
|
152
|
+
const DEFAULT_PORTAL_TTL_MINUTES = 30;
|
|
153
|
+
const BrandingNameSchema = z.string().trim().min(1).max(100);
|
|
154
|
+
const LogoUrlSchema = z.url({ protocol: /^https$/ }).max(MAX_URL_LENGTH);
|
|
155
|
+
const AccentColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/, "#rrggbb").transform((value) => value.toLowerCase());
|
|
156
|
+
const SupportEmailSchema = z.email().max(254);
|
|
157
|
+
const BrandingSchema = z.object({
|
|
158
|
+
/** Hosted sayfada görünen ürün adı. */
|
|
159
|
+
name: BrandingNameSchema.optional(),
|
|
160
|
+
/** Yalnızca https (hosted sayfanın CSP'si logonun origin'ine izin verir). */
|
|
161
|
+
logoUrl: LogoUrlSchema.optional(),
|
|
162
|
+
/** `#rrggbb` (küçük harfe indirgenir). */
|
|
163
|
+
accentColor: AccentColorSchema.optional(),
|
|
164
|
+
supportEmail: SupportEmailSchema.optional()
|
|
165
|
+
});
|
|
166
|
+
const ProductSettingsSchema = z.object({
|
|
167
|
+
/** Checkout'ları alan ödeme sağlayıcısı. Operatör ayarıdır (pod CLI); yönetim ucundan yazılmaz. */
|
|
168
|
+
provider: z.string().min(1).default("iyzico"),
|
|
169
|
+
appUrl: WebUrlSchema.optional(),
|
|
170
|
+
defaultCurrency: CurrencySchema.default("TRY"),
|
|
171
|
+
defaultLocale: SettingsLocaleSchema.default("tr"),
|
|
172
|
+
extraReturnOrigins: ReturnOriginsSchema.default([]),
|
|
173
|
+
privacyUrl: WebUrlSchema.optional(),
|
|
174
|
+
termsUrl: WebUrlSchema.optional(),
|
|
175
|
+
branding: BrandingSchema.default({}),
|
|
176
|
+
/** Açık checkout oturumunun ömrü (sağlayıcının form ömrü daha kısaysa o geçerli). */
|
|
177
|
+
checkoutTtlMinutes: TtlMinutesSchema.default(30),
|
|
178
|
+
portalTtlMinutes: TtlMinutesSchema.default(30)
|
|
179
|
+
});
|
|
180
|
+
const BrandingPatchSchema = z.object({
|
|
181
|
+
name: BrandingNameSchema.nullable().optional(),
|
|
182
|
+
logoUrl: LogoUrlSchema.nullable().optional(),
|
|
183
|
+
accentColor: AccentColorSchema.nullable().optional(),
|
|
184
|
+
supportEmail: SupportEmailSchema.nullable().optional()
|
|
185
|
+
}).strict();
|
|
186
|
+
/** `PATCH /v1/admin/settings` gövdesi. */
|
|
187
|
+
const ProductSettingsPatchSchema = z.object({
|
|
188
|
+
appUrl: WebUrlSchema.nullable().optional(),
|
|
189
|
+
defaultCurrency: CurrencySchema.optional(),
|
|
190
|
+
defaultLocale: SettingsLocaleSchema.optional(),
|
|
191
|
+
extraReturnOrigins: ReturnOriginsSchema.optional(),
|
|
192
|
+
privacyUrl: WebUrlSchema.nullable().optional(),
|
|
193
|
+
termsUrl: WebUrlSchema.nullable().optional(),
|
|
194
|
+
branding: BrandingPatchSchema.nullable().optional(),
|
|
195
|
+
checkoutTtlMinutes: TtlMinutesSchema.optional(),
|
|
196
|
+
portalTtlMinutes: TtlMinutesSchema.optional()
|
|
197
|
+
}).strict();
|
|
198
|
+
/**
|
|
199
|
+
* Doğrulanmış yamayı ayarlara uygular (saf). Sonuç şemadan geçer; alan kuralları
|
|
200
|
+
* yamada zaten denetlendiği için yalnızca programlama hatasında fırlatır.
|
|
201
|
+
*/
|
|
202
|
+
function applySettingsPatch(current, patch) {
|
|
203
|
+
const next = { ...current };
|
|
204
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
205
|
+
if (value === void 0) continue;
|
|
206
|
+
if (key === "branding") {
|
|
207
|
+
const branding = value === null ? {} : { ...current.branding };
|
|
208
|
+
if (value !== null) {
|
|
209
|
+
for (const [field, fieldValue] of Object.entries(value)) if (fieldValue === null) delete branding[field];
|
|
210
|
+
else if (fieldValue !== void 0) branding[field] = fieldValue;
|
|
211
|
+
}
|
|
212
|
+
next.branding = branding;
|
|
213
|
+
} else if (value === null) delete next[key];
|
|
214
|
+
else next[key] = value;
|
|
215
|
+
}
|
|
216
|
+
return ProductSettingsSchema.parse(next);
|
|
217
|
+
}
|
|
218
|
+
/** Checkout dönüşüne izinli origin'ler: `appUrl`'in origin'i + `extraReturnOrigins`. */
|
|
219
|
+
function returnOriginsOf(settings) {
|
|
220
|
+
const app = settings.appUrl === void 0 ? null : parseUrl(settings.appUrl);
|
|
221
|
+
return [.../* @__PURE__ */ new Set([...app ? [app.origin] : [], ...settings.extraReturnOrigins])];
|
|
222
|
+
}
|
|
223
|
+
/** `returnUrl`'in origin'i izinli mi (geçersiz adres: hayır). */
|
|
224
|
+
function isReturnUrlAllowed(settings, returnUrl) {
|
|
225
|
+
const url = parseUrl(returnUrl);
|
|
226
|
+
return url !== null && returnOriginsOf(settings).includes(url.origin);
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region ../contract/src/checkout.ts
|
|
230
|
+
const ReturnUrlSchema = z.url({ protocol: /^https?$/ });
|
|
231
|
+
/** Kabul edilen sözleşme — içerik üründe yaşar, burada yalnızca iz. */
|
|
232
|
+
const ConsentSchema = z.object({
|
|
233
|
+
document: z.string().min(1).max(100),
|
|
234
|
+
version: z.string().min(1).max(100),
|
|
235
|
+
acceptedAt: DateTimeSchema,
|
|
236
|
+
/** Belgenin bağı (hosted akışta her zaman; eski gövdede verildiyse). */
|
|
237
|
+
url: z.string().optional()
|
|
238
|
+
});
|
|
239
|
+
/** Checkout gövdesindeki onay: hosted akışta onaylatılacak belge, eski gövdede kabul izi. */
|
|
240
|
+
const CheckoutConsentInputSchema = z.object({
|
|
241
|
+
document: z.string().min(1).max(100),
|
|
242
|
+
version: z.string().min(1).max(100),
|
|
243
|
+
/** Hosted akışta zorunlu: sayfadaki belge bağı. */
|
|
244
|
+
url: z.url({ protocol: /^https?$/ }).max(2e3).optional(),
|
|
245
|
+
/** Yalnızca eski gövdede (ve orada zorunlu): ürün onayı kendisi aldı. */
|
|
246
|
+
acceptedAt: DateTimeSchema.optional()
|
|
247
|
+
});
|
|
248
|
+
/** Hosted sayfanın ön doldurması (profil/kimlik verisi değil; K21). */
|
|
249
|
+
const CheckoutPrefillSchema = z.object({
|
|
250
|
+
email: z.email().max(200).optional(),
|
|
251
|
+
name: z.string().trim().min(1).max(200).optional(),
|
|
252
|
+
kind: BillingProfileKindSchema.optional()
|
|
253
|
+
});
|
|
254
|
+
/** Hosted akışta onaylatılabilecek en çok belge sayısı. */
|
|
255
|
+
const MAX_CHECKOUT_CONSENT_DOCS = 20;
|
|
256
|
+
const CheckoutSessionCreateInputSchema = z.object({
|
|
257
|
+
planCode: CodeSchema,
|
|
258
|
+
interval: IntervalSchema,
|
|
259
|
+
/** Verilmezse ürün ayarı `defaultCurrency` (eski gövdede zorunlu). */
|
|
260
|
+
currency: CurrencySchema.optional(),
|
|
261
|
+
/** Hosted: `tr` | `en`, verilmezse ürün ayarı `defaultLocale`. Eski gövdede zorunlu, iki harf. */
|
|
262
|
+
locale: z.string().regex(/^[a-z]{2}$/).optional(),
|
|
263
|
+
/**
|
|
264
|
+
* Sonuç bu adrese döner. Adreste yer tutucu (`{CHECKOUT_ID}`, `{STATUS}`,
|
|
265
|
+
* `{ACCOUNT_REF}`, `{LOCALE}`) varsa (yalnızca hosted) steward yalnızca onları
|
|
266
|
+
* doldurur, parametre eklemez; yoksa `?session=<id>&status=success|failed|pending`
|
|
267
|
+
* eklenir. pending: sağlayıcıya ulaşılamadı, oturum açık; ürün "işleniyor" gösterir.
|
|
268
|
+
* Origin'i ürün ayarlarında izinli olmalı (`appUrl` + `extraReturnOrigins`).
|
|
269
|
+
*/
|
|
270
|
+
successUrl: ReturnUrlSchema.optional(),
|
|
271
|
+
/** `successUrl`in eski adı. */
|
|
272
|
+
returnUrl: ReturnUrlSchema.optional(),
|
|
273
|
+
/** Yalnızca hosted: sayfadaki "Vazgeç" hedefi (yoksa ürün ayarı `appUrl`). Origin kuralı `successUrl`le aynı. */
|
|
274
|
+
cancelUrl: ReturnUrlSchema.optional(),
|
|
275
|
+
/** Hesap yoksa bu adla açılır; hesap varsa adı değiştirmez. */
|
|
276
|
+
displayName: z.string().min(1).max(200).optional(),
|
|
277
|
+
/** Yalnızca hosted. */
|
|
278
|
+
prefill: CheckoutPrefillSchema.optional(),
|
|
279
|
+
consents: z.array(CheckoutConsentInputSchema).optional(),
|
|
280
|
+
/** Yalnızca eski gövde. */
|
|
281
|
+
profile: BillingProfileSchema.optional(),
|
|
282
|
+
/**
|
|
283
|
+
* Yalnızca eski gövde: sağlayıcının istediği kimlik numarası (ör. TCKN/VKN).
|
|
284
|
+
* Yalnızca sağlayıcıya iletilir; servis bunu SAKLAMAZ ve LOGLAMAZ.
|
|
285
|
+
*/
|
|
286
|
+
identityNumber: z.string().regex(/^\d{10,11}$/).optional()
|
|
287
|
+
}).superRefine((body, ctx) => {
|
|
288
|
+
const issue = (path, message) => ctx.addIssue({
|
|
289
|
+
code: "custom",
|
|
290
|
+
path: [path],
|
|
291
|
+
message
|
|
292
|
+
});
|
|
293
|
+
if (body.successUrl !== void 0 && body.returnUrl !== void 0) issue("returnUrl", "successUrl ve returnUrl (eski ad) birlikte verilemez");
|
|
294
|
+
if (body.successUrl === void 0 && body.returnUrl === void 0) issue("successUrl", "zorunlu");
|
|
295
|
+
const hasProfile = body.profile !== void 0;
|
|
296
|
+
if (hasProfile !== (body.identityNumber !== void 0)) {
|
|
297
|
+
issue(hasProfile ? "identityNumber" : "profile", "eski gövde profile ve identityNumber'ı birlikte taşır; hosted checkout ikisini de taşımaz");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (hasProfile) {
|
|
301
|
+
if (body.currency === void 0) issue("currency", "eski gövdede zorunlu");
|
|
302
|
+
if (body.locale === void 0) issue("locale", "eski gövdede zorunlu");
|
|
303
|
+
if (body.consents === void 0) issue("consents", "eski gövdede zorunlu");
|
|
304
|
+
body.consents?.forEach((consent, i) => {
|
|
305
|
+
if (consent.acceptedAt === void 0) ctx.addIssue({
|
|
306
|
+
code: "custom",
|
|
307
|
+
path: [
|
|
308
|
+
"consents",
|
|
309
|
+
i,
|
|
310
|
+
"acceptedAt"
|
|
311
|
+
],
|
|
312
|
+
message: "eski gövdede zorunlu"
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
if (body.cancelUrl !== void 0) issue("cancelUrl", "yalnızca hosted checkout");
|
|
316
|
+
if (body.prefill !== void 0) issue("prefill", "yalnızca hosted checkout");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (body.locale !== void 0 && !SETTINGS_LOCALES.includes(body.locale)) issue("locale", `hosted checkout: ${SETTINGS_LOCALES.join(" | ")}`);
|
|
320
|
+
const consents = body.consents ?? [];
|
|
321
|
+
if (consents.length > 20) issue("consents", `en çok 20 belge`);
|
|
322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
323
|
+
consents.forEach((consent, i) => {
|
|
324
|
+
if (consent.url === void 0) ctx.addIssue({
|
|
325
|
+
code: "custom",
|
|
326
|
+
path: [
|
|
327
|
+
"consents",
|
|
328
|
+
i,
|
|
329
|
+
"url"
|
|
330
|
+
],
|
|
331
|
+
message: "hosted checkout'ta zorunlu"
|
|
332
|
+
});
|
|
333
|
+
if (consent.acceptedAt !== void 0) ctx.addIssue({
|
|
334
|
+
code: "custom",
|
|
335
|
+
path: [
|
|
336
|
+
"consents",
|
|
337
|
+
i,
|
|
338
|
+
"acceptedAt"
|
|
339
|
+
],
|
|
340
|
+
message: "hosted checkout'ta onayı sayfa kaydeder"
|
|
341
|
+
});
|
|
342
|
+
if (seen.has(consent.document)) ctx.addIssue({
|
|
343
|
+
code: "custom",
|
|
344
|
+
path: [
|
|
345
|
+
"consents",
|
|
346
|
+
i,
|
|
347
|
+
"document"
|
|
348
|
+
],
|
|
349
|
+
message: "belge adı tekrar ediyor"
|
|
350
|
+
});
|
|
351
|
+
seen.add(consent.document);
|
|
352
|
+
});
|
|
353
|
+
}).transform(({ returnUrl, successUrl, ...rest }) => ({
|
|
354
|
+
...rest,
|
|
355
|
+
successUrl: successUrl ?? returnUrl
|
|
356
|
+
}));
|
|
357
|
+
/** Doğrulanmış gövde eski akışı mı istiyor (K16). Hosted: false. */
|
|
358
|
+
function isLegacyCheckoutInput(input) {
|
|
359
|
+
return input.profile !== void 0 && input.identityNumber !== void 0;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* `canceled` (F2b): müşteri hosted sayfada vazgeçti. Eski (0.2.0 öncesi hosted'sız)
|
|
363
|
+
* istemci bu değeri tanımaz; yalnızca hosted akışı kullanan ürün görür.
|
|
364
|
+
*/
|
|
365
|
+
const CheckoutStatusSchema = z.enum([
|
|
366
|
+
"open",
|
|
367
|
+
"completed",
|
|
368
|
+
"failed",
|
|
369
|
+
"expired",
|
|
370
|
+
"canceled"
|
|
371
|
+
]);
|
|
372
|
+
const ProviderPayloadSchema = z.discriminatedUnion("type", [z.object({
|
|
373
|
+
type: z.literal("html_snippet"),
|
|
374
|
+
html: z.string()
|
|
375
|
+
}), z.object({
|
|
376
|
+
type: z.literal("redirect"),
|
|
377
|
+
url: z.url()
|
|
378
|
+
})]);
|
|
379
|
+
const CheckoutSessionSchema = z.object({
|
|
380
|
+
id: IdSchema,
|
|
381
|
+
accountRef: ExternalRefSchema,
|
|
382
|
+
status: CheckoutStatusSchema,
|
|
383
|
+
planCode: CodeSchema,
|
|
384
|
+
interval: IntervalSchema,
|
|
385
|
+
amountMinor: AmountMinorSchema,
|
|
386
|
+
currency: CurrencySchema,
|
|
387
|
+
provider: z.string(),
|
|
388
|
+
locale: z.string(),
|
|
389
|
+
/** Eski ad; `successUrl` ile aynı değer. */
|
|
390
|
+
returnUrl: z.string(),
|
|
391
|
+
successUrl: z.string().optional(),
|
|
392
|
+
/** Hosted: "Vazgeç" hedefi (verilmediyse null; sayfa ürün ayarı `appUrl`e döner). */
|
|
393
|
+
cancelUrl: z.string().nullable().optional(),
|
|
394
|
+
/** Kabul edilen belgeler (hosted: sayfada onaylandıktan sonra dolar). */
|
|
395
|
+
consents: z.array(ConsentSchema),
|
|
396
|
+
actorRef: ActorRefSchema.nullable(),
|
|
397
|
+
failureReason: z.string().nullable(),
|
|
398
|
+
subscriptionId: IdSchema.nullable(),
|
|
399
|
+
expiresAt: DateTimeSchema,
|
|
400
|
+
createdAt: DateTimeSchema,
|
|
401
|
+
completedAt: DateTimeSchema.nullable()
|
|
402
|
+
});
|
|
403
|
+
/**
|
|
404
|
+
* `POST /v1/accounts/{ref}/checkout-sessions` yanıtı.
|
|
405
|
+
* - hosted: `url` = steward checkout sayfası (`…/public/v1/checkout/{id}?k=<anahtar>`;
|
|
406
|
+
* anahtar yalnızca burada), `providerPayload` yok, `account` işlem sonrası durum.
|
|
407
|
+
* - eski gövde: `url: null`, `providerPayload` sağlayıcı formu.
|
|
408
|
+
* `url` F2b öncesi steward'da yoktur (null okunur).
|
|
409
|
+
*/
|
|
410
|
+
const CheckoutSessionCreatedSchema = CheckoutSessionSchema.extend({
|
|
411
|
+
url: z.string().nullable().default(null),
|
|
412
|
+
providerPayload: ProviderPayloadSchema.optional(),
|
|
413
|
+
account: AccountStateSchema.optional()
|
|
414
|
+
});
|
|
415
|
+
const RETURN_PLACEHOLDER_RE = /\{(CHECKOUT_ID|STATUS|ACCOUNT_REF|LOCALE)\}/g;
|
|
416
|
+
function originOf(value) {
|
|
417
|
+
try {
|
|
418
|
+
return new URL(value).origin;
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Checkout sonucunun tarayıcı yönlendirmesi — servis ve sahte steward aynı fonksiyonu
|
|
425
|
+
* kullanır. Eski oturum: `returnUrl?session=&status=`. Hosted: `successUrl`de yer tutucu
|
|
426
|
+
* (`{CHECKOUT_ID}` `{STATUS}` `{ACCOUNT_REF}` `{LOCALE}`) varsa YALNIZCA onlar doldurulur
|
|
427
|
+
* (değerler URL-kodlu), parametre eklenmez; yoksa eski biçim. Kodlu değer origin'i
|
|
428
|
+
* değiştiremez; yine de denetlenir, değişirse eski biçime düşülür.
|
|
429
|
+
*/
|
|
430
|
+
function checkoutRedirectUrl(target, status) {
|
|
431
|
+
const template = target.successUrl;
|
|
432
|
+
if (template !== null && template.match(RETURN_PLACEHOLDER_RE)) {
|
|
433
|
+
const values = {
|
|
434
|
+
CHECKOUT_ID: target.id,
|
|
435
|
+
STATUS: status,
|
|
436
|
+
ACCOUNT_REF: target.accountRef,
|
|
437
|
+
LOCALE: target.locale
|
|
438
|
+
};
|
|
439
|
+
const filled = template.replace(RETURN_PLACEHOLDER_RE, (_, name) => encodeURIComponent(values[name]));
|
|
440
|
+
if (originOf(filled) !== null && originOf(filled) === originOf(template)) return filled;
|
|
441
|
+
}
|
|
442
|
+
const url = new URL(template ?? target.returnUrl);
|
|
443
|
+
url.searchParams.set("session", target.id);
|
|
444
|
+
url.searchParams.set("status", status);
|
|
445
|
+
return url.toString();
|
|
446
|
+
}
|
|
447
|
+
const CHECKOUT_ERRORS = [
|
|
448
|
+
"account_not_found",
|
|
449
|
+
"already_subscribed",
|
|
450
|
+
"plan_not_sellable",
|
|
451
|
+
"return_url_not_allowed",
|
|
452
|
+
"provider_not_configured",
|
|
453
|
+
"provider_error",
|
|
454
|
+
"account_deleted"
|
|
455
|
+
];
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region ../contract/src/portal.ts
|
|
458
|
+
const PortalSessionCreateInputSchema = z.object({
|
|
459
|
+
/** Sayfa dili; verilmezse ürün ayarı `defaultLocale`. */
|
|
460
|
+
locale: SettingsLocaleSchema.optional(),
|
|
461
|
+
/**
|
|
462
|
+
* "Ürüne dön" hedefi; verilmezse ürün ayarı `appUrl`. Origin'i ürün ayarlarında
|
|
463
|
+
* izinli olmalı (`appUrl` + `extraReturnOrigins`), değilse 422 `return_url_not_allowed`.
|
|
464
|
+
*/
|
|
465
|
+
returnUrl: z.url({ protocol: /^https?$/ }).max(2e3).optional()
|
|
466
|
+
});
|
|
467
|
+
/** `POST /v1/accounts/{ref}/portal-sessions` yanıtı (201). */
|
|
468
|
+
const PortalSessionSchema = z.object({
|
|
469
|
+
id: IdSchema,
|
|
470
|
+
/** Steward portal sayfası (sayfa anahtarı dahil); tarayıcı buraya yönlendirilir. */
|
|
471
|
+
url: z.string(),
|
|
472
|
+
expiresAt: DateTimeSchema
|
|
473
|
+
});
|
|
474
|
+
const PORTAL_SESSION_ERRORS = ["account_not_found", "return_url_not_allowed"];
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region ../contract/src/subscriptions.ts
|
|
477
|
+
const SubscriptionSchema = z.object({
|
|
478
|
+
id: IdSchema,
|
|
479
|
+
accountRef: ExternalRefSchema,
|
|
480
|
+
status: SubscriptionStatusSchema,
|
|
481
|
+
planCode: CodeSchema,
|
|
482
|
+
priceId: IdSchema,
|
|
483
|
+
interval: IntervalSchema,
|
|
484
|
+
amountMinor: AmountMinorSchema,
|
|
485
|
+
currency: CurrencySchema,
|
|
486
|
+
provider: z.string(),
|
|
487
|
+
providerSubscriptionRef: z.string(),
|
|
488
|
+
cancelAtPeriodEnd: z.boolean(),
|
|
489
|
+
cancelAt: DateTimeSchema.nullable(),
|
|
490
|
+
cancelReason: z.string().nullable(),
|
|
491
|
+
canceledAt: DateTimeSchema.nullable(),
|
|
492
|
+
startedAt: DateTimeSchema,
|
|
493
|
+
currentPeriodStart: DateTimeSchema.nullable(),
|
|
494
|
+
currentPeriodEnd: DateTimeSchema.nullable(),
|
|
495
|
+
/** Ödeme alınamadığı günden beri dunning'de ise başlangıcı. */
|
|
496
|
+
dunningStartedAt: DateTimeSchema.nullable()
|
|
497
|
+
});
|
|
498
|
+
const CancelSubscriptionInputSchema = z.object({
|
|
499
|
+
/** at_period_end: haklar dönem sonuna kadar sürer; immediate: hemen varsayılan plana düşer. */
|
|
500
|
+
mode: z.enum(["at_period_end", "immediate"]),
|
|
501
|
+
reason: z.string().min(3).max(500),
|
|
502
|
+
/** Sağlayıcıda zaten iptal edildiyse (panelden) sağlayıcı çağrısını atla. */
|
|
503
|
+
skipProvider: z.boolean().default(false)
|
|
504
|
+
});
|
|
505
|
+
const PaymentSchema = z.object({
|
|
506
|
+
id: IdSchema,
|
|
507
|
+
subscriptionId: IdSchema,
|
|
508
|
+
providerOrderRef: z.string(),
|
|
509
|
+
status: z.enum(["succeeded", "failed"]),
|
|
510
|
+
amountMinor: AmountMinorSchema,
|
|
511
|
+
currency: CurrencySchema,
|
|
512
|
+
periodStart: DateTimeSchema.nullable(),
|
|
513
|
+
periodEnd: DateTimeSchema.nullable(),
|
|
514
|
+
occurredAt: DateTimeSchema
|
|
515
|
+
});
|
|
516
|
+
const GrantCreateInputSchema = z.object({
|
|
517
|
+
planCode: CodeSchema.optional(),
|
|
518
|
+
/** Plan değerlerinin üzerine yazılan tek tek haklar. */
|
|
519
|
+
overrides: EntitlementsSchema.default({}),
|
|
520
|
+
reason: z.string().min(3).max(500),
|
|
521
|
+
startsAt: DateTimeSchema.optional(),
|
|
522
|
+
endsAt: DateTimeSchema.optional()
|
|
523
|
+
}).refine((g) => g.planCode !== void 0 || Object.keys(g.overrides).length > 0, { message: "planCode ya da en az bir override gerekli" });
|
|
524
|
+
const GrantSchema = z.object({
|
|
525
|
+
id: IdSchema,
|
|
526
|
+
accountRef: ExternalRefSchema,
|
|
527
|
+
planCode: CodeSchema.nullable(),
|
|
528
|
+
overrides: EntitlementsSchema,
|
|
529
|
+
reason: z.string(),
|
|
530
|
+
actorRef: ActorRefSchema.nullable(),
|
|
531
|
+
startsAt: DateTimeSchema,
|
|
532
|
+
endsAt: DateTimeSchema.nullable(),
|
|
533
|
+
revokedAt: DateTimeSchema.nullable(),
|
|
534
|
+
createdAt: DateTimeSchema
|
|
535
|
+
});
|
|
536
|
+
const StatsSchema = z.object({
|
|
537
|
+
subscriptions: z.record(SubscriptionStatusSchema, z.number().int().nonnegative()),
|
|
538
|
+
/** Aylık yinelenen gelir, para birimine göre (yıllık fiyat / 12, yuvarlanmış). */
|
|
539
|
+
mrrMinor: z.record(z.string(), AmountMinorSchema),
|
|
540
|
+
accounts: z.number().int().nonnegative()
|
|
541
|
+
});
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region ../contract/src/invoices.ts
|
|
544
|
+
const InvoiceStatusSchema = z.enum([
|
|
545
|
+
"draft",
|
|
546
|
+
"issued",
|
|
547
|
+
"void"
|
|
548
|
+
]);
|
|
549
|
+
const InvoiceLineSchema = z.object({
|
|
550
|
+
description: z.string(),
|
|
551
|
+
quantity: z.number().int().positive(),
|
|
552
|
+
unitAmountMinor: AmountMinorSchema,
|
|
553
|
+
amountMinor: AmountMinorSchema
|
|
554
|
+
});
|
|
555
|
+
const InvoiceSchema = z.object({
|
|
556
|
+
id: IdSchema,
|
|
557
|
+
accountRef: ExternalRefSchema,
|
|
558
|
+
subscriptionId: IdSchema.nullable(),
|
|
559
|
+
paymentId: IdSchema.nullable(),
|
|
560
|
+
status: InvoiceStatusSchema,
|
|
561
|
+
number: z.string().nullable(),
|
|
562
|
+
currency: CurrencySchema,
|
|
563
|
+
subtotalMinor: AmountMinorSchema,
|
|
564
|
+
taxMinor: AmountMinorSchema,
|
|
565
|
+
totalMinor: AmountMinorSchema,
|
|
566
|
+
taxRateBps: z.number().int().min(0).max(1e4),
|
|
567
|
+
/** Ödeme anındaki fatura profili (sonradan profil değişse de fatura değişmez). */
|
|
568
|
+
buyer: BillingProfileSchema,
|
|
569
|
+
lines: z.array(InvoiceLineSchema),
|
|
570
|
+
issuer: z.string(),
|
|
571
|
+
externalRef: z.string().nullable(),
|
|
572
|
+
hasDocument: z.boolean(),
|
|
573
|
+
issuedAt: DateTimeSchema.nullable(),
|
|
574
|
+
createdAt: DateTimeSchema,
|
|
575
|
+
/** Manuel faturanın vadesi (bilgi; steward tahsil etmez). */
|
|
576
|
+
dueAt: DateTimeSchema.nullable().optional(),
|
|
577
|
+
/** Manuel faturanın notu (ör. sözleşme ya da teklif referansı). */
|
|
578
|
+
note: z.string().nullable().optional(),
|
|
579
|
+
voidedAt: DateTimeSchema.nullable().optional(),
|
|
580
|
+
voidReason: z.string().nullable().optional()
|
|
581
|
+
});
|
|
582
|
+
const InvoiceIssueInputSchema = z.object({
|
|
583
|
+
number: z.string().min(1).max(100),
|
|
584
|
+
issuedAt: DateTimeSchema.optional(),
|
|
585
|
+
/** PDF, base64 (en fazla ~5 MB). */
|
|
586
|
+
documentBase64: z.base64().max(7e6).optional(),
|
|
587
|
+
externalRef: z.string().max(200).optional()
|
|
588
|
+
}).refine((i) => i.documentBase64 !== void 0 || i.externalRef !== void 0, { message: "belge ya da harici referans gerekli" });
|
|
589
|
+
/** Manuel faturada satır tutarları toplamının üst sınırı: vergi hesabı `Number` içinde tam kalır. */
|
|
590
|
+
const MANUAL_INVOICE_MAX_AMOUNT_MINOR = 9e11;
|
|
591
|
+
const ManualInvoiceLineInputSchema = z.object({
|
|
592
|
+
description: z.string().trim().min(1).max(500),
|
|
593
|
+
quantity: z.number().int().min(1).max(1e6),
|
|
594
|
+
/** Birim fiyat, en küçük birimde; `taxInclusive` ise KDV dahil. */
|
|
595
|
+
unitAmountMinor: AmountMinorSchema.max(MANUAL_INVOICE_MAX_AMOUNT_MINOR)
|
|
596
|
+
});
|
|
597
|
+
/**
|
|
598
|
+
* `POST /v1/accounts/{ref}/invoices` — manuel taslak fatura. Alıcı hesabın o
|
|
599
|
+
* anki fatura profilinin kopyasıdır (profil yoksa 422 `billing_profile_required`).
|
|
600
|
+
* Para birimi katalogdan bağımsızdır (Enterprise teklifi katalog dışıdır).
|
|
601
|
+
* Tutarlar `manualInvoiceTotals` ile hesaplanır.
|
|
602
|
+
*/
|
|
603
|
+
const ManualInvoiceInputSchema = z.object({
|
|
604
|
+
lines: z.array(ManualInvoiceLineInputSchema).min(1).max(100),
|
|
605
|
+
currency: CurrencySchema,
|
|
606
|
+
/** Baz puan: %20 = 2000. */
|
|
607
|
+
taxRateBps: z.number().int().min(0).max(1e4),
|
|
608
|
+
/** Katalog fiyatıyla aynı anlam; varsayılan true: birim fiyatlar KDV dahil, vergi toplamdan ayrıştırılır. */
|
|
609
|
+
taxInclusive: z.boolean().default(true),
|
|
610
|
+
dueAt: DateTimeSchema.optional(),
|
|
611
|
+
note: z.string().trim().min(1).max(1e3).optional()
|
|
612
|
+
}).refine((i) => i.lines.reduce((sum, l) => sum + l.quantity * l.unitAmountMinor, 0) <= MANUAL_INVOICE_MAX_AMOUNT_MINOR, {
|
|
613
|
+
message: "toplam tutar çok büyük",
|
|
614
|
+
path: ["lines"]
|
|
615
|
+
});
|
|
616
|
+
/** `POST /v1/invoices/{id}/void` — yalnızca manuel fatura; numara serbest kalmaz, belge silinmez. */
|
|
617
|
+
const InvoiceVoidInputSchema = z.object({ reason: z.string().trim().min(3).max(500) });
|
|
618
|
+
/**
|
|
619
|
+
* Vergi dahil tutardan matrah ve vergiyi ayrıştırır (tam sayı, kuruşa
|
|
620
|
+
* yuvarlama). Toplam her zaman korunur: subtotal + tax === total.
|
|
621
|
+
*/
|
|
622
|
+
function splitInclusiveTax(totalMinor, taxRateBps) {
|
|
623
|
+
if (!Number.isInteger(totalMinor) || totalMinor < 0) throw new Error(`tutar tam sayı olmalı: ${totalMinor}`);
|
|
624
|
+
if (!Number.isInteger(taxRateBps) || taxRateBps < 0) throw new Error(`oran tam sayı bps olmalı: ${taxRateBps}`);
|
|
625
|
+
const subtotalMinor = Math.round(totalMinor * 1e4 / (1e4 + taxRateBps));
|
|
626
|
+
return {
|
|
627
|
+
subtotalMinor,
|
|
628
|
+
taxMinor: totalMinor - subtotalMinor
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Manuel faturanın tutarları ve saklanan satırları.
|
|
633
|
+
*
|
|
634
|
+
* Ödemeli faturanın kuralı, fatura düzeyinde TEK yuvarlamayla: satır tutarları
|
|
635
|
+
* (adet × birim) toplanır; `taxInclusive` ise toplam korunur ve matrah/vergi
|
|
636
|
+
* `splitInclusiveTax` ile ayrıştırılır, değilse vergi matrahın üstüne eklenir
|
|
637
|
+
* (`Math.round`). Tek satır ve adet 1 girdi, aynı tutarlı ödemeli faturanın
|
|
638
|
+
* toplamlarını ve satırını birebir üretir.
|
|
639
|
+
*
|
|
640
|
+
* Saklanan satırlar ödemeli faturadaki gibi KDV HARİÇTİR ve tutarları matrahı
|
|
641
|
+
* tam verir: vergi dahil girdide matrah satırlara tutarları oranında dağıtılır
|
|
642
|
+
* (en büyük kalan; eşitlikte önceki satır). Satırın `amountMinor`'u esastır;
|
|
643
|
+
* adet > 1 ise `unitAmountMinor` onun yuvarlanmış birim payıdır.
|
|
644
|
+
*/
|
|
645
|
+
function manualInvoiceTotals(input) {
|
|
646
|
+
const amounts = input.lines.map((l) => l.quantity * l.unitAmountMinor);
|
|
647
|
+
const amountMinor = amounts.reduce((sum, a) => sum + a, 0);
|
|
648
|
+
if (!input.taxInclusive) {
|
|
649
|
+
const taxMinor = Math.round(amountMinor * input.taxRateBps / 1e4);
|
|
650
|
+
return {
|
|
651
|
+
subtotalMinor: amountMinor,
|
|
652
|
+
taxMinor,
|
|
653
|
+
totalMinor: amountMinor + taxMinor,
|
|
654
|
+
lines: input.lines.map((l, i) => ({
|
|
655
|
+
description: l.description,
|
|
656
|
+
quantity: l.quantity,
|
|
657
|
+
unitAmountMinor: l.unitAmountMinor,
|
|
658
|
+
amountMinor: amounts[i]
|
|
659
|
+
}))
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const { subtotalMinor, taxMinor } = splitInclusiveTax(amountMinor, input.taxRateBps);
|
|
663
|
+
const net = allocate(subtotalMinor, amounts);
|
|
664
|
+
return {
|
|
665
|
+
subtotalMinor,
|
|
666
|
+
taxMinor,
|
|
667
|
+
totalMinor: amountMinor,
|
|
668
|
+
lines: input.lines.map((l, i) => ({
|
|
669
|
+
description: l.description,
|
|
670
|
+
quantity: l.quantity,
|
|
671
|
+
unitAmountMinor: Math.round(net[i] / l.quantity),
|
|
672
|
+
amountMinor: net[i]
|
|
673
|
+
}))
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** `total`'ı ağırlıklar oranında tam sayılara böler; toplam korunur (en büyük kalan). BigInt: çarpım 2^53'ü aşabilir. */
|
|
677
|
+
function allocate(total, weights) {
|
|
678
|
+
const sum = BigInt(weights.reduce((s, w) => s + w, 0));
|
|
679
|
+
if (sum === 0n) return weights.map(() => 0);
|
|
680
|
+
const shares = weights.map((w, index) => {
|
|
681
|
+
const scaled = BigInt(total) * BigInt(w);
|
|
682
|
+
return {
|
|
683
|
+
index,
|
|
684
|
+
base: scaled / sum,
|
|
685
|
+
remainder: scaled % sum
|
|
686
|
+
};
|
|
687
|
+
});
|
|
688
|
+
let left = BigInt(total) - shares.reduce((s, x) => s + x.base, 0n);
|
|
689
|
+
const order = [...shares].sort((a, b) => a.remainder === b.remainder ? a.index - b.index : a.remainder > b.remainder ? -1 : 1);
|
|
690
|
+
for (const share of order) {
|
|
691
|
+
if (left === 0n) break;
|
|
692
|
+
share.base += 1n;
|
|
693
|
+
left -= 1n;
|
|
694
|
+
}
|
|
695
|
+
return shares.map((x) => Number(x.base));
|
|
696
|
+
}
|
|
697
|
+
//#endregion
|
|
698
|
+
//#region ../contract/src/api.ts
|
|
699
|
+
/** `GET /v1/accounts/{ref}?include=…` — istenmeyen alanlar yanıtta yoktur. */
|
|
700
|
+
const AccountDetailSchema = AccountSchema.extend({
|
|
701
|
+
profile: BillingProfileSchema.nullable().optional(),
|
|
702
|
+
subscriptions: z.array(SubscriptionSchema).optional(),
|
|
703
|
+
checkouts: z.array(CheckoutSessionSchema).optional(),
|
|
704
|
+
payments: z.array(PaymentSchema).optional(),
|
|
705
|
+
invoices: z.array(InvoiceSchema).optional(),
|
|
706
|
+
grants: z.array(GrantSchema).optional()
|
|
707
|
+
});
|
|
708
|
+
const withAccount = { account: AccountStateSchema.optional() };
|
|
709
|
+
/** `PUT /v1/accounts/{ref}` (201 ya da 200). */
|
|
710
|
+
const AccountUpsertResponseSchema = AccountSchema.extend(withAccount);
|
|
711
|
+
/** `PUT /v1/accounts/{ref}/billing-profile` */
|
|
712
|
+
const BillingProfileResponseSchema = BillingProfileSchema.extend(withAccount);
|
|
713
|
+
/** `POST /v1/accounts/{ref}/grants` */
|
|
714
|
+
const GrantCreateResponseSchema = GrantSchema.extend(withAccount);
|
|
715
|
+
/** `POST /v1/subscriptions/{id}/cancel` */
|
|
716
|
+
const CancelSubscriptionResponseSchema = SubscriptionSchema.extend(withAccount);
|
|
717
|
+
/** `POST /v1/invoices/{id}/issue` */
|
|
718
|
+
const InvoiceIssueResponseSchema = InvoiceSchema.extend(withAccount);
|
|
719
|
+
/** `POST /v1/accounts/{ref}/invoices` (201; F4f). Durum değişmez: mevcut durum. */
|
|
720
|
+
const ManualInvoiceResponseSchema = InvoiceSchema.extend(withAccount);
|
|
721
|
+
/** `POST /v1/invoices/{id}/void` (F4f). Durum değişmez: mevcut durum. */
|
|
722
|
+
const InvoiceVoidResponseSchema = InvoiceSchema.extend(withAccount);
|
|
723
|
+
/** `GET /v1/checkout-sessions/{id}` — `account` yalnızca oturum `completed` ise. */
|
|
724
|
+
const CheckoutSessionResponseSchema = CheckoutSessionSchema.extend(withAccount);
|
|
725
|
+
const DeliverySchema = z.object({
|
|
726
|
+
endpointId: IdSchema,
|
|
727
|
+
status: z.enum([
|
|
728
|
+
"pending",
|
|
729
|
+
"delivered",
|
|
730
|
+
"dead"
|
|
731
|
+
]),
|
|
732
|
+
attempts: z.number().int().nonnegative(),
|
|
733
|
+
lastHttpStatus: z.number().int().nullable(),
|
|
734
|
+
lastError: z.string().nullable(),
|
|
735
|
+
nextAttemptAt: DateTimeSchema.nullable(),
|
|
736
|
+
deliveredAt: DateTimeSchema.nullable()
|
|
737
|
+
});
|
|
738
|
+
const EventRecordSchema = z.object({
|
|
739
|
+
event: BillingEventSchema,
|
|
740
|
+
deliveries: z.array(DeliverySchema)
|
|
741
|
+
});
|
|
742
|
+
const EventListSchema = z.object({
|
|
743
|
+
items: z.array(EventRecordSchema),
|
|
744
|
+
nextCursor: z.string().nullable()
|
|
745
|
+
});
|
|
746
|
+
/** Değişiklik yapan her istekte zorunlu başlıklar. */
|
|
747
|
+
const HEADER_IDEMPOTENCY_KEY = "idempotency-key";
|
|
748
|
+
const HEADER_ACTOR_REF = "x-actor-ref";
|
|
749
|
+
const HEADER_API_VERSION = "steward-api-version";
|
|
750
|
+
const HEADER_SDK = "x-steward-sdk";
|
|
751
|
+
const HEADER_REQUEST_ID = "x-request-id";
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region ../contract/src/errors.ts
|
|
754
|
+
const spec = (httpStatus, category, retryable = false) => ({
|
|
755
|
+
httpStatus,
|
|
756
|
+
category,
|
|
757
|
+
retryable
|
|
758
|
+
});
|
|
759
|
+
const ERROR_CODES = Object.freeze({
|
|
760
|
+
invalid_json: spec(400, "validation"),
|
|
761
|
+
invalid_request: spec(400, "validation"),
|
|
762
|
+
invalid_account_ref: spec(400, "validation"),
|
|
763
|
+
invalid_include: spec(400, "validation"),
|
|
764
|
+
invalid_cursor: spec(400, "validation"),
|
|
765
|
+
invalid_limit: spec(400, "validation"),
|
|
766
|
+
invalid_actor_ref: spec(400, "validation"),
|
|
767
|
+
actor_ref_required: spec(400, "validation"),
|
|
768
|
+
idempotency_key_required: spec(400, "validation"),
|
|
769
|
+
invalid_idempotency_key: spec(400, "validation"),
|
|
770
|
+
/** Anahtar yok / iptal / yanlış — neden ayrıştırılmaz. */
|
|
771
|
+
unauthorized: spec(401, "config"),
|
|
772
|
+
/** Admin kapsamı gereken uç. */
|
|
773
|
+
forbidden: spec(403, "config"),
|
|
774
|
+
/** Aynı anahtarlı önceki istek sürüyor; aynı anahtarla sonra tekrar dene. */
|
|
775
|
+
request_in_progress: spec(409, "conflict", true),
|
|
776
|
+
/** Anahtar farklı bir istekte kullanılmış. */
|
|
777
|
+
idempotency_key_reused: spec(422, "conflict"),
|
|
778
|
+
/** Bilinmeyen yol ya da biçimi geçersiz fatura id'si. */
|
|
779
|
+
not_found: spec(404, "not_found"),
|
|
780
|
+
account_not_found: spec(404, "not_found"),
|
|
781
|
+
subscription_not_found: spec(404, "not_found"),
|
|
782
|
+
grant_not_found: spec(404, "not_found"),
|
|
783
|
+
invoice_not_found: spec(404, "not_found"),
|
|
784
|
+
document_not_found: spec(404, "not_found"),
|
|
785
|
+
event_not_found: spec(404, "not_found"),
|
|
786
|
+
checkout_session_not_found: spec(404, "not_found"),
|
|
787
|
+
/** Yönetim ucu: endpoint yok ya da başka ürünün. */
|
|
788
|
+
webhook_endpoint_not_found: spec(404, "not_found"),
|
|
789
|
+
/** Ürün için henüz katalog senkronlanmadı (404 ama kurulum eksiği). */
|
|
790
|
+
catalog_not_configured: spec(404, "config"),
|
|
791
|
+
plan_missing_feature: spec(422, "validation"),
|
|
792
|
+
/** Katalog senkronu ve grant override'ı. */
|
|
793
|
+
override_type_mismatch: spec(422, "validation"),
|
|
794
|
+
/** Grant girdisindeki plan katalogda yok. */
|
|
795
|
+
plan_not_found: spec(422, "validation"),
|
|
796
|
+
invalid_window: spec(422, "validation"),
|
|
797
|
+
already_subscribed: spec(409, "conflict"),
|
|
798
|
+
already_canceled: spec(409, "conflict"),
|
|
799
|
+
/** `PUT …/plan` `mode: "strict"`: canlı aboneliğin planı hedefi geçiyor; hiçbir şey yazılmadı. */
|
|
800
|
+
subscription_plan_higher: spec(409, "conflict"),
|
|
801
|
+
plan_not_sellable: spec(422, "validation"),
|
|
802
|
+
/**
|
|
803
|
+
* F4e: hesap silinmiş (anonimleştirilmiş). `/v1/accounts/:ref` altındaki her uç (tekrar
|
|
804
|
+
* `DELETE` ve aynı ref'le `PUT` dahil) 410 döner; hesap geri açılmaz.
|
|
805
|
+
*/
|
|
806
|
+
account_deleted: spec(410, "not_found"),
|
|
807
|
+
/** `DELETE /v1/accounts/:ref`: canlı abonelik (dönem sonunda iptali planlı dahil) var; önce hemen iptal. */
|
|
808
|
+
subscription_active: spec(409, "conflict"),
|
|
809
|
+
/** `DELETE /v1/accounts/:ref`: sağlayıcı formu açık checkout oturumu var; süresi dolup kapanınca tekrar dene. */
|
|
810
|
+
checkout_in_progress: spec(409, "conflict", true),
|
|
811
|
+
/** `returnUrl` ürünün izinli origin'lerinde değil: ürün yapılandırması hatalı. */
|
|
812
|
+
return_url_not_allowed: spec(422, "config"),
|
|
813
|
+
/** Bu deployment'ta sağlayıcı yapılandırılmamış (503 ama beklemekle düzelmez). */
|
|
814
|
+
provider_not_configured: spec(503, "config"),
|
|
815
|
+
provider_error: spec(502, "provider"),
|
|
816
|
+
already_issued: spec(409, "conflict"),
|
|
817
|
+
number_taken: spec(409, "conflict"),
|
|
818
|
+
invalid_document: spec(422, "validation"),
|
|
819
|
+
/** Manuel fatura (F4f): hesabın fatura profili yok (alıcı profilin kopyasıdır). */
|
|
820
|
+
billing_profile_required: spec(422, "conflict"),
|
|
821
|
+
/** Void: fatura zaten `void`. */
|
|
822
|
+
invoice_not_voidable: spec(409, "conflict"),
|
|
823
|
+
/** Void: ödemeli fatura; iptali iade yolundadır (F4d). */
|
|
824
|
+
invoice_has_payment: spec(409, "conflict"),
|
|
825
|
+
internal_error: spec(500, "unavailable", true),
|
|
826
|
+
/** `/health`: veritabanına ulaşılamıyor. */
|
|
827
|
+
database_unavailable: spec(503, "unavailable", true),
|
|
828
|
+
/** F2c (K26): anahtar prefix'i başına bütçe aşıldı; `Retry-After` başlığı taşır. */
|
|
829
|
+
rate_limited: spec(429, "unavailable", true),
|
|
830
|
+
unknown_provider: spec(404, "not_found"),
|
|
831
|
+
bad_signature: spec(401, "config"),
|
|
832
|
+
invalid_payload: spec(400, "validation"),
|
|
833
|
+
stale_event: spec(400, "validation"),
|
|
834
|
+
not_configured: spec(503, "config"),
|
|
835
|
+
/** Yalnızca sahte sağlayıcı (yerel/test). */
|
|
836
|
+
unknown_subscription: spec(404, "not_found")
|
|
837
|
+
});
|
|
838
|
+
function isKnownErrorCode(code) {
|
|
839
|
+
return Object.hasOwn(ERROR_CODES, code);
|
|
840
|
+
}
|
|
841
|
+
/** Bilinen kodun tanımı; bilinmeyen kod için null. */
|
|
842
|
+
function errorCodeSpec(code) {
|
|
843
|
+
return isKnownErrorCode(code) ? ERROR_CODES[code] : null;
|
|
844
|
+
}
|
|
845
|
+
//#endregion
|
|
846
|
+
//#region ../contract/src/plan.ts
|
|
847
|
+
const PLAN_SET_MODES = ["ensure", "strict"];
|
|
848
|
+
const PlanSetModeSchema = z.enum(PLAN_SET_MODES);
|
|
849
|
+
const PlanSetInputSchema = z.object({
|
|
850
|
+
/** Hedef plan; `null` elle verilmiş planı kaldırır (hesap aboneliğe ya da varsayılan plana döner). Alan zorunludur. */
|
|
851
|
+
planCode: CodeSchema.nullable(),
|
|
852
|
+
reason: z.string().min(3).max(500),
|
|
853
|
+
mode: PlanSetModeSchema.default("ensure"),
|
|
854
|
+
/** Planın bitişi; verilmezse süresiz. */
|
|
855
|
+
endsAt: DateTimeSchema.optional(),
|
|
856
|
+
/** Plan değerlerinin üzerine yazılan tek tek haklar (grant override'ı ile aynı tip kuralı). */
|
|
857
|
+
overrides: EntitlementsSchema.default({})
|
|
858
|
+
}).superRefine((input, ctx) => {
|
|
859
|
+
if (input.planCode !== null) return;
|
|
860
|
+
if (input.endsAt !== void 0) ctx.addIssue({
|
|
861
|
+
code: "custom",
|
|
862
|
+
path: ["endsAt"],
|
|
863
|
+
message: "planCode null iken verilemez"
|
|
864
|
+
});
|
|
865
|
+
if (Object.keys(input.overrides).length > 0) ctx.addIssue({
|
|
866
|
+
code: "custom",
|
|
867
|
+
path: ["overrides"],
|
|
868
|
+
message: "planCode null iken verilemez"
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
const PlanSetWarningSchema = z.object({
|
|
872
|
+
code: z.string().min(1).max(100),
|
|
873
|
+
message: z.string()
|
|
874
|
+
});
|
|
875
|
+
/** `PUT /v1/accounts/{ref}/plan` (200). */
|
|
876
|
+
const PlanSetResponseSchema = z.object({
|
|
877
|
+
/** Hesabın geçerli plan grant'i; `planCode: null` ise null. */
|
|
878
|
+
grant: GrantSchema.nullable(),
|
|
879
|
+
/** İşlem sonrası hesap durumu (K5). */
|
|
880
|
+
account: AccountStateSchema,
|
|
881
|
+
warnings: z.array(PlanSetWarningSchema)
|
|
882
|
+
});
|
|
883
|
+
const ResyncResultSchema = z.object({
|
|
884
|
+
queued: z.literal(true),
|
|
885
|
+
/** Yazılan `account.state_changed` event'inin id'si. */
|
|
886
|
+
eventId: EventIdSchema.optional()
|
|
887
|
+
});
|
|
888
|
+
//#endregion
|
|
889
|
+
//#region ../contract/src/webhook-endpoints.ts
|
|
890
|
+
const WEBHOOK_SECRET_PREFIX = "whsec_";
|
|
891
|
+
const SECRET_MARKERS = [WEBHOOK_SECRET_PREFIX, "stw_"];
|
|
892
|
+
const hasSecretMarker = (value) => SECRET_MARKERS.some((marker) => value.includes(marker));
|
|
893
|
+
/**
|
|
894
|
+
* `whsec_` + en az 32 karakter (base64/base64url alfabesi; virgül ve boşluk yok —
|
|
895
|
+
* ürün sırları `yeni,eski` listesi olarak tutar). Düşük çeşitlilikteki değer
|
|
896
|
+
* (ör. tek karakter tekrarı) reddedilir; `generateWebhookSecret` 32 rastgele bayt üretir.
|
|
897
|
+
*/
|
|
898
|
+
const WebhookSecretSchema = z.string().regex(/^whsec_[A-Za-z0-9_\-+/=]{32,200}$/, "whsec_ + en az 32 karakter (A-Z a-z 0-9 _ - + / =)").refine((value) => new Set(value.slice(6)).size >= 8, "sır yeterince rastgele değil");
|
|
899
|
+
const EventTypeSchema = z.enum(EVENT_TYPES);
|
|
900
|
+
const WEBHOOK_ENDPOINT_STATUSES = [
|
|
901
|
+
"active",
|
|
902
|
+
"paused",
|
|
903
|
+
"inactive"
|
|
904
|
+
];
|
|
905
|
+
const WebhookEndpointStatusSchema = z.enum(WEBHOOK_ENDPOINT_STATUSES);
|
|
906
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
907
|
+
"localhost",
|
|
908
|
+
"127.0.0.1",
|
|
909
|
+
"[::1]"
|
|
910
|
+
]);
|
|
911
|
+
const CLUSTER_SUFFIX = ".svc.cluster.local";
|
|
912
|
+
/**
|
|
913
|
+
* Adres politikası; uygunsa null, değilse nedeni. `allowLoopback` üretimde false.
|
|
914
|
+
* Şemadan ayrıdır: üretim bilgisi yalnızca serviste var (servis ve sahte steward
|
|
915
|
+
* gövde şemasına `superRefine` ile ekler).
|
|
916
|
+
*/
|
|
917
|
+
function webhookEndpointUrlProblem(value, options) {
|
|
918
|
+
let url;
|
|
919
|
+
try {
|
|
920
|
+
url = new URL(value);
|
|
921
|
+
} catch {
|
|
922
|
+
return "geçerli bir adres değil";
|
|
923
|
+
}
|
|
924
|
+
if (url.username !== "" || url.password !== "") return "adreste kullanıcı bilgisi olamaz";
|
|
925
|
+
if (url.protocol === "https:") return null;
|
|
926
|
+
const host = url.hostname.toLowerCase();
|
|
927
|
+
if (url.protocol === "http:") {
|
|
928
|
+
if (host.endsWith(CLUSTER_SUFFIX) && host.length > 18) return null;
|
|
929
|
+
if (options.allowLoopback && LOOPBACK_HOSTS.has(host)) return null;
|
|
930
|
+
}
|
|
931
|
+
return options.allowLoopback ? "https://, cluster içi http://*.svc.cluster.local ya da http://localhost olmalı" : "https:// ya da cluster içi http://*.svc.cluster.local olmalı";
|
|
932
|
+
}
|
|
933
|
+
const EndpointUrlSchema = z.url({ protocol: /^https?$/ }).max(2e3).refine((value) => !hasSecretMarker(value), "adres sır içeremez");
|
|
934
|
+
const DescriptionSchema = z.string().trim().min(1).max(200).refine((value) => !hasSecretMarker(value), "açıklama sır içeremez");
|
|
935
|
+
/** `POST /v1/admin/webhook-endpoints`. `eventTypes` verilmezse yalnızca `account.state_changed` (plan §7.5). */
|
|
936
|
+
const WebhookEndpointCreateInputSchema = z.object({
|
|
937
|
+
url: EndpointUrlSchema,
|
|
938
|
+
secret: WebhookSecretSchema,
|
|
939
|
+
eventTypes: z.array(EventTypeSchema).min(1).transform((types) => [...new Set(types)]).optional(),
|
|
940
|
+
description: DescriptionSchema.optional()
|
|
941
|
+
}).strict();
|
|
942
|
+
/** Yönetim ucuyla açılan endpoint'in varsayılan filtresi. */
|
|
943
|
+
const DEFAULT_ENDPOINT_EVENT_TYPES = Object.freeze(["account.state_changed"]);
|
|
944
|
+
/** `POST /v1/admin/webhook-endpoints/{id}/rotate`. */
|
|
945
|
+
const WebhookEndpointRotateInputSchema = z.object({ secret: WebhookSecretSchema }).strict();
|
|
946
|
+
/**
|
|
947
|
+
* Endpoint'in en son DENENMİŞ teslimatı. `at`: teslim edildiyse teslim anı, değilse
|
|
948
|
+
* teslimatın kuyruğa girdiği an. `httpStatus` 401 = ürün yanlış sırla doğruluyor.
|
|
949
|
+
*/
|
|
950
|
+
const WebhookLastDeliverySchema = z.object({
|
|
951
|
+
status: z.enum([
|
|
952
|
+
"pending",
|
|
953
|
+
"delivered",
|
|
954
|
+
"dead"
|
|
955
|
+
]),
|
|
956
|
+
httpStatus: z.number().int().nullable(),
|
|
957
|
+
/** `http_<kod>`, `timeout`, `network_error`, `endpoint_inactive`; başarıda null. */
|
|
958
|
+
error: z.string().nullable(),
|
|
959
|
+
attempts: z.number().int().nonnegative(),
|
|
960
|
+
at: DateTimeSchema
|
|
961
|
+
});
|
|
962
|
+
/** Yanıt gövdesi. Sır alanı yoktur (K9). */
|
|
963
|
+
const WebhookEndpointSchema = z.object({
|
|
964
|
+
id: IdSchema,
|
|
965
|
+
url: z.string(),
|
|
966
|
+
status: WebhookEndpointStatusSchema,
|
|
967
|
+
/** null: filtre yok, yalnızca 0.1'in 15 eski tipi (`LEGACY_EVENT_TYPES`). */
|
|
968
|
+
eventTypes: z.array(EventTypeSchema).nullable(),
|
|
969
|
+
description: z.string().nullable(),
|
|
970
|
+
createdAt: DateTimeSchema,
|
|
971
|
+
rotatedAt: DateTimeSchema.nullable(),
|
|
972
|
+
lastDelivery: WebhookLastDeliverySchema.nullable()
|
|
973
|
+
});
|
|
974
|
+
/** `GET /v1/admin/webhook-endpoints` (oluşturulma sırasıyla). */
|
|
975
|
+
const WebhookEndpointListSchema = z.object({ items: z.array(WebhookEndpointSchema) });
|
|
976
|
+
/** `whsec_` + 32 rastgele bayt (base64url). Web Crypto; Node ≥22, edge ve workerd. */
|
|
977
|
+
function generateWebhookSecret() {
|
|
978
|
+
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
|
|
979
|
+
const base64 = btoa(String.fromCharCode(...bytes));
|
|
980
|
+
return `${WEBHOOK_SECRET_PREFIX}${base64.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "")}`;
|
|
981
|
+
}
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region ../contract/src/doctor.ts
|
|
984
|
+
const DOCTOR_SEVERITIES = [
|
|
985
|
+
"error",
|
|
986
|
+
"warning",
|
|
987
|
+
"info"
|
|
988
|
+
];
|
|
989
|
+
const DoctorSeveritySchema = z.enum(DOCTOR_SEVERITIES);
|
|
990
|
+
const DoctorWarningSchema = z.object({
|
|
991
|
+
code: z.string().min(1).max(100),
|
|
992
|
+
severity: DoctorSeveritySchema,
|
|
993
|
+
/** İnsan için (tr); makine `code`'a bakar. */
|
|
994
|
+
message: z.string(),
|
|
995
|
+
/** Uyarı tek bir endpoint'e aitse. */
|
|
996
|
+
endpointId: IdSchema.optional()
|
|
997
|
+
});
|
|
998
|
+
const DoctorUnsellablePriceSchema = z.object({
|
|
999
|
+
priceId: IdSchema,
|
|
1000
|
+
planCode: z.string(),
|
|
1001
|
+
interval: IntervalSchema,
|
|
1002
|
+
intervalCount: z.number().int().positive(),
|
|
1003
|
+
currency: z.string(),
|
|
1004
|
+
reason: z.string()
|
|
1005
|
+
});
|
|
1006
|
+
const DoctorCatalogSchema = z.object({
|
|
1007
|
+
/** Katalog en az bir kez senkronlandı (`GET /v1/catalog` 200). */
|
|
1008
|
+
synced: z.boolean(),
|
|
1009
|
+
/** `GET /v1/catalog` ETag'inin tırnaksız değeri (`AccountState.catalogVersion`); senkron yoksa null. */
|
|
1010
|
+
version: z.string().nullable(),
|
|
1011
|
+
/** Aktif plan sayısı. */
|
|
1012
|
+
plans: z.number().int().nonnegative(),
|
|
1013
|
+
/** Satışa açık planların (`sellable`) bugün checkout'ta satılabilen aktif fiyatları. */
|
|
1014
|
+
sellablePrices: z.number().int().nonnegative(),
|
|
1015
|
+
/** Satışa açık planların satılamayan aktif fiyatları (satışa kapalı planların fiyatları sayılmaz). */
|
|
1016
|
+
unsellablePrices: z.array(DoctorUnsellablePriceSchema)
|
|
1017
|
+
});
|
|
1018
|
+
const DOCTOR_PROVIDER_ENVIRONMENTS = [
|
|
1019
|
+
"sandbox",
|
|
1020
|
+
"live",
|
|
1021
|
+
"fake"
|
|
1022
|
+
];
|
|
1023
|
+
const DoctorProviderSchema = z.object({
|
|
1024
|
+
/** Ürün ayarındaki sağlayıcı (`settings.provider`). */
|
|
1025
|
+
id: z.string(),
|
|
1026
|
+
/** Sağlayıcı bu deployment'ta yapılandırılmış mı (anahtarlar var). */
|
|
1027
|
+
configured: z.boolean(),
|
|
1028
|
+
/** `sandbox` | `live` (iyzico taban adresinden) | `fake`; yapılandırılmamışsa null (K20). */
|
|
1029
|
+
environment: z.enum(DOCTOR_PROVIDER_ENVIRONMENTS).nullable(),
|
|
1030
|
+
/** Sağlayıcı hesabı (iyzico merchant id; sır değildir); bilinmiyorsa null (K20). */
|
|
1031
|
+
merchantId: z.string().nullable(),
|
|
1032
|
+
/**
|
|
1033
|
+
* Yanıtı veren pod'un gözlemi: art arda erişilebilirlik hatası yoksa true (çağrı
|
|
1034
|
+
* olmaması da true). Pod başına, süreç içi; yapılandırılmamışsa null.
|
|
1035
|
+
*/
|
|
1036
|
+
healthy: z.boolean().nullable(),
|
|
1037
|
+
/** Bu pod'un gördüğü son sağlayıcı hatası (kod ve işlem; mesaj yok). */
|
|
1038
|
+
lastError: z.object({
|
|
1039
|
+
/** Sağlayıcı hata kodu, ör. `iyzico:timeout`, `iyzico:1001`. */
|
|
1040
|
+
code: z.string(),
|
|
1041
|
+
/** `ensure_price` | `create_checkout` | `retrieve_checkout` | `retrieve_subscription` | `cancel_subscription` | `retry_payment`. */
|
|
1042
|
+
operation: z.string(),
|
|
1043
|
+
at: DateTimeSchema
|
|
1044
|
+
}).nullable()
|
|
1045
|
+
});
|
|
1046
|
+
const DoctorSettingsSchema = z.object({
|
|
1047
|
+
appUrl: z.string().nullable(),
|
|
1048
|
+
/** Checkout dönüşüne izinli origin'ler (`appUrl`'in origin'i + `extraReturnOrigins`). */
|
|
1049
|
+
returnOrigins: z.array(z.string())
|
|
1050
|
+
});
|
|
1051
|
+
/** Hosted sayfanın (F2b checkout, F3a portal) zorunlu saydığı marka alanları; logo ve renk nötr varsayılana düşer. */
|
|
1052
|
+
const DOCTOR_REQUIRED_BRANDING = ["name", "supportEmail"];
|
|
1053
|
+
/** KVKK/mesafeli satış bağları. */
|
|
1054
|
+
const DOCTOR_REQUIRED_LEGAL_LINKS = ["privacyUrl", "termsUrl"];
|
|
1055
|
+
const DoctorHostedSchema = z.object({
|
|
1056
|
+
branding: z.object({ missing: z.array(z.string()) }),
|
|
1057
|
+
legalLinks: z.object({ missing: z.array(z.string()) }),
|
|
1058
|
+
/** Marka ve yasal bağ eksiği yok ve `appUrl` var (sayfanın "ürüne dön" hedefi). */
|
|
1059
|
+
ready: z.boolean()
|
|
1060
|
+
});
|
|
1061
|
+
const DoctorEndpointSchema = z.object({
|
|
1062
|
+
id: IdSchema,
|
|
1063
|
+
/** Ürünün kaydettiği adres. */
|
|
1064
|
+
url: z.string(),
|
|
1065
|
+
status: WebhookEndpointStatusSchema,
|
|
1066
|
+
/** null: filtre yok, yalnızca 0.1'in 15 eski tipi (`account.state_changed` almaz). */
|
|
1067
|
+
eventTypes: z.array(EventTypeSchema).nullable(),
|
|
1068
|
+
/** Bekleyen teslimat sayısı. */
|
|
1069
|
+
pending: z.number().int().nonnegative(),
|
|
1070
|
+
/** Vazgeçilmiş (dead) teslimat sayısı; `resync` ile kurtarılır. */
|
|
1071
|
+
dead: z.number().int().nonnegative(),
|
|
1072
|
+
/** Bekleyen en eski teslimatın kuyruğa girdiği an. */
|
|
1073
|
+
oldestPendingAt: DateTimeSchema.nullable(),
|
|
1074
|
+
/** `GET /v1/admin/webhook-endpoints` ile aynı: en son denenmiş teslimat. */
|
|
1075
|
+
lastDelivery: WebhookLastDeliverySchema.nullable(),
|
|
1076
|
+
diagnosis: z.string()
|
|
1077
|
+
});
|
|
1078
|
+
const DoctorReportSchema = z.object({
|
|
1079
|
+
/** `error` önemde uyarı yok. */
|
|
1080
|
+
ok: z.boolean(),
|
|
1081
|
+
checkedAt: DateTimeSchema,
|
|
1082
|
+
product: z.object({
|
|
1083
|
+
code: z.string(),
|
|
1084
|
+
name: z.string()
|
|
1085
|
+
}),
|
|
1086
|
+
catalog: DoctorCatalogSchema,
|
|
1087
|
+
provider: DoctorProviderSchema,
|
|
1088
|
+
settings: DoctorSettingsSchema,
|
|
1089
|
+
hosted: DoctorHostedSchema,
|
|
1090
|
+
/** Pasif (`inactive`) olmayan endpoint'ler, oluşturulma sırasıyla. */
|
|
1091
|
+
endpoints: z.array(DoctorEndpointSchema),
|
|
1092
|
+
dunning: z.object({
|
|
1093
|
+
/** true: dunning adımları yalnızca iz yazar (askı/sonlandırma yok). */
|
|
1094
|
+
dryRun: z.boolean() }),
|
|
1095
|
+
warnings: z.array(DoctorWarningSchema)
|
|
1096
|
+
});
|
|
1097
|
+
/** Bekleyen en eski teslimat bundan eskiyse `endpoint_pending_backlog` (K23 alarmıyla aynı eşik). */
|
|
1098
|
+
const DOCTOR_PENDING_BACKLOG_MS = 36e5;
|
|
1099
|
+
/** Son denenmiş teslimattan endpoint teşhisi (saf). */
|
|
1100
|
+
function endpointDiagnosisOf(last) {
|
|
1101
|
+
if (last === null) return "no_deliveries";
|
|
1102
|
+
if (last.status === "delivered") return "ok";
|
|
1103
|
+
if (last.httpStatus === 401) return "wrong_secret";
|
|
1104
|
+
if (last.httpStatus === 400) return "old_contract";
|
|
1105
|
+
if (last.error === "timeout" || last.error === "network_error") return "unreachable";
|
|
1106
|
+
if (last.status === "pending" && last.httpStatus === null && last.error === null) return "in_progress";
|
|
1107
|
+
return "failing";
|
|
1108
|
+
}
|
|
1109
|
+
/** Hosted sayfa hazırlığı: eksik zorunlu marka alanları ve yasal bağlar (saf). */
|
|
1110
|
+
function hostedReadinessOf(settings) {
|
|
1111
|
+
const branding = DOCTOR_REQUIRED_BRANDING.filter((field) => settings.branding[field] === void 0);
|
|
1112
|
+
const legalLinks = DOCTOR_REQUIRED_LEGAL_LINKS.filter((field) => settings[field] === void 0);
|
|
1113
|
+
return {
|
|
1114
|
+
branding: { missing: [...branding] },
|
|
1115
|
+
legalLinks: { missing: [...legalLinks] },
|
|
1116
|
+
ready: branding.length === 0 && legalLinks.length === 0 && settings.appUrl !== void 0
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
const receivesStateChanged = (endpoint) => endpoint.status === "active" && (endpoint.eventTypes ?? []).includes("account.state_changed");
|
|
1120
|
+
const DIAGNOSIS_WARNINGS = {
|
|
1121
|
+
wrong_secret: {
|
|
1122
|
+
code: "endpoint_wrong_secret",
|
|
1123
|
+
severity: "error",
|
|
1124
|
+
message: "son teslimat 401 aldı: ürün imzayı yanlış sırla doğruluyor (rotasyonda `yeni,eski` listesi)"
|
|
1125
|
+
},
|
|
1126
|
+
old_contract: {
|
|
1127
|
+
code: "endpoint_old_contract",
|
|
1128
|
+
severity: "error",
|
|
1129
|
+
message: "son teslimat 400 aldı: alıcı gövdeyi tanımıyor (eski SDK ya da filtredeki tipi bilmiyor)"
|
|
1130
|
+
},
|
|
1131
|
+
unreachable: {
|
|
1132
|
+
code: "endpoint_unreachable",
|
|
1133
|
+
severity: "error",
|
|
1134
|
+
message: "son teslimat adrese ulaşamadı (zaman aşımı ya da ağ hatası)"
|
|
1135
|
+
},
|
|
1136
|
+
failing: {
|
|
1137
|
+
code: "endpoint_failing",
|
|
1138
|
+
severity: "warning",
|
|
1139
|
+
message: "son teslimat başarısız yanıt aldı; steward yeniden deniyor"
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
/** Olgulardan raporu türetir (saf): endpoint teşhisleri, hosted hazırlığı, uyarılar ve `ok`. */
|
|
1143
|
+
function buildDoctorReport(facts) {
|
|
1144
|
+
const warnings = [];
|
|
1145
|
+
const warn = (severity, code, message, endpointId) => {
|
|
1146
|
+
warnings.push({
|
|
1147
|
+
code,
|
|
1148
|
+
severity,
|
|
1149
|
+
message,
|
|
1150
|
+
...endpointId !== void 0 ? { endpointId } : {}
|
|
1151
|
+
});
|
|
1152
|
+
};
|
|
1153
|
+
const { catalog, provider, settings, dunning } = facts;
|
|
1154
|
+
if (!catalog.synced) warn("error", "catalog_not_synced", "katalog senkronlanmamış: `PUT /v1/admin/catalog` (steward catalog push)");
|
|
1155
|
+
const byReason = (reason) => catalog.unsellablePrices.filter((p) => p.reason === reason);
|
|
1156
|
+
const unsynced = byReason("provider_price_missing");
|
|
1157
|
+
if (unsynced.length > 0) warn("error", "price_not_synced_with_provider", `${unsynced.length} fiyatın sağlayıcıda karşılığı yok (checkout plan_not_sellable döner): katalog senkronunu yeniden koşun`);
|
|
1158
|
+
const otherUnsellable = catalog.unsellablePrices.filter((p) => p.reason !== "provider_price_missing" && p.reason !== "provider_not_configured");
|
|
1159
|
+
if (otherUnsellable.length > 0) {
|
|
1160
|
+
const reasons = [...new Set(otherUnsellable.map((p) => p.reason))].join(", ");
|
|
1161
|
+
warn("warning", "price_unsellable", `${otherUnsellable.length} fiyat satılamaz (${reasons})`);
|
|
1162
|
+
}
|
|
1163
|
+
if (!provider.configured) warn("error", "provider_not_configured", `ödeme sağlayıcısı ${provider.id} bu deployment'ta yapılandırılmamış`);
|
|
1164
|
+
else if (provider.healthy === false) warn("warning", "provider_unhealthy", `ödeme sağlayıcısı ${provider.id} art arda erişilemedi (yanıtı veren pod'un gözlemi)`);
|
|
1165
|
+
const returnOrigins = returnOriginsOf(settings);
|
|
1166
|
+
if (returnOrigins.length === 0) warn("error", "no_return_origin", "checkout dönüşüne izinli origin yok: `appUrl` ayarlanmalı (her checkout return_url_not_allowed alır)");
|
|
1167
|
+
if (settings.appUrl === void 0) warn("warning", "app_url_missing", "`appUrl` ayarlı değil: `PATCH /v1/admin/settings`");
|
|
1168
|
+
const hosted = hostedReadinessOf(settings);
|
|
1169
|
+
if (hosted.branding.missing.length > 0) warn("warning", "branding_incomplete", `hosted sayfa markası eksik: branding.${hosted.branding.missing.join(", branding.")}`);
|
|
1170
|
+
if (hosted.legalLinks.missing.length > 0) warn("warning", "legal_links_missing", `hosted sayfa yasal bağları eksik: ${hosted.legalLinks.missing.join(", ")}`);
|
|
1171
|
+
const endpoints = facts.endpoints.map((endpoint) => ({
|
|
1172
|
+
...endpoint,
|
|
1173
|
+
diagnosis: endpointDiagnosisOf(endpoint.lastDelivery)
|
|
1174
|
+
}));
|
|
1175
|
+
const active = endpoints.filter((e) => e.status === "active");
|
|
1176
|
+
if (active.length === 0) warn("error", "no_active_endpoint", "aktif webhook endpoint'i yok: ürün durum değişikliklerini almaz");
|
|
1177
|
+
else if (!active.some(receivesStateChanged)) warn("warning", "no_state_changed_endpoint", "hiçbir aktif endpoint account.state_changed almıyor (filtresiz endpoint yalnızca 0.1 tiplerini alır)");
|
|
1178
|
+
for (const endpoint of endpoints) {
|
|
1179
|
+
const diagnosis = DIAGNOSIS_WARNINGS[endpoint.diagnosis];
|
|
1180
|
+
if (diagnosis) warn(diagnosis.severity, diagnosis.code, diagnosis.message, endpoint.id);
|
|
1181
|
+
if (endpoint.dead > 0) warn("warning", "endpoint_dead_deliveries", `${endpoint.dead} teslimattan vazgeçildi (dead): etkilenen hesaplar için resync`, endpoint.id);
|
|
1182
|
+
if (endpoint.oldestPendingAt !== null && facts.checkedAt.getTime() - Date.parse(endpoint.oldestPendingAt) > 36e5) warn("warning", "endpoint_pending_backlog", `${endpoint.pending} teslimat bekliyor; en eskisi bir saatten eski`, endpoint.id);
|
|
1183
|
+
}
|
|
1184
|
+
if (dunning.dryRun) warn("info", "dunning_dry_run", "dunning dry-run: adımlar yalnızca iz yazar, askı ve sonlandırma uygulanmaz");
|
|
1185
|
+
return {
|
|
1186
|
+
ok: !warnings.some((w) => w.severity === "error"),
|
|
1187
|
+
checkedAt: facts.checkedAt.toISOString(),
|
|
1188
|
+
product: facts.product,
|
|
1189
|
+
catalog,
|
|
1190
|
+
provider,
|
|
1191
|
+
settings: {
|
|
1192
|
+
appUrl: settings.appUrl ?? null,
|
|
1193
|
+
returnOrigins
|
|
1194
|
+
},
|
|
1195
|
+
hosted,
|
|
1196
|
+
endpoints,
|
|
1197
|
+
dunning,
|
|
1198
|
+
warnings
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
//#endregion
|
|
1202
|
+
export { HEADER_SDK as $, FeatureTypeSchema as $t, webhookEndpointUrlProblem as A, MAX_CHECKOUT_CONSENT_DOCS as At, AccountDetailSchema as B, ReturnOriginSchema as Bt, WebhookEndpointListSchema as C, CheckoutConsentInputSchema as Ct, WebhookLastDeliverySchema as D, CheckoutSessionSchema as Dt, WebhookEndpointStatusSchema as E, CheckoutSessionCreatedSchema as Et, PlanSetWarningSchema as F, BrandingSchema as Ft, DeliverySchema as G, applySettingsPatch as Gt, BillingProfileResponseSchema as H, SettingsLocaleSchema as Ht, ResyncResultSchema as I, DEFAULT_CHECKOUT_TTL_MINUTES as It, GrantCreateResponseSchema as J, AccountsRefreshResultSchema as Jt, EventListSchema as K, isReturnUrlAllowed as Kt, ERROR_CODES as L, DEFAULT_PORTAL_TTL_MINUTES as Lt, PlanSetInputSchema as M, checkoutRedirectUrl as Mt, PlanSetModeSchema as N, isLegacyCheckoutInput as Nt, WebhookSecretSchema as O, CheckoutStatusSchema as Ot, PlanSetResponseSchema as P, BrandingPatchSchema as Pt, HEADER_REQUEST_ID as Q, FeatureDefSchema as Qt, errorCodeSpec as R, ProductSettingsPatchSchema as Rt, WebhookEndpointCreateInputSchema as S, CHECKOUT_ERRORS as St, WebhookEndpointSchema as T, CheckoutSessionCreateInputSchema as Tt, CancelSubscriptionResponseSchema as U, TtlMinutesSchema as Ut, AccountUpsertResponseSchema as V, SETTINGS_LOCALES as Vt, CheckoutSessionResponseSchema as W, WebUrlSchema as Wt, HEADER_API_VERSION as X, CatalogSyncInputSchema as Xt, HEADER_ACTOR_REF as Y, CatalogSchema as Yt, HEADER_IDEMPOTENCY_KEY as Z, CatalogSyncResultSchema as Zt, hostedReadinessOf as _, StatsSchema as _t, DOCTOR_SEVERITIES as a, InvoiceSchema as at, WEBHOOK_ENDPOINT_STATUSES as b, PortalSessionCreateInputSchema as bt, DoctorHostedSchema as c, MANUAL_INVOICE_MAX_AMOUNT_MINOR as ct, DoctorSettingsSchema as d, manualInvoiceTotals as dt, PlanDefSchema as en, InvoiceIssueResponseSchema as et, DoctorSeveritySchema as f, splitInclusiveTax as ft, endpointDiagnosisOf as g, PaymentSchema as gt, buildDoctorReport as h, GrantSchema as ht, DOCTOR_REQUIRED_LEGAL_LINKS as i, InvoiceLineSchema as it, PLAN_SET_MODES as j, ProviderPayloadSchema as jt, generateWebhookSecret as k, ConsentSchema as kt, DoctorProviderSchema as l, ManualInvoiceInputSchema as lt, DoctorWarningSchema as m, GrantCreateInputSchema as mt, DOCTOR_PROVIDER_ENVIRONMENTS as n, PriceSchema as nn, ManualInvoiceResponseSchema as nt, DoctorCatalogSchema as o, InvoiceStatusSchema as ot, DoctorUnsellablePriceSchema as p, CancelSubscriptionInputSchema as pt, EventRecordSchema as q, returnOriginsOf as qt, DOCTOR_REQUIRED_BRANDING as r, InvoiceIssueInputSchema as rt, DoctorEndpointSchema as s, InvoiceVoidInputSchema as st, DOCTOR_PENDING_BACKLOG_MS as t, PriceDefSchema as tn, InvoiceVoidResponseSchema as tt, DoctorReportSchema as u, ManualInvoiceLineInputSchema as ut, DEFAULT_ENDPOINT_EVENT_TYPES as v, SubscriptionSchema as vt, WebhookEndpointRotateInputSchema as w, CheckoutPrefillSchema as wt, WEBHOOK_SECRET_PREFIX as x, PortalSessionSchema as xt, EventTypeSchema as y, PORTAL_SESSION_ERRORS as yt, isKnownErrorCode as z, ProductSettingsSchema as zt };
|