@stewardhq/sdk 0.3.0 → 0.6.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.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +332 -273
  3. package/dist/_chunks/errors.js +1 -1
  4. package/dist/_chunks/events.d.ts +386 -78
  5. package/dist/_chunks/events.js +1035 -19
  6. package/dist/_chunks/index.d.ts +3752 -305
  7. package/dist/_chunks/locale.d.ts +270 -2
  8. package/dist/_chunks/src.js +925 -31
  9. package/dist/_chunks/validators.d.ts +46 -2
  10. package/dist/_chunks/validators.js +128 -27
  11. package/dist/_chunks/webhook-core.d.ts +2 -2
  12. package/dist/contract.d.ts +4 -4
  13. package/dist/contract.js +4 -4
  14. package/dist/index.d.ts +447 -33
  15. package/dist/index.js +1659 -26
  16. package/dist/server.d.ts +66 -16
  17. package/dist/server.js +35 -4
  18. package/package.json +14 -13
  19. package/dist/_chunks/steward.d.ts +0 -189
  20. package/dist/_chunks/steward.js +0 -564
  21. package/dist/testing/fixtures/events/LOCK.json +0 -27
  22. package/dist/testing/fixtures/events/account.deleted.json +0 -36
  23. package/dist/testing/fixtures/events/account.state_changed.json +0 -53
  24. package/dist/testing/fixtures/events/account.updated.json +0 -37
  25. package/dist/testing/fixtures/events/checkout.completed.json +0 -36
  26. package/dist/testing/fixtures/events/checkout.expired.json +0 -26
  27. package/dist/testing/fixtures/events/checkout.failed.json +0 -27
  28. package/dist/testing/fixtures/events/invoice.created.json +0 -37
  29. package/dist/testing/fixtures/events/invoice.issued.json +0 -38
  30. package/dist/testing/fixtures/events/invoice.voided.json +0 -39
  31. package/dist/testing/fixtures/events/subscription.activated.json +0 -40
  32. package/dist/testing/fixtures/events/subscription.cancel_scheduled.json +0 -38
  33. package/dist/testing/fixtures/events/subscription.canceled.json +0 -29
  34. package/dist/testing/fixtures/events/subscription.expired.json +0 -27
  35. package/dist/testing/fixtures/events/subscription.payment_failed.json +0 -38
  36. package/dist/testing/fixtures/events/subscription.reactivated.json +0 -36
  37. package/dist/testing/fixtures/events/subscription.renewed.json +0 -39
  38. package/dist/testing/fixtures/events/subscription.suspended.json +0 -36
  39. package/dist/testing/fixtures/events/subscription.terminated.json +0 -28
  40. package/dist/testing.d.ts +0 -656
  41. package/dist/testing.js +0 -3684
@@ -1,4 +1,4 @@
1
- import { $ as CurrencySchema, C as BillingProfileKindSchema, E as SubscriptionStatusSchema, F as EmailSettingsPatchSchema, I as EmailSettingsSchema, Q as CodeSchema, V as applyEmailSettingsPatch, X as ActorRefSchema, Z as AmountMinorSchema, a as EVENT_TYPES, b as AccountStateSchema, ct as IntervalSchema, et as DateTimeSchema, j as EMAIL_TEMPLATES, o as EventIdSchema, ot as ExternalRefSchema, rt as EntitlementsSchema, st as IdSchema, w as BillingProfileSchema, y as AccountSchema } from "./events.js";
1
+ import { $t as MeteredPriceSchema, A as SubscriptionStatusSchema, Bn as EntitlementsSchema, C as AccountSchema, D as BillingProfileKindSchema, Fn as CodeSchema, H as EmailSettingsSchema, Ht as MeterCreditPropertiesSchema, In as CurrencySchema, Jn as InvoiceBillingReasonSchema, Kn as IdSchema, L as EMAIL_TEMPLATES, Ln as DateTimeSchema, Nn as ActorRefSchema, O as BillingProfileSchema, Pn as AmountMinorSchema, Qt as MeteredPriceDefSchema, Un as ExternalRefSchema, Ut as MeterDefSchema, V as EmailSettingsPatchSchema, Vt as MeterCreditEntrySchema, Yn as InvoicePaymentStatusSchema, Z as applyEmailSettingsPatch, a as EVENT_TYPES, bt as AccountMetersSchema, ct as BenefitDefSchema, ft as CatalogBenefitSchema, gt as featureValueMatchesType, ln as plainDecimalOf, o as EventIdSchema, qn as IntervalSchema, rn as MICRO_UNITS_PER_UNIT, tn as meterFilterClauseOf, un as toMicroUnits, w as AccountStateSchema, wt as DecimalUnitsStringSchema, xt as CatalogMeterSchema } from "./events.js";
2
2
  import { z } from "zod";
3
3
  //#region ../contract/src/catalog.ts
4
4
  const FeatureTypeSchema = z.enum([
@@ -11,14 +11,34 @@ const FeatureDefSchema = z.object({
11
11
  type: FeatureTypeSchema,
12
12
  description: z.string().max(500).optional()
13
13
  });
14
+ const PlanRankSchema = z.number().int().nonnegative();
15
+ /** Upper bound of benefits a plan lists. */
16
+ const MAX_PLAN_BENEFITS = 100;
17
+ /** Plan in the catalog sync input. */
14
18
  const PlanDefSchema = z.object({
15
19
  code: CodeSchema,
16
20
  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(),
21
+ /**
22
+ * Upgrade order of the pre-benefits entitlement rule (higher = upper plan). Optional since
23
+ * billing core (M35): default 0; the benefits resolution does not read it.
24
+ */
25
+ rank: PlanRankSchema.default(0),
19
26
  /** Self-servis satın alınabilir mi (false: yalnızca grant ile verilir). */
20
27
  sellable: z.boolean(),
21
- entitlements: EntitlementsSchema
28
+ entitlements: EntitlementsSchema,
29
+ /** Codes of catalog `benefits` the plan includes, in display order (billing core; default empty). */
30
+ benefits: z.array(CodeSchema).max(100).default([])
31
+ });
32
+ /** Plan as returned by `GET /v1/catalog`. */
33
+ const CatalogPlanSchema = z.object({
34
+ code: CodeSchema,
35
+ name: z.string().min(1).max(200),
36
+ /** Stored value (0 when the catalog omitted it); kept for older clients (M35). */
37
+ rank: PlanRankSchema.default(0),
38
+ sellable: z.boolean(),
39
+ entitlements: EntitlementsSchema,
40
+ /** Present only for a product that defines benefits. */
41
+ benefits: z.array(CodeSchema).optional()
22
42
  });
23
43
  const PriceDefSchema = z.object({
24
44
  planCode: CodeSchema,
@@ -31,11 +51,25 @@ const PriceDefSchema = z.object({
31
51
  /** Baz puan: %20 = 2000. */
32
52
  taxRateBps: z.number().int().min(0).max(1e4)
33
53
  });
54
+ /** Upper bound of benefit definitions in one catalog. */
55
+ const MAX_CATALOG_BENEFITS = 200;
56
+ /** Upper bound of meters in one catalog. */
57
+ const MAX_CATALOG_METERS = 100;
58
+ /** Upper bound of metered prices in one catalog. */
59
+ const MAX_CATALOG_METERED_PRICES = 500;
60
+ /** Issue `params.code` of a metered price whose tax terms differ from the plan's base price (M20). */
61
+ const METERED_PRICE_TAX_MISMATCH = "metered_price_tax_mismatch";
34
62
  const CatalogSyncInputSchema = z.object({
35
63
  defaultPlanCode: CodeSchema,
36
64
  features: z.array(FeatureDefSchema),
37
65
  plans: z.array(PlanDefSchema).min(1),
38
- prices: z.array(PriceDefSchema)
66
+ prices: z.array(PriceDefSchema),
67
+ /** Benefit definitions by code (billing core; default empty). */
68
+ benefits: z.record(CodeSchema, BenefitDefSchema).default({}),
69
+ /** Meter definitions by code (metering; default empty). */
70
+ meters: z.record(CodeSchema, MeterDefSchema).default({}),
71
+ /** Metered prices (metering; default empty). */
72
+ meteredPrices: z.array(MeteredPriceDefSchema).default([])
39
73
  }).superRefine((c, ctx) => {
40
74
  const features = new Set(c.features.map((f) => f.key));
41
75
  const plans = new Set(c.plans.map((p) => p.code));
@@ -62,6 +96,78 @@ const CatalogSyncInputSchema = z.object({
62
96
  ],
63
97
  message: "tanımsız feature"
64
98
  });
99
+ const benefitCodes = Object.keys(c.benefits);
100
+ if (benefitCodes.length > 200) ctx.addIssue({
101
+ code: "custom",
102
+ path: ["benefits"],
103
+ message: `at most 200 benefits`
104
+ });
105
+ for (const [i, p] of c.plans.entries()) {
106
+ const listed = /* @__PURE__ */ new Set();
107
+ for (const [j, code] of p.benefits.entries()) {
108
+ if (!Object.hasOwn(c.benefits, code)) ctx.addIssue({
109
+ code: "custom",
110
+ path: [
111
+ "plans",
112
+ i,
113
+ "benefits",
114
+ j
115
+ ],
116
+ message: `undefined benefit: ${code}`
117
+ });
118
+ if (listed.has(code)) ctx.addIssue({
119
+ code: "custom",
120
+ path: [
121
+ "plans",
122
+ i,
123
+ "benefits",
124
+ j
125
+ ],
126
+ message: `duplicate benefit: ${code}`
127
+ });
128
+ listed.add(code);
129
+ }
130
+ }
131
+ const featureTypes = new Map(c.features.map((f) => [f.key, f.type]));
132
+ for (const code of benefitCodes) {
133
+ const benefit = c.benefits[code];
134
+ if (benefit.type === "meter_credit") {
135
+ if (!Object.hasOwn(c.meters, benefit.properties.meter)) ctx.addIssue({
136
+ code: "custom",
137
+ path: [
138
+ "benefits",
139
+ code,
140
+ "properties",
141
+ "meter"
142
+ ],
143
+ message: `undefined meter: ${benefit.properties.meter}`
144
+ });
145
+ continue;
146
+ }
147
+ for (const [key, value] of Object.entries(benefit.properties)) {
148
+ const type = featureTypes.get(key);
149
+ if (type === void 0) ctx.addIssue({
150
+ code: "custom",
151
+ path: [
152
+ "benefits",
153
+ code,
154
+ "properties",
155
+ key
156
+ ],
157
+ message: "undefined feature"
158
+ });
159
+ else if (!featureValueMatchesType(type, value)) ctx.addIssue({
160
+ code: "custom",
161
+ path: [
162
+ "benefits",
163
+ code,
164
+ "properties",
165
+ key
166
+ ],
167
+ message: `value does not match feature type ${type}`
168
+ });
169
+ }
170
+ }
65
171
  const seen = /* @__PURE__ */ new Set();
66
172
  for (const [i, p] of c.prices.entries()) {
67
173
  if (!plans.has(p.planCode)) ctx.addIssue({
@@ -81,6 +187,61 @@ const CatalogSyncInputSchema = z.object({
81
187
  });
82
188
  seen.add(key);
83
189
  }
190
+ if (Object.keys(c.meters).length > 100) ctx.addIssue({
191
+ code: "custom",
192
+ path: ["meters"],
193
+ message: `at most 100 meters`
194
+ });
195
+ if (c.meteredPrices.length > 500) ctx.addIssue({
196
+ code: "custom",
197
+ path: ["meteredPrices"],
198
+ message: `at most 500 metered prices`
199
+ });
200
+ const meteredSeen = /* @__PURE__ */ new Set();
201
+ for (const [i, mp] of c.meteredPrices.entries()) {
202
+ if (!plans.has(mp.planCode)) ctx.addIssue({
203
+ code: "custom",
204
+ path: [
205
+ "meteredPrices",
206
+ i,
207
+ "planCode"
208
+ ],
209
+ message: `undefined plan: ${mp.planCode}`
210
+ });
211
+ if (!Object.hasOwn(c.meters, mp.meter)) ctx.addIssue({
212
+ code: "custom",
213
+ path: [
214
+ "meteredPrices",
215
+ i,
216
+ "meter"
217
+ ],
218
+ message: `undefined meter: ${mp.meter}`
219
+ });
220
+ const key = `${mp.planCode}/${mp.meter}/${mp.currency}`;
221
+ if (meteredSeen.has(key)) ctx.addIssue({
222
+ code: "custom",
223
+ path: ["meteredPrices", i],
224
+ message: `duplicate metered price key: ${key}`
225
+ });
226
+ meteredSeen.add(key);
227
+ if (!plans.has(mp.planCode)) continue;
228
+ const base = c.prices.filter((p) => p.planCode === mp.planCode && p.currency === mp.currency);
229
+ if (base.length === 0) ctx.addIssue({
230
+ code: "custom",
231
+ path: [
232
+ "meteredPrices",
233
+ i,
234
+ "currency"
235
+ ],
236
+ message: `plan ${mp.planCode} has no base price in ${mp.currency}`
237
+ });
238
+ else if (base.some((p) => p.taxInclusive !== mp.taxInclusive || p.taxRateBps !== mp.taxRateBps)) ctx.addIssue({
239
+ code: "custom",
240
+ path: ["meteredPrices", i],
241
+ message: `${METERED_PRICE_TAX_MISMATCH}: taxInclusive and taxRateBps must equal the base prices of plan ${mp.planCode} in ${mp.currency}`,
242
+ params: { code: METERED_PRICE_TAX_MISMATCH }
243
+ });
244
+ }
84
245
  });
85
246
  const PriceSchema = PriceDefSchema.extend({
86
247
  id: IdSchema,
@@ -91,9 +252,15 @@ const CatalogSchema = z.object({
91
252
  product: CodeSchema,
92
253
  defaultPlanCode: CodeSchema,
93
254
  features: z.array(FeatureDefSchema),
94
- plans: z.array(PlanDefSchema),
255
+ plans: z.array(CatalogPlanSchema),
95
256
  /** Varsayılan olarak yalnızca aktif fiyatlar. */
96
- prices: z.array(PriceSchema)
257
+ prices: z.array(PriceSchema),
258
+ /** Active (not archived) benefit definitions by code; present only for a product that defines benefits. */
259
+ benefits: z.record(CodeSchema, CatalogBenefitSchema).optional(),
260
+ /** Active (not archived) meters by code; present only for a product that defines meters (metering). */
261
+ meters: z.record(CodeSchema, CatalogMeterSchema).optional(),
262
+ /** Active metered prices; present only for a product that defines meters (metering). */
263
+ meteredPrices: z.array(MeteredPriceSchema).optional()
97
264
  });
98
265
  const CatalogSyncResultSchema = z.object({
99
266
  /** Aynı girdiyle tekrar senkron: hiçbir şey değişmedi. */
@@ -111,7 +278,21 @@ const CatalogSyncResultSchema = z.object({
111
278
  priceId: IdSchema,
112
279
  provider: z.string(),
113
280
  code: z.string()
114
- }))
281
+ })),
282
+ /** Benefits created, changed or unarchived by this sync. */
283
+ changedBenefits: z.array(CodeSchema).optional(),
284
+ /** Benefits removed from the catalog and archived by this sync. */
285
+ archivedBenefits: z.array(CodeSchema).optional(),
286
+ /** Prices whose amount or tax terms were updated in place (the price id is kept; history in `price_changes`). */
287
+ updatedPrices: z.array(IdSchema).optional(),
288
+ /** Meters created, changed (labels, `lowAt`, display) or unarchived by this sync. */
289
+ changedMeters: z.array(CodeSchema).optional(),
290
+ /** Meters removed from the catalog and archived by this sync (their history stays). */
291
+ archivedMeters: z.array(CodeSchema).optional(),
292
+ createdMeteredPrices: z.array(IdSchema).optional(),
293
+ /** Metered prices whose amount, cap or tax terms were updated in place (the id is kept). */
294
+ updatedMeteredPrices: z.array(IdSchema).optional(),
295
+ deactivatedMeteredPrices: z.array(IdSchema).optional()
115
296
  });
116
297
  /** `POST /v1/admin/accounts/refresh` — ürünün tüm hesaplarının snapshot'ı yeniden hesaplanır. */
117
298
  const AccountsRefreshResultSchema = z.object({
@@ -163,6 +344,60 @@ const BrandingSchema = z.object({
163
344
  accentColor: AccentColorSchema.optional(),
164
345
  supportEmail: SupportEmailSchema.optional()
165
346
  });
347
+ const DEFAULT_PAYMENT_DUE_DAYS = 7;
348
+ const DEFAULT_MIN_INVOICE_MINOR = 1e3;
349
+ const DEFAULT_BANK_TRANSFER_PAYMENT_DAYS = 5;
350
+ const PaymentDueDaysSchema = z.number().int().min(1).max(60);
351
+ const BankTransferPaymentDaysSchema = z.number().int().min(1).max(14);
352
+ const BankTransferSettingsSchema = z.object({
353
+ enabled: z.boolean().default(false),
354
+ paymentDays: BankTransferPaymentDaysSchema.default(5)
355
+ });
356
+ const BillingSettingsSchema = z.object({
357
+ paymentDueDays: PaymentDueDaysSchema.default(7),
358
+ minInvoiceMinor: AmountMinorSchema.default(DEFAULT_MIN_INVOICE_MINOR),
359
+ allowMultipleSubscriptions: z.boolean().default(false),
360
+ bankTransfer: BankTransferSettingsSchema.default({
361
+ enabled: false,
362
+ paymentDays: 5
363
+ })
364
+ });
365
+ /** Patch of `settings.billing.bankTransfer`: an omitted field is unchanged, `null` restores its default. */
366
+ const BankTransferSettingsPatchSchema = z.object({
367
+ enabled: z.boolean().nullable().optional(),
368
+ paymentDays: BankTransferPaymentDaysSchema.nullable().optional()
369
+ }).strict();
370
+ /** Patch of `settings.billing`: an omitted field is unchanged, `null` restores its default. */
371
+ const BillingSettingsPatchSchema = z.object({
372
+ paymentDueDays: PaymentDueDaysSchema.nullable().optional(),
373
+ minInvoiceMinor: AmountMinorSchema.nullable().optional(),
374
+ allowMultipleSubscriptions: z.boolean().nullable().optional(),
375
+ /** Merges per field; `bankTransfer: null` restores its defaults (off). */
376
+ bankTransfer: BankTransferSettingsPatchSchema.nullable().optional()
377
+ }).strict();
378
+ const DEFAULT_EVENT_RETENTION_DAYS = 120;
379
+ const DEFAULT_LATE_GRACE_MINUTES = 60;
380
+ const METER_CHARGE_MODES = [
381
+ "off",
382
+ "dry_run",
383
+ "live"
384
+ ];
385
+ const MeterChargeModeSchema = z.enum(METER_CHARGE_MODES);
386
+ const DEFAULT_METER_CHARGE_MODE = "dry_run";
387
+ const EventRetentionDaysSchema = z.number().int().min(45).max(400);
388
+ const LateGraceMinutesSchema = z.number().int().min(0).max(1440);
389
+ const EventsSettingsSchema = z.object({ retentionDays: EventRetentionDaysSchema.default(120) });
390
+ const MeteringSettingsSchema = z.object({
391
+ lateGraceMinutes: LateGraceMinutesSchema.default(60),
392
+ chargeMode: MeterChargeModeSchema.default(DEFAULT_METER_CHARGE_MODE)
393
+ });
394
+ /** Patch of `settings.events`: an omitted field is unchanged, `null` restores its default. */
395
+ const EventsSettingsPatchSchema = z.object({ retentionDays: EventRetentionDaysSchema.nullable().optional() }).strict();
396
+ /** Patch of `settings.metering`: an omitted field is unchanged, `null` restores its default. */
397
+ const MeteringSettingsPatchSchema = z.object({
398
+ lateGraceMinutes: LateGraceMinutesSchema.nullable().optional(),
399
+ chargeMode: MeterChargeModeSchema.nullable().optional()
400
+ }).strict();
166
401
  const ProductSettingsSchema = z.object({
167
402
  /** Checkout'ları alan ödeme sağlayıcısı. Operatör ayarıdır (pod CLI); yönetim ucundan yazılmaz. */
168
403
  provider: z.string().min(1).default("iyzico"),
@@ -177,8 +412,26 @@ const ProductSettingsSchema = z.object({
177
412
  checkoutTtlMinutes: TtlMinutesSchema.default(30),
178
413
  portalTtlMinutes: TtlMinutesSchema.default(30),
179
414
  /** Billing emails sent by steward (`emails.ts`). */
180
- emails: EmailSettingsSchema.default({ templates: {} })
415
+ emails: EmailSettingsSchema.default({ templates: {} }),
416
+ /** Billing behavior (billing core); absent = defaults (`billingSettingsOf`). */
417
+ billing: BillingSettingsSchema.optional(),
418
+ /** Raw event retention (metering); absent = defaults (`eventsSettingsOf`). */
419
+ events: EventsSettingsSchema.optional(),
420
+ /** Meter periods and overage pricing (metering); absent = defaults (`meteringSettingsOf`). */
421
+ metering: MeteringSettingsSchema.optional()
181
422
  });
423
+ /** `settings.billing` with defaults filled in. */
424
+ function billingSettingsOf(settings) {
425
+ return settings.billing ?? BillingSettingsSchema.parse({});
426
+ }
427
+ /** `settings.events` with defaults filled in. */
428
+ function eventsSettingsOf(settings) {
429
+ return settings.events ?? EventsSettingsSchema.parse({});
430
+ }
431
+ /** `settings.metering` with defaults filled in. */
432
+ function meteringSettingsOf(settings) {
433
+ return settings.metering ?? MeteringSettingsSchema.parse({});
434
+ }
182
435
  const BrandingPatchSchema = z.object({
183
436
  name: BrandingNameSchema.nullable().optional(),
184
437
  logoUrl: LogoUrlSchema.nullable().optional(),
@@ -196,8 +449,31 @@ const ProductSettingsPatchSchema = z.object({
196
449
  branding: BrandingPatchSchema.nullable().optional(),
197
450
  checkoutTtlMinutes: TtlMinutesSchema.optional(),
198
451
  portalTtlMinutes: TtlMinutesSchema.optional(),
199
- emails: EmailSettingsPatchSchema.nullable().optional()
452
+ emails: EmailSettingsPatchSchema.nullable().optional(),
453
+ /** Merges per field; `billing: null` restores every default. */
454
+ billing: BillingSettingsPatchSchema.nullable().optional(),
455
+ /** Merges per field; `events: null` restores every default. */
456
+ events: EventsSettingsPatchSchema.nullable().optional(),
457
+ /** Merges per field; `metering: null` restores every default. */
458
+ metering: MeteringSettingsPatchSchema.nullable().optional()
200
459
  }).strict();
460
+ /** Settings sections patched field by field (`null` section: every default back). */
461
+ const MERGED_SECTIONS = /* @__PURE__ */ new Set([
462
+ "billing",
463
+ "events",
464
+ "metering"
465
+ ]);
466
+ /** Objects inside a merged section that merge one level deeper (`billing.bankTransfer`). */
467
+ const NESTED_MERGED_FIELDS = /* @__PURE__ */ new Set(["bankTransfer"]);
468
+ /** Field-by-field merge: `null` deletes a field (its default comes back), `undefined` keeps it. */
469
+ function mergeFields(current, patch, nested = /* @__PURE__ */ new Set()) {
470
+ const merged = { ...current };
471
+ for (const [field, value] of Object.entries(patch)) if (value === null) delete merged[field];
472
+ else if (value === void 0) continue;
473
+ else if (nested.has(field)) merged[field] = mergeFields(current[field] ?? {}, value);
474
+ else merged[field] = value;
475
+ return merged;
476
+ }
201
477
  /**
202
478
  * Doğrulanmış yamayı ayarlara uygular (saf). Sonuç şemadan geçer; alan kuralları
203
479
  * yamada zaten denetlendiği için yalnızca programlama hatasında fırlatır.
@@ -214,7 +490,10 @@ function applySettingsPatch(current, patch) {
214
490
  }
215
491
  next.branding = branding;
216
492
  } else if (key === "emails") next.emails = applyEmailSettingsPatch(current.emails, value);
217
- else if (value === null) delete next[key];
493
+ else if (MERGED_SECTIONS.has(key)) {
494
+ if (value === null) delete next[key];
495
+ else next[key] = mergeFields(current[key] ?? {}, value, NESTED_MERGED_FIELDS);
496
+ } else if (value === null) delete next[key];
218
497
  else next[key] = value;
219
498
  }
220
499
  return ProductSettingsSchema.parse(next);
@@ -232,6 +511,16 @@ function isReturnUrlAllowed(settings, returnUrl) {
232
511
  //#endregion
233
512
  //#region ../contract/src/checkout.ts
234
513
  const ReturnUrlSchema = z.url({ protocol: /^https?$/ });
514
+ /**
515
+ * Identity number sent to the payment provider (and, when the invoice needs one, used on the
516
+ * invoice) for an INDIVIDUAL buyer (M31): steward never asks for or stores a TCKN; the Turkish
517
+ * "final consumer" placeholder goes instead. A company buyer's VKN comes from the billing profile.
518
+ *
519
+ * It deliberately FAILS the TCKN checksum (`isValidTckn("11111111111") === false`): never run it
520
+ * through a TCKN validator and never show it as a customer's TCKN. Kept here, not in a provider
521
+ * adapter, because another provider may need a different rule.
522
+ */
523
+ const INDIVIDUAL_IDENTITY_PLACEHOLDER = "11111111111";
235
524
  /** Accepted document — content lives in the product, steward keeps only the trace. */
236
525
  const ConsentSchema = z.object({
237
526
  document: z.string().min(1).max(100),
@@ -274,7 +563,15 @@ const CheckoutSessionCreateInputSchema = z.object({
274
563
  /** Opens the account with this name if it does not exist; never renames an existing account. */
275
564
  displayName: z.string().min(1).max(200).optional(),
276
565
  prefill: CheckoutPrefillSchema.optional(),
277
- consents: z.array(CheckoutConsentInputSchema).max(20).optional()
566
+ consents: z.array(CheckoutConsentInputSchema).max(20).optional(),
567
+ /**
568
+ * Upgrade (bank transfer plan H16): a live subscription of the account this checkout replaces.
569
+ * It does not block the session (`already_subscribed`) and keeps running until the payment is
570
+ * recorded; in the transaction that records it (card or bank transfer) it is canceled
571
+ * immediately and the new subscription starts. An abandoned, expired or canceled session leaves
572
+ * it untouched. Not a live subscription of the account → 422 `replaces_subscription_invalid`.
573
+ */
574
+ replacesSubscriptionId: IdSchema.optional()
278
575
  }).superRefine((body, ctx) => {
279
576
  const seen = /* @__PURE__ */ new Set();
280
577
  body.consents?.forEach((consent, i) => {
@@ -301,7 +598,18 @@ const CheckoutStatusSchema = z.enum([
301
598
  "expired",
302
599
  "canceled"
303
600
  ]);
304
- /** Provider form shown on steward's hosted checkout page (never returned to the product). */
601
+ /**
602
+ * How the customer chose to pay on the hosted page (bank transfer plan H5): `card` (the provider's
603
+ * form) or `bank_transfer` (the session stays `open` until an operator records the transfer).
604
+ * Absent/null: not chosen yet. The session status set does not change for it.
605
+ */
606
+ const PAYMENT_METHODS = ["card", "bank_transfer"];
607
+ const PaymentMethodSchema = z.enum(PAYMENT_METHODS);
608
+ /**
609
+ * Provider form shown on steward's hosted checkout page (never returned to the product).
610
+ * @deprecated Ürün yüzeyinde kullanılmıyor; yalnızca servis içi. Bir sonraki minor sürümde paketin
611
+ * dış yüzeyinden çıkarılacak (temizlik planı C9).
612
+ */
305
613
  const ProviderPayloadSchema = z.discriminatedUnion("type", [z.object({
306
614
  type: z.literal("html_snippet"),
307
615
  html: z.string()
@@ -329,7 +637,11 @@ const CheckoutSessionSchema = z.object({
329
637
  subscriptionId: IdSchema.nullable(),
330
638
  expiresAt: DateTimeSchema,
331
639
  createdAt: DateTimeSchema,
332
- completedAt: DateTimeSchema.nullable()
640
+ completedAt: DateTimeSchema.nullable(),
641
+ /** Payment method chosen on the page (H6); absent (or null): not chosen yet, or an older steward. */
642
+ paymentMethod: PaymentMethodSchema.nullable().optional(),
643
+ /** The subscription this checkout replaces (H16); absent (or null): none, or an older steward. */
644
+ replacesSubscriptionId: IdSchema.nullable().optional()
333
645
  });
334
646
  /**
335
647
  * `POST /v1/accounts/{ref}/checkout-sessions` response: `url` is steward's checkout page
@@ -349,7 +661,7 @@ function originOf(value) {
349
661
  }
350
662
  }
351
663
  /**
352
- * Browser redirect for a checkout outcome — the service and the fake steward share it.
664
+ * Browser redirect for a checkout outcome — used by the service.
353
665
  * Placeholders in `successUrl` (`{CHECKOUT_ID}` `{STATUS}` `{ACCOUNT_REF}` `{LOCALE}`) are
354
666
  * filled with URL-encoded values; nothing is appended, so a URL without placeholders is used
355
667
  * as-is. An encoded value cannot change the origin; it is still checked, and if the origin
@@ -373,9 +685,54 @@ const CHECKOUT_ERRORS = [
373
685
  "return_url_not_allowed",
374
686
  "provider_not_configured",
375
687
  "provider_error",
376
- "account_deleted"
688
+ "account_deleted",
689
+ "meter_already_billed",
690
+ "replaces_subscription_invalid"
377
691
  ];
378
692
  //#endregion
693
+ //#region ../contract/src/bank-transfers.ts
694
+ const BankTransferRecordInputSchema = z.object({
695
+ /** The account's order code as written in the transfer description (`K7M2-QX9D-4`; case and separators are forgiven). */
696
+ code: z.string().min(1).max(40),
697
+ /** The amount received, minor units (kuruş, cent); must equal what the item waits for. */
698
+ amountMinor: z.number().int().positive(),
699
+ currency: CurrencySchema,
700
+ /** When the money reached the account. */
701
+ receivedAt: DateTimeSchema,
702
+ /** The bank's transaction / receipt number; one payment per reference. */
703
+ bankRef: z.string().trim().min(1).max(100),
704
+ /** Settle this checkout (required when several items match, and with `acceptExpired`). */
705
+ checkoutSessionId: IdSchema.optional(),
706
+ /** Pay this invoice (a checkout matches too, or a written-off invoice the transfer pays). */
707
+ invoiceId: IdSchema.optional(),
708
+ /** H10: settle the named checkout although it expired (a late transfer the operator accepts). */
709
+ acceptExpired: z.boolean().optional()
710
+ }).strict().refine((input) => !input.acceptExpired || input.checkoutSessionId !== void 0, {
711
+ path: ["checkoutSessionId"],
712
+ message: "acceptExpired needs checkoutSessionId"
713
+ }).refine((input) => input.checkoutSessionId === void 0 || input.invoiceId === void 0, {
714
+ path: ["invoiceId"],
715
+ message: "name either checkoutSessionId or invoiceId"
716
+ });
717
+ const BankTransferRecordedSchema = z.object({
718
+ accountRef: ExternalRefSchema,
719
+ code: z.string(),
720
+ /** The checkout the transfer completed; null when it paid an invoice. */
721
+ checkoutSessionId: IdSchema.nullable(),
722
+ /** The invoice the transfer paid; null when it completed a checkout. */
723
+ invoiceId: IdSchema.nullable(),
724
+ subscriptionId: IdSchema,
725
+ paymentId: IdSchema,
726
+ /** The subscription the upgrade replaced (canceled in the same transaction, H16); null: none. */
727
+ replacedSubscriptionId: IdSchema.nullable(),
728
+ /** The checkout had expired and the operator accepted the transfer (H10). */
729
+ acceptedAfterExpiry: z.boolean(),
730
+ /** The paid invoice had been written off (waived / uncollectible). */
731
+ paidAfterWriteoff: z.boolean(),
732
+ /** The account's state after the recording. */
733
+ account: AccountStateSchema.optional()
734
+ });
735
+ //#endregion
379
736
  //#region ../contract/src/portal.ts
380
737
  const PortalSessionCreateInputSchema = z.object({
381
738
  /** Sayfa dili; verilmezse ürün ayarı `defaultLocale`. */
@@ -403,9 +760,11 @@ const SubscriptionSchema = z.object({
403
760
  planCode: CodeSchema,
404
761
  priceId: IdSchema,
405
762
  interval: IntervalSchema,
763
+ /** Amount per period; billing core (M36): the subscription's locked amount. */
406
764
  amountMinor: AmountMinorSchema,
407
765
  currency: CurrencySchema,
408
766
  provider: z.string(),
767
+ /** Legacy provider-managed subscription reference; `""` for a steward-managed subscription (billing core, M30). */
409
768
  providerSubscriptionRef: z.string(),
410
769
  cancelAtPeriodEnd: z.boolean(),
411
770
  cancelAt: DateTimeSchema.nullable(),
@@ -424,6 +783,28 @@ const CancelSubscriptionInputSchema = z.object({
424
783
  /** Sağlayıcıda zaten iptal edildiyse (panelden) sağlayıcı çağrısını atla. */
425
784
  skipProvider: z.boolean().default(false)
426
785
  });
786
+ /**
787
+ * `POST /v1/subscriptions/{id}/change` (billing core MF9): schedule a change for the next period
788
+ * boundary (`AccountState.subscription.scheduledChange`); the next renewal invoice is written from
789
+ * the new terms. `planCode` / `interval` pick another price of the subscription's currency;
790
+ * `price: "current"` re-locks the current amount of the (possibly updated) price and the plan's
791
+ * current metered rates — a metered price added later is attached only this way (M20, M36). At
792
+ * least one of the three. `when` is required so other timings can be added later. A second change
793
+ * replaces the first; asking for the current plan and interval while a change is pending takes it
794
+ * back ("keep my plan"; a move to the current price the operator announced stays).
795
+ * Errors: 404 `subscription_not_found`; 409 `already_subscribed` (another live subscription of the
796
+ * account holds the target plan or is moving to it), `meter_already_billed` (the target prices a
797
+ * meter another live subscription bills; for `price: "current"` only when that is the only
798
+ * difference); 422 `plan_not_found`, `plan_not_sellable`, `subscription_change_invalid` (no such
799
+ * price, nothing changes, the subscription is not live, has a cancellation scheduled or is a legacy
800
+ * provider subscription).
801
+ */
802
+ const SubscriptionChangeInputSchema = z.object({
803
+ planCode: CodeSchema.optional(),
804
+ interval: IntervalSchema.optional(),
805
+ price: z.literal("current").optional(),
806
+ when: z.literal("next_period")
807
+ }).refine((c) => c.planCode !== void 0 || c.interval !== void 0 || c.price !== void 0, { message: "planCode, interval or price is required" });
427
808
  const PaymentSchema = z.object({
428
809
  id: IdSchema,
429
810
  subscriptionId: IdSchema,
@@ -435,19 +816,28 @@ const PaymentSchema = z.object({
435
816
  periodEnd: DateTimeSchema.nullable(),
436
817
  occurredAt: DateTimeSchema
437
818
  });
819
+ /** Upper bound of named benefits in one grant. */
820
+ const MAX_GRANT_BENEFITS = 100;
438
821
  const GrantCreateInputSchema = z.object({
439
822
  planCode: CodeSchema.optional(),
440
- /** Plan değerlerinin üzerine yazılan tek tek haklar. */
823
+ /** Single values written over the merged entitlements (applied last; may lower). */
441
824
  overrides: EntitlementsSchema.default({}),
825
+ /**
826
+ * Catalog benefit codes granted independently of a plan (billing core, M32), e.g. only "sso".
827
+ * An unknown or archived code is rejected with 422 `benefit_not_found`.
828
+ */
829
+ benefitCodes: z.array(CodeSchema).max(100).optional(),
442
830
  reason: z.string().min(3).max(500),
443
831
  startsAt: DateTimeSchema.optional(),
444
832
  endsAt: DateTimeSchema.optional()
445
- }).refine((g) => g.planCode !== void 0 || Object.keys(g.overrides).length > 0, { message: "planCode ya da en az bir override gerekli" });
833
+ }).refine((g) => g.planCode !== void 0 || Object.keys(g.overrides).length > 0 || (g.benefitCodes?.length ?? 0) > 0, { message: "planCode, benefitCodes or at least one override is required" });
446
834
  const GrantSchema = z.object({
447
835
  id: IdSchema,
448
836
  accountRef: ExternalRefSchema,
449
837
  planCode: CodeSchema.nullable(),
450
838
  overrides: EntitlementsSchema,
839
+ /** Named benefits the grant gives (billing core; absent when none or from an older steward). */
840
+ benefitCodes: z.array(CodeSchema).optional(),
451
841
  reason: z.string(),
452
842
  actorRef: ActorRefSchema.nullable(),
453
843
  startsAt: DateTimeSchema,
@@ -472,7 +862,11 @@ const InvoiceLineSchema = z.object({
472
862
  description: z.string(),
473
863
  quantity: z.number().int().positive(),
474
864
  unitAmountMinor: AmountMinorSchema,
475
- amountMinor: AmountMinorSchema
865
+ amountMinor: AmountMinorSchema,
866
+ meter: CodeSchema.optional(),
867
+ units: DecimalUnitsStringSchema.optional(),
868
+ priceAmountMinor: AmountMinorSchema.optional(),
869
+ pricePerUnits: z.number().int().positive().optional()
476
870
  });
477
871
  const InvoiceSchema = z.object({
478
872
  id: IdSchema,
@@ -499,7 +893,12 @@ const InvoiceSchema = z.object({
499
893
  /** Manuel faturanın notu (ör. sözleşme ya da teklif referansı). */
500
894
  note: z.string().nullable().optional(),
501
895
  voidedAt: DateTimeSchema.nullable().optional(),
502
- voidReason: z.string().nullable().optional()
896
+ voidReason: z.string().nullable().optional(),
897
+ billingReason: InvoiceBillingReasonSchema.nullable().optional(),
898
+ paymentStatus: InvoicePaymentStatusSchema.nullable().optional(),
899
+ /** Billing period the invoice covers. */
900
+ periodStart: DateTimeSchema.nullable().optional(),
901
+ periodEnd: DateTimeSchema.nullable().optional()
503
902
  });
504
903
  const InvoiceIssueInputSchema = z.object({
505
904
  number: z.string().min(1).max(100),
@@ -538,6 +937,28 @@ const ManualInvoiceInputSchema = z.object({
538
937
  /** `POST /v1/invoices/{id}/void` — yalnızca manuel fatura; numara serbest kalmaz, belge silinmez. */
539
938
  const InvoiceVoidInputSchema = z.object({ reason: z.string().trim().min(3).max(500) });
540
939
  /**
940
+ * `POST /v1/invoices/{id}/payment-sessions` — opens steward's hosted pay page for an unpaid
941
+ * invoice (the product's own "Pay invoice" button; emails and the portal link to the same page).
942
+ * 409 `invoice_not_payable` when the invoice is not unpaid.
943
+ */
944
+ const InvoicePaymentSessionCreateInputSchema = z.object({
945
+ /** `tr` | `en`; defaults to the account's email locale, else the product's `defaultLocale`. */
946
+ locale: z.enum(SETTINGS_LOCALES).optional(),
947
+ /** Where the page returns after the outcome; its origin must be allowed by product settings. Defaults to `appUrl`. */
948
+ returnUrl: z.url({ protocol: /^https?$/ }).max(2e3).optional()
949
+ });
950
+ /** Pay page session: redirect the customer to `url` (it carries the page key; do not log it). */
951
+ const InvoicePaymentSessionSchema = z.object({
952
+ id: IdSchema,
953
+ url: z.string(),
954
+ expiresAt: DateTimeSchema
955
+ });
956
+ /**
957
+ * `POST /v1/invoices/{id}/waive` — an operator forgives an unpaid invoice (`paymentStatus:
958
+ * "waived"`, e.g. before deleting the account). 409 `invoice_not_waivable` otherwise.
959
+ */
960
+ const InvoiceWaiveInputSchema = z.object({ reason: z.string().trim().min(3).max(500) });
961
+ /**
541
962
  * Vergi dahil tutardan matrah ve vergiyi ayrıştırır (tam sayı, kuruşa
542
963
  * yuvarlama). Toplam her zaman korunur: subtotal + tax === total.
543
964
  */
@@ -642,6 +1063,45 @@ const ManualInvoiceResponseSchema = InvoiceSchema.extend(withAccount);
642
1063
  const InvoiceVoidResponseSchema = InvoiceSchema.extend(withAccount);
643
1064
  /** `GET /v1/checkout-sessions/{id}` — `account` yalnızca oturum `completed` ise. */
644
1065
  const CheckoutSessionResponseSchema = CheckoutSessionSchema.extend(withAccount);
1066
+ /** `POST /v1/invoices/{id}/waive` (billing core): the waived invoice and the state after it (`openInvoices` shrinks). */
1067
+ const InvoiceWaiveResponseSchema = InvoiceSchema.extend(withAccount);
1068
+ /** `POST /v1/subscriptions/{id}/change` (billing core MF9): the subscription and the state with `scheduledChange`. */
1069
+ const ChangeSubscriptionResponseSchema = SubscriptionSchema.extend(withAccount);
1070
+ /** `POST /v1/subscriptions/{id}/uncancel` (billing core MF9): the scheduled cancellation is taken back. */
1071
+ const UncancelSubscriptionResponseSchema = SubscriptionSchema.extend(withAccount);
1072
+ /**
1073
+ * `POST /v1/accounts/{ref}/credits` (metering MF2): the ledger entry written, the meters after it
1074
+ * and (K5) the account state after it (a credit may change a meter's standing).
1075
+ */
1076
+ const CreditGrantResponseSchema = z.object({
1077
+ entry: MeterCreditEntrySchema,
1078
+ meters: AccountMetersSchema,
1079
+ ...withAccount
1080
+ });
1081
+ /**
1082
+ * Known `GET /v1/me` capabilities (M27): what this steward understands beyond the 0.2 contract.
1083
+ * `steward catalog push` sends a catalog with `benefits` only when `benefits` is listed (an older
1084
+ * steward would silently drop the key); phase 2 adds `events` (`POST /v1/events/ingest`), `meters`
1085
+ * (catalog `meters`, `meter_credit` benefits, `GET …/meters`, credits) and `overage` (metered
1086
+ * prices, meter charges). A steward lists a capability only once it implements it. Open set.
1087
+ */
1088
+ const STEWARD_CAPABILITIES = [
1089
+ "benefits",
1090
+ "events",
1091
+ "meters",
1092
+ "overage"
1093
+ ];
1094
+ /** `GET /v1/me`: the key's product and scope (setup check). `capabilities` absent from older steward. */
1095
+ const MeResponseSchema = z.object({
1096
+ product: z.string(),
1097
+ keyName: z.string(),
1098
+ scope: z.enum([
1099
+ "product",
1100
+ "admin",
1101
+ "checkout"
1102
+ ]),
1103
+ capabilities: z.array(z.string()).optional()
1104
+ });
645
1105
  /** Değişiklik yapan her istekte zorunlu başlıklar. */
646
1106
  const HEADER_IDEMPOTENCY_KEY = "idempotency-key";
647
1107
  const HEADER_ACTOR_REF = "x-actor-ref";
@@ -689,12 +1149,20 @@ const ERROR_CODES = Object.freeze({
689
1149
  override_type_mismatch: spec(422, "validation"),
690
1150
  /** Grant girdisindeki plan katalogda yok. */
691
1151
  plan_not_found: spec(422, "validation"),
1152
+ /** Grant `benefitCodes`: a code is not an active (not archived) catalog benefit of the product. */
1153
+ benefit_not_found: spec(422, "validation"),
692
1154
  invalid_window: spec(422, "validation"),
693
1155
  already_subscribed: spec(409, "conflict"),
694
1156
  already_canceled: spec(409, "conflict"),
695
- /** `PUT …/plan` `mode: "strict"`: canlı aboneliğin planı hedefi geçiyor; hiçbir şey yazılmadı. */
1157
+ /**
1158
+ * @deprecated LEGACY — no longer produced (billing core, M35: no plan rank). Steward before the billing core
1159
+ * answered `PUT …/plan` `mode: "strict"` with it when a live subscription's plan outranked the
1160
+ * target. Kept so clients that handle it keep compiling.
1161
+ */
696
1162
  subscription_plan_higher: spec(409, "conflict"),
697
1163
  plan_not_sellable: spec(422, "validation"),
1164
+ /** Checkout `replacesSubscriptionId` (bank transfer plan H16): not a live subscription of the account. */
1165
+ replaces_subscription_invalid: spec(422, "validation"),
698
1166
  /**
699
1167
  * F4e: hesap silinmiş (anonimleştirilmiş). `/v1/accounts/:ref` altındaki her uç (tekrar
700
1168
  * `DELETE` ve aynı ref'le `PUT` dahil) 410 döner; hesap geri açılmaz.
@@ -702,13 +1170,26 @@ const ERROR_CODES = Object.freeze({
702
1170
  account_deleted: spec(410, "not_found"),
703
1171
  /** `DELETE /v1/accounts/:ref`: canlı abonelik (dönem sonunda iptali planlı dahil) var; önce hemen iptal. */
704
1172
  subscription_active: spec(409, "conflict"),
705
- /** `DELETE /v1/accounts/:ref`: sağlayıcı formu açık checkout oturumu var; süresi dolup kapanınca tekrar dene. */
1173
+ /**
1174
+ * `DELETE /v1/accounts/:ref`: sağlayıcı formu açık ya da havale bekleyen (bank transfer plan H5) checkout
1175
+ * oturumu var; kapanınca (süre dolumu, ödeme kaydı) tekrar dene.
1176
+ */
706
1177
  checkout_in_progress: spec(409, "conflict", true),
707
1178
  /** `successUrl`, `cancelUrl` or portal `returnUrl` is not in the product's allowed origins: product misconfiguration. */
708
1179
  return_url_not_allowed: spec(422, "config"),
709
1180
  /** Bu deployment'ta sağlayıcı yapılandırılmamış (503 ama beklemekle düzelmez). */
710
1181
  provider_not_configured: spec(503, "config"),
711
1182
  provider_error: spec(502, "provider"),
1183
+ /** Not an order code, or its check symbol does not match (a typo in the transfer description). */
1184
+ bank_transfer_code_invalid: spec(422, "validation"),
1185
+ /** No account of the product has this order code. */
1186
+ bank_transfer_code_not_found: spec(404, "not_found"),
1187
+ /** This bank reference is already recorded. */
1188
+ bank_transfer_duplicate: spec(409, "conflict"),
1189
+ /** Nothing of the account waits for exactly this amount, or several items do (name one), or the named checkout is not one. */
1190
+ bank_transfer_unmatched: spec(422, "conflict"),
1191
+ /** The checkout expired (only `acceptExpired` settles it), is closed or cannot be completed: refund the transfer. */
1192
+ bank_transfer_rejected: spec(409, "conflict"),
712
1193
  already_issued: spec(409, "conflict"),
713
1194
  number_taken: spec(409, "conflict"),
714
1195
  invalid_document: spec(422, "validation"),
@@ -718,17 +1199,53 @@ const ERROR_CODES = Object.freeze({
718
1199
  invoice_not_voidable: spec(409, "conflict"),
719
1200
  /** Void: ödemeli fatura; iptali iade yolundadır (F4d). */
720
1201
  invoice_has_payment: spec(409, "conflict"),
1202
+ /** `DELETE /v1/accounts/:ref`: the account has an unpaid invoice; pay or waive it first. */
1203
+ invoice_open: spec(409, "conflict"),
1204
+ /** Pay page session: the invoice is not unpaid (paid, waived, uncollectible, or not an invoice-first invoice). */
1205
+ invoice_not_payable: spec(409, "conflict"),
1206
+ /** Waive: the invoice is not unpaid. */
1207
+ invoice_not_waivable: spec(409, "conflict"),
1208
+ /**
1209
+ * `POST /v1/subscriptions/:id/change`: the requested change is not possible — no active price for
1210
+ * the target plan/interval in the subscription's currency, nothing would change, or the
1211
+ * subscription is not live. Plan lookup errors keep their codes (`plan_not_found`, `plan_not_sellable`).
1212
+ */
1213
+ subscription_change_invalid: spec(422, "validation"),
1214
+ /**
1215
+ * `POST /v1/subscriptions/:id/uncancel`: the live subscription has no cancellation scheduled for
1216
+ * the period end (an ended or immediately canceled one is `already_canceled`).
1217
+ */
1218
+ cancel_not_scheduled: spec(409, "conflict"),
1219
+ /** `POST /v1/events/ingest`: more than 1 000 events (or a body over 1 MiB) in one batch; split it. */
1220
+ batch_too_large: spec(422, "validation"),
1221
+ /** `POST /v1/events/ingest`: the unprocessed event backlog is too large; `Retry-After`, keep the batch queued (M11). */
1222
+ events_backlog: spec(503, "unavailable", true),
1223
+ /** Catalog sync: a meter's filter or aggregation changed after its first period opened; use a new meter code (M4). */
1224
+ meter_locked: spec(422, "conflict"),
1225
+ /** Credit grant (or another meter lookup): the code is not an active catalog meter of the product. */
1226
+ meter_not_found: spec(422, "validation"),
1227
+ /** Catalog sync: a metered price's tax terms differ from the plan's base price in that currency (M20). */
1228
+ metered_price_tax_mismatch: spec(422, "validation"),
1229
+ /** A second live subscription would bill the same meter's overage (M20: one billing subscription per meter). */
1230
+ meter_already_billed: spec(409, "conflict"),
721
1231
  internal_error: spec(500, "unavailable", true),
722
1232
  /** `/health`: veritabanına ulaşılamıyor. */
723
1233
  database_unavailable: spec(503, "unavailable", true),
724
- /** F2c (K26): anahtar prefix'i başına bütçe aşıldı; `Retry-After` başlığı taşır. */
1234
+ /**
1235
+ * Bugün yalnızca hosted sayfalar üretir (yanlış sayfa anahtarı / aksiyon sınırı; `hosted/routes.ts`,
1236
+ * `hosted/portal-routes.ts`); `Retry-After` başlığı taşır. `/v1` için anahtar başına bütçe (F2c, K26)
1237
+ * ertelendi — geldiğinde aynı kod kullanılır.
1238
+ */
725
1239
  rate_limited: spec(429, "unavailable", true),
726
1240
  unknown_provider: spec(404, "not_found"),
727
1241
  bad_signature: spec(401, "config"),
728
1242
  invalid_payload: spec(400, "validation"),
729
1243
  stale_event: spec(400, "validation"),
730
1244
  not_configured: spec(503, "config"),
731
- /** Yalnızca sahte sağlayıcı (yerel/test). */
1245
+ /**
1246
+ * @deprecated Üretilmiyor: sağlayıcı abonelik yolu silindi (M30) ve sahte sağlayıcı da bu kodu
1247
+ * döndürmüyor. Eski istemciler derlensin diye duruyor; bir sonraki minor sürümde kaldırılacak.
1248
+ */
732
1249
  unknown_subscription: spec(404, "not_found")
733
1250
  });
734
1251
  function isKnownErrorCode(code) {
@@ -739,6 +1256,297 @@ function errorCodeSpec(code) {
739
1256
  return isKnownErrorCode(code) ? ERROR_CODES[code] : null;
740
1257
  }
741
1258
  //#endregion
1259
+ //#region ../contract/src/meter-rules.ts
1260
+ const METADATA_PREFIX = "metadata.";
1261
+ function propertyValue(event, property) {
1262
+ if (property === "name") return event.name;
1263
+ const key = property.slice(9);
1264
+ const metadata = event.metadata;
1265
+ return metadata !== void 0 && metadata !== null && Object.hasOwn(metadata, key) ? metadata[key] : void 0;
1266
+ }
1267
+ const isScalar = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean";
1268
+ /**
1269
+ * Whether an event matches a meter filter (syntax and semantics: `meters.ts`). A condition on an
1270
+ * absent metadata key is false for every operator; `eq` needs the same type; `like` is a
1271
+ * case-sensitive substring. Throws on a filter the schema would reject.
1272
+ */
1273
+ function matchesFilter(event, filter) {
1274
+ if ("and" in filter && Array.isArray(filter.and)) return filter.and.every((f) => matchesFilter(event, f));
1275
+ if ("or" in filter && Array.isArray(filter.or)) return filter.or.some((f) => matchesFilter(event, f));
1276
+ const clause = meterFilterClauseOf(filter);
1277
+ if (clause === null) throw new Error("invalid meter filter condition");
1278
+ const value = propertyValue(event, clause.property);
1279
+ if (!isScalar(value)) return false;
1280
+ const operand = clause.value;
1281
+ switch (clause.operator) {
1282
+ case "eq": return typeof value === typeof operand && value === operand;
1283
+ case "ne": return !(typeof value === typeof operand && value === operand);
1284
+ case "gt": return typeof value === "number" && value > operand;
1285
+ case "gte": return typeof value === "number" && value >= operand;
1286
+ case "lt": return typeof value === "number" && value < operand;
1287
+ case "lte": return typeof value === "number" && value <= operand;
1288
+ case "like": return typeof value === "string" && value.includes(operand);
1289
+ case "not_like": return !(typeof value === "string" && value.includes(operand));
1290
+ }
1291
+ }
1292
+ function contributionOf(event, aggregation) {
1293
+ if (aggregation.kind === "count") return { kind: "count" };
1294
+ const value = propertyValue(event, `${METADATA_PREFIX}${aggregation.key}`);
1295
+ if (aggregation.kind === "unique") {
1296
+ if (typeof value === "string") return {
1297
+ kind: "unique",
1298
+ value: `s:${value}`
1299
+ };
1300
+ if (typeof value === "number" && Number.isFinite(value)) return {
1301
+ kind: "unique",
1302
+ value: `n:${plainDecimalOf(value)}`
1303
+ };
1304
+ if (typeof value === "boolean") return {
1305
+ kind: "unique",
1306
+ value: `b:${value}`
1307
+ };
1308
+ return { kind: "invalid_value" };
1309
+ }
1310
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return { kind: "invalid_value" };
1311
+ return {
1312
+ kind: aggregation.kind,
1313
+ micro: toMicroUnits(value)
1314
+ };
1315
+ }
1316
+ /**
1317
+ * A meter's value over a set of events (reference computation; the service recomputes a closing
1318
+ * period from raw events with the same result, M8): `count` → matching events, `sum` → total,
1319
+ * `max` → the largest value (0 when none), `unique` → distinct values. `matched` counts events
1320
+ * that passed the filter (invalid ones included), `invalid` those without a usable value.
1321
+ */
1322
+ function consumptionOf(events, meter) {
1323
+ let matched = 0;
1324
+ let invalid = 0;
1325
+ let total = 0n;
1326
+ const distinct = /* @__PURE__ */ new Set();
1327
+ for (const event of events) {
1328
+ if (!matchesFilter(event, meter.filter)) continue;
1329
+ matched += 1;
1330
+ const c = contributionOf(event, meter.aggregation);
1331
+ switch (c.kind) {
1332
+ case "invalid_value":
1333
+ invalid += 1;
1334
+ break;
1335
+ case "count":
1336
+ total += MICRO_UNITS_PER_UNIT;
1337
+ break;
1338
+ case "sum":
1339
+ total += c.micro;
1340
+ break;
1341
+ case "max":
1342
+ if (c.micro > total) total = c.micro;
1343
+ break;
1344
+ case "unique": distinct.add(c.value);
1345
+ }
1346
+ }
1347
+ return {
1348
+ consumedMicro: meter.aggregation.kind === "unique" ? BigInt(distinct.size) * MICRO_UNITS_PER_UNIT : total,
1349
+ matched,
1350
+ invalid
1351
+ };
1352
+ }
1353
+ function nonNegative(name, value) {
1354
+ if (value < 0n) throw new RangeError(`${name} must not be negative`);
1355
+ }
1356
+ /**
1357
+ * The credit rule (M5; decided 2026-09-19): the granted `meter_credit` benefits per meter code.
1358
+ * - Several benefits on one meter sum their units AND their rollover caps (plan + add-on).
1359
+ * - A benefit code granted by several layers counts once (its first counted grant).
1360
+ * - The default plan's credit for a meter counts only when no other layer credits that meter
1361
+ * (`options.defaultPlanCode`): free 50k + team 2M → 2M, free only → 50k.
1362
+ * - Benefits of another type, without a code, or whose properties do not parse are ignored.
1363
+ * The service (`AccountState.meters.includedUnits`, cycle grants) and the SDK (`stateOf`,
1364
+ * `Meters()` fallback) both use this function. Vectors: `credits.json`.
1365
+ */
1366
+ function meterCreditsOf(benefits, options = {}) {
1367
+ const defaultSource = options.defaultPlanCode ? `plan:${options.defaultPlanCode}` : null;
1368
+ const parsed = [];
1369
+ for (const benefit of benefits) {
1370
+ if (benefit.type !== "meter_credit" || typeof benefit.code !== "string") continue;
1371
+ const properties = MeterCreditPropertiesSchema.safeParse(benefit.properties);
1372
+ if (!properties.success) continue;
1373
+ const { meter, units, rolloverCapUnits } = properties.data;
1374
+ parsed.push({
1375
+ code: benefit.code,
1376
+ source: benefit.source ?? null,
1377
+ isDefault: defaultSource !== null && benefit.source === defaultSource,
1378
+ meter,
1379
+ unitsMicro: toMicroUnits(units),
1380
+ rolloverCapMicro: rolloverCapUnits === void 0 ? null : toMicroUnits(rolloverCapUnits)
1381
+ });
1382
+ }
1383
+ const creditedElsewhere = new Set(parsed.filter((b) => !b.isDefault).map((b) => b.meter));
1384
+ const credits = /* @__PURE__ */ new Map();
1385
+ const seen = /* @__PURE__ */ new Set();
1386
+ for (const benefit of parsed) {
1387
+ if (seen.has(benefit.code) || benefit.isDefault && creditedElsewhere.has(benefit.meter)) continue;
1388
+ seen.add(benefit.code);
1389
+ const entry = credits.get(benefit.meter) ?? {
1390
+ unitsMicro: 0n,
1391
+ rolloverCapMicro: null,
1392
+ benefits: []
1393
+ };
1394
+ entry.unitsMicro += benefit.unitsMicro;
1395
+ if (benefit.rolloverCapMicro !== null) entry.rolloverCapMicro = (entry.rolloverCapMicro ?? 0n) + benefit.rolloverCapMicro;
1396
+ entry.benefits.push({
1397
+ code: benefit.code,
1398
+ source: benefit.source,
1399
+ unitsMicro: benefit.unitsMicro,
1400
+ rolloverCapMicro: benefit.rolloverCapMicro
1401
+ });
1402
+ credits.set(benefit.meter, entry);
1403
+ }
1404
+ return credits;
1405
+ }
1406
+ /** `max(0, consumed − credited)` (M21). */
1407
+ function overageOf(input) {
1408
+ const overage = input.consumedMicro - input.creditedMicro;
1409
+ return overage > 0n ? overage : 0n;
1410
+ }
1411
+ /**
1412
+ * Credit standing of a meter (M14): no credit (`creditedMicro` 0) → `ok`; balance
1413
+ * (`credited − consumed`) ≤ 0 → `exhausted`; consumption ≥ `lowAt` × credit → `low` (`lowAt`
1414
+ * null disables `low`); else `ok`. Exact integer comparison (`lowAt` read as micro units).
1415
+ * `billable` does not change the standing — it describes the credit; a billable meter goes
1416
+ * `low`/`exhausted` too and only the gate stays open (M15).
1417
+ */
1418
+ function standingOf(input) {
1419
+ nonNegative("creditedMicro", input.creditedMicro);
1420
+ nonNegative("consumedMicro", input.consumedMicro);
1421
+ if (input.lowAt !== null && !(input.lowAt > 0 && input.lowAt <= 1)) throw new RangeError("lowAt must be in (0, 1]");
1422
+ if (input.creditedMicro === 0n) return "ok";
1423
+ if (input.creditedMicro - input.consumedMicro <= 0n) return "exhausted";
1424
+ if (input.lowAt !== null && input.consumedMicro * 1000000n >= toMicroUnits(input.lowAt) * input.creditedMicro) return "low";
1425
+ return "ok";
1426
+ }
1427
+ /**
1428
+ * Credit carried into the next period (M6): `min(max(closing balance, 0), cap)`; `capMicro`
1429
+ * null (rollover off) → 0. Debt never carries.
1430
+ */
1431
+ function rolloverOf(input) {
1432
+ if (input.capMicro === null) return 0n;
1433
+ nonNegative("capMicro", input.capMicro);
1434
+ const balance = input.closingBalanceMicro > 0n ? input.closingBalanceMicro : 0n;
1435
+ return balance < input.capMicro ? balance : input.capMicro;
1436
+ }
1437
+ function daysInUtcMonth(year, month) {
1438
+ const d = /* @__PURE__ */ new Date(0);
1439
+ d.setUTCFullYear(year, month + 1, 0);
1440
+ return d.getUTCDate();
1441
+ }
1442
+ /**
1443
+ * `from` + `months` months in UTC with the month-end clamp of the service's `addInterval`
1444
+ * (`service/src/periods.ts`, date-fns `addMonths` on `UTCDate`): the day of month is kept, or the
1445
+ * last day of a shorter month (Jan 31 + 1 → Feb 28/29); the time of day is kept.
1446
+ */
1447
+ function addUtcMonths(from, months) {
1448
+ if (!Number.isInteger(months)) throw new RangeError("months must be an integer");
1449
+ const total = from.getUTCMonth() + months;
1450
+ const year = from.getUTCFullYear() + Math.floor(total / 12);
1451
+ const month = (total % 12 + 12) % 12;
1452
+ const out = new Date(from.getTime());
1453
+ out.setUTCFullYear(year, month, Math.min(from.getUTCDate(), daysInUtcMonth(year, month)));
1454
+ return out;
1455
+ }
1456
+ /**
1457
+ * The monthly meter period containing `at` (M7): `[anchor + n months, anchor + (n + 1) months)`,
1458
+ * start inclusive, end exclusive, where every boundary is computed FROM THE ANCHOR (`addUtcMonths`;
1459
+ * Jan 31 → Feb 28 → Mar 31, no drift) — the same grid as the billing cycle's `nextPeriodEnd` for a
1460
+ * monthly subscription. `n` may be negative (`at` before the anchor). UTC throughout.
1461
+ */
1462
+ function periodBoundsOf(anchor, at) {
1463
+ if (Number.isNaN(anchor.getTime()) || Number.isNaN(at.getTime())) throw new RangeError("invalid date");
1464
+ let n = (at.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (at.getUTCMonth() - anchor.getUTCMonth());
1465
+ while (addUtcMonths(anchor, n).getTime() > at.getTime()) n -= 1;
1466
+ while (addUtcMonths(anchor, n + 1).getTime() <= at.getTime()) n += 1;
1467
+ return {
1468
+ start: addUtcMonths(anchor, n),
1469
+ end: addUtcMonths(anchor, n + 1)
1470
+ };
1471
+ }
1472
+ function nonNegativeInteger(name, value, min = 0) {
1473
+ if (!Number.isSafeInteger(value) || value < min) throw new RangeError(`${name} must be an integer ≥ ${min}`);
1474
+ return BigInt(value);
1475
+ }
1476
+ /** The overage amount before the cap (BigInt, half up) and the cap (null: none); validates the price. */
1477
+ function pricedOverage(input) {
1478
+ const amount = nonNegativeInteger("amountMinor", input.amountMinor);
1479
+ const perUnits = nonNegativeInteger("perUnits", input.perUnits, 1);
1480
+ const cap = input.capMinor === void 0 || input.capMinor === null ? null : nonNegativeInteger("capMinor", input.capMinor);
1481
+ if (input.overageMicro <= 0n) return {
1482
+ minor: 0n,
1483
+ cap
1484
+ };
1485
+ const numerator = input.overageMicro * amount;
1486
+ const denominator = perUnits * MICRO_UNITS_PER_UNIT;
1487
+ let minor = numerator / denominator;
1488
+ if (2n * (numerator % denominator) >= denominator) minor += 1n;
1489
+ return {
1490
+ minor,
1491
+ cap
1492
+ };
1493
+ }
1494
+ /**
1495
+ * Overage amount in minor units (M21): `round_half_up(overage × amountMinor / perUnits)` on micro
1496
+ * units with BigInt — proportional (no block rounding), always whole minor units — then clipped
1497
+ * by `capMinor`. Tax terms are the price's (gross when tax-inclusive).
1498
+ */
1499
+ function rateOverage(input) {
1500
+ const { minor: uncapped, cap } = pricedOverage(input);
1501
+ const minor = cap !== null && uncapped > cap ? cap : uncapped;
1502
+ if (minor > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError("overage amount exceeds the safe integer range");
1503
+ return Number(minor);
1504
+ }
1505
+ /**
1506
+ * Whether the period's priced overage reached the rate's `capMinor` (M15: the SDK gate closes on a
1507
+ * billable meter once the cap is reached — further use would be free): there is overage
1508
+ * (`overageMicro` > 0) and its amount before the cap (`rateOverage` without `capMinor`) is ≥ the
1509
+ * cap. No cap → false. Same validation as `rateOverage`.
1510
+ */
1511
+ function overageCapReached(input) {
1512
+ const { minor, cap } = pricedOverage(input);
1513
+ return cap !== null && input.overageMicro > 0n && minor >= cap;
1514
+ }
1515
+ /**
1516
+ * Totals and stored lines of an invoice (or meter charge) with overage lines: the manual invoice
1517
+ * rule (`manualInvoiceTotals`) with every line at quantity 1 — ONE rounding at invoice level
1518
+ * (tax-inclusive: the total is kept and split with `splitInclusiveTax`, the net spread over the
1519
+ * lines by largest remainder; exclusive: tax added on the sum). Stored lines are net and keep
1520
+ * their meter fields (`meter`, `units`, `priceAmountMinor`, `pricePerUnits`).
1521
+ */
1522
+ function meterChargeTotals(lines, terms) {
1523
+ for (const line of lines) nonNegativeInteger("amountMinor", line.amountMinor);
1524
+ const totals = manualInvoiceTotals({
1525
+ lines: lines.map((l) => ({
1526
+ description: l.description,
1527
+ quantity: 1,
1528
+ unitAmountMinor: l.amountMinor
1529
+ })),
1530
+ taxRateBps: terms.taxRateBps,
1531
+ taxInclusive: terms.taxInclusive
1532
+ });
1533
+ return {
1534
+ subtotalMinor: totals.subtotalMinor,
1535
+ taxMinor: totals.taxMinor,
1536
+ totalMinor: totals.totalMinor,
1537
+ lines: totals.lines.map((line, i) => {
1538
+ const source = lines[i];
1539
+ return {
1540
+ ...line,
1541
+ ...source.meter !== void 0 ? { meter: source.meter } : {},
1542
+ ...source.units !== void 0 ? { units: source.units } : {},
1543
+ ...source.priceAmountMinor !== void 0 ? { priceAmountMinor: source.priceAmountMinor } : {},
1544
+ ...source.pricePerUnits !== void 0 ? { pricePerUnits: source.pricePerUnits } : {}
1545
+ };
1546
+ })
1547
+ };
1548
+ }
1549
+ //#endregion
742
1550
  //#region ../contract/src/plan.ts
743
1551
  const PLAN_SET_MODES = ["ensure", "strict"];
744
1552
  const PlanSetModeSchema = z.enum(PLAN_SET_MODES);
@@ -909,7 +1717,69 @@ const DoctorCatalogSchema = z.object({
909
1717
  /** Satışa açık planların (`sellable`) bugün checkout'ta satılabilen aktif fiyatları. */
910
1718
  sellablePrices: z.number().int().nonnegative(),
911
1719
  /** Satışa açık planların satılamayan aktif fiyatları (satışa kapalı planların fiyatları sayılmaz). */
912
- unsellablePrices: z.array(DoctorUnsellablePriceSchema)
1720
+ unsellablePrices: z.array(DoctorUnsellablePriceSchema),
1721
+ /**
1722
+ * Live subscriptions whose locked amount (or tax inclusion) differs from the current price of
1723
+ * their key (M36): they keep paying what they locked until explicitly moved. Absent from older steward.
1724
+ */
1725
+ priceDriftSubscriptions: z.number().int().nonnegative().optional()
1726
+ });
1727
+ /** Billing core facts (M30). Absent from older steward. */
1728
+ const DoctorBillingSchema = z.object({
1729
+ /**
1730
+ * Live subscriptions that still carry a provider subscription reference (created by the removed
1731
+ * iyzico subscription path). Steward never invoices them and the provider may still charge them:
1732
+ * cancel each in the provider panel, then immediately in steward. Should be 0.
1733
+ */
1734
+ legacyProviderSubscriptions: z.number().int().nonnegative() });
1735
+ /**
1736
+ * Event ingest and rollup facts (metering phase 2, M29; only for a product that defines meters).
1737
+ * Absent from older steward. Names and meter codes only: no metadata, no account refs.
1738
+ */
1739
+ const DoctorEventsSchema = z.object({
1740
+ /** Non-archived meters. */
1741
+ meters: z.number().int().nonnegative(),
1742
+ /** Meters whose already received events are still being counted in (`backfilling`). */
1743
+ backfillingMeters: z.array(z.string()),
1744
+ /** This product's events not rolled up yet (counted up to just above the back-pressure limit). */
1745
+ backlogRows: z.number().int().nonnegative(),
1746
+ /** Age of this product's oldest event not rolled up yet, seconds (0 when none). */
1747
+ rollupLagSeconds: z.number().nonnegative(),
1748
+ /** The deployment's backlog is over the limits: `POST /v1/events/ingest` answers `503 events_backlog`. */
1749
+ backlogged: z.boolean(),
1750
+ /** Events of the last 7 days examined for the fields below (newest first, bounded). */
1751
+ sampledEvents: z.number().int().nonnegative(),
1752
+ /** Event names of the sample that no meter selects (top 5 by count). Normal: events are generic (M2). */
1753
+ unmatchedEventNames: z.array(z.object({
1754
+ name: z.string(),
1755
+ events: z.number().int().nonnegative()
1756
+ })),
1757
+ /** Per meter, sampled events it selects that carry no usable value (M4 `invalid_value`). */
1758
+ invalidValues: z.array(z.object({
1759
+ meter: z.string(),
1760
+ events: z.number().int().nonnegative()
1761
+ })),
1762
+ /** Non-archived meters with no usage in their current periods and no selected event in the sample. */
1763
+ metersWithoutEvents: z.array(z.string())
1764
+ });
1765
+ /** Rollup lag above which the doctor warns (the `rollup lag > 60 s` alert, M29). */
1766
+ const DOCTOR_ROLLUP_LAG_WARN_SECONDS = 60;
1767
+ /**
1768
+ * Overage facts (metering MF5, M20/M21): present when the catalog has a metered price or the
1769
+ * product has meter charges. Absent from older steward.
1770
+ */
1771
+ const DoctorOverageSchema = z.object({
1772
+ /** `settings.metering.chargeMode`: `off` | `dry_run` | `live` (open set for newer steward). */
1773
+ chargeMode: z.string(),
1774
+ /** Active metered prices of the catalog. */
1775
+ meteredPrices: z.number().int().nonnegative(),
1776
+ /**
1777
+ * Live subscriptions without a locked rate for a metered price of their plan: a metered price
1778
+ * added after a subscription started is not attached to it (M20), so its overage is not billed.
1779
+ */
1780
+ unratedSubscriptions: z.number().int().nonnegative(),
1781
+ /** Charges below the minimum invoice amount waiting for their subscription's next invoice. */
1782
+ carriedCharges: z.number().int().nonnegative()
913
1783
  });
914
1784
  const DOCTOR_PROVIDER_ENVIRONMENTS = [
915
1785
  "sandbox",
@@ -934,7 +1804,7 @@ const DoctorProviderSchema = z.object({
934
1804
  lastError: z.object({
935
1805
  /** Sağlayıcı hata kodu, ör. `iyzico:timeout`, `iyzico:1001`. */
936
1806
  code: z.string(),
937
- /** `ensure_price` | `create_checkout` | `retrieve_checkout` | `retrieve_subscription` | `cancel_subscription` | `retry_payment`. */
1807
+ /** Provider operation, e.g. `create_payment_checkout`, `retrieve_payment_checkout`, `retrieve_payment` (open set). */
938
1808
  operation: z.string(),
939
1809
  at: DateTimeSchema
940
1810
  }).nullable()
@@ -1001,6 +1871,12 @@ const DoctorReportSchema = z.object({
1001
1871
  dryRun: z.boolean() }),
1002
1872
  /** Absent in reports of steward versions without billing emails. */
1003
1873
  emails: DoctorEmailsSchema.optional(),
1874
+ /** Absent in reports of steward versions before the billing core. */
1875
+ billing: DoctorBillingSchema.optional(),
1876
+ /** Event ingest and rollup (metering); absent for a product without meters and from older steward. */
1877
+ events: DoctorEventsSchema.optional(),
1878
+ /** Metering MF5; absent without metered prices and from older steward. */
1879
+ overage: DoctorOverageSchema.optional(),
1004
1880
  warnings: z.array(DoctorWarningSchema)
1005
1881
  });
1006
1882
  /** Email readiness from settings and the deployment's transport (pure). */
@@ -1073,14 +1949,15 @@ function buildDoctorReport(facts) {
1073
1949
  };
1074
1950
  const { catalog, provider, settings, dunning } = facts;
1075
1951
  if (!catalog.synced) warn("error", "catalog_not_synced", "katalog senkronlanmamış: `PUT /v1/admin/catalog` (steward catalog push)");
1076
- const byReason = (reason) => catalog.unsellablePrices.filter((p) => p.reason === reason);
1077
- const unsynced = byReason("provider_price_missing");
1078
- 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`);
1079
- const otherUnsellable = catalog.unsellablePrices.filter((p) => p.reason !== "provider_price_missing" && p.reason !== "provider_not_configured");
1952
+ const otherUnsellable = catalog.unsellablePrices.filter((p) => p.reason !== "provider_not_configured");
1080
1953
  if (otherUnsellable.length > 0) {
1081
1954
  const reasons = [...new Set(otherUnsellable.map((p) => p.reason))].join(", ");
1082
1955
  warn("warning", "price_unsellable", `${otherUnsellable.length} fiyat satılamaz (${reasons})`);
1083
1956
  }
1957
+ const drift = catalog.priceDriftSubscriptions ?? 0;
1958
+ if (drift > 0) warn("info", "price_drift", `${drift} live subscription(s) keep a locked amount that differs from the current price; moving them to the current price is an explicit operation`);
1959
+ const legacy = facts.billing?.legacyProviderSubscriptions ?? 0;
1960
+ if (legacy > 0) warn("error", "legacy_provider_subscription", `${legacy} live subscription(s) still have a provider subscription: steward never invoices them and the provider may still charge them; cancel each in the provider panel, then immediately in steward`);
1084
1961
  if (!provider.configured) warn("error", "provider_not_configured", `ödeme sağlayıcısı ${provider.id} bu deployment'ta yapılandırılmamış`);
1085
1962
  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)`);
1086
1963
  const returnOrigins = returnOriginsOf(settings);
@@ -1109,6 +1986,20 @@ function buildDoctorReport(facts) {
1109
1986
  if (settings.emails?.billingPath === void 0) warn("info", "email_billing_link_missing", "`settings.emails.billingPath` is not set: email buttons open `appUrl`");
1110
1987
  }
1111
1988
  if (dunning.dryRun) warn("info", "dunning_dry_run", "dunning dry-run: adımlar yalnızca iz yazar, askı ve sonlandırma uygulanmaz");
1989
+ const ev = facts.events;
1990
+ if (ev) {
1991
+ if (ev.backlogged) warn("warning", "events_backlog", "the event backlog is over the limits: ingest answers 503 events_backlog (the SDK keeps batches queued)");
1992
+ if (ev.rollupLagSeconds > 60) warn("warning", "meter_rollup_lag", `the oldest event waiting for the rollup is ${ev.rollupLagSeconds} s old (${ev.backlogRows} waiting): meters lag behind`);
1993
+ for (const { meter, events } of ev.invalidValues) warn("warning", "events_invalid_value", `meter ${meter}: ${events} recent event(s) carry no usable value (sum/max need a finite, non-negative number; unique needs the key) and add nothing`);
1994
+ if (ev.metersWithoutEvents.length > 0) warn("info", "meter_without_events", `no recent event counts in meter(s) ${ev.metersWithoutEvents.join(", ")}: check the event name and filter`);
1995
+ if (ev.unmatchedEventNames.length > 0) warn("info", "events_unmatched", `recent events no meter selects: ${ev.unmatchedEventNames.map((u) => `${u.name} (${u.events})`).join(", ")} — normal when no meter is meant for them`);
1996
+ }
1997
+ const overage = facts.overage;
1998
+ if (overage && overage.meteredPrices > 0) {
1999
+ if (overage.chargeMode === "off") warn("warning", "overage_charge_mode_off", "metering.chargeMode is off: the catalog has metered prices but overage is neither priced nor invoiced");
2000
+ else if (overage.chargeMode === "dry_run") warn("info", "overage_dry_run", "metering.chargeMode is dry_run: overage is priced into meter charges (GET /v1/accounts/:ref/meter-charges) but not invoiced; switch to live after one full cycle");
2001
+ }
2002
+ if (overage && overage.unratedSubscriptions > 0) warn("info", "overage_unrated_subscriptions", `${overage.unratedSubscriptions} live subscription(s) have no locked rate for a metered price of their plan (added after they started, M20): their overage is not billed until they move to the current terms`);
1112
2003
  return {
1113
2004
  ok: !warnings.some((w) => w.severity === "error"),
1114
2005
  checkedAt: facts.checkedAt.toISOString(),
@@ -1123,8 +2014,11 @@ function buildDoctorReport(facts) {
1123
2014
  endpoints,
1124
2015
  dunning,
1125
2016
  ...emails ? { emails } : {},
2017
+ ...facts.billing ? { billing: facts.billing } : {},
2018
+ ...facts.events ? { events: facts.events } : {},
2019
+ ...overage ? { overage } : {},
1126
2020
  warnings
1127
2021
  };
1128
2022
  }
1129
2023
  //#endregion
1130
- export { InvoiceVoidResponseSchema as $, PriceSchema as $t, WebhookSecretSchema as A, checkoutRedirectUrl as At, errorCodeSpec as B, TtlMinutesSchema as Bt, WEBHOOK_SECRET_PREFIX as C, CheckoutSessionCreateInputSchema as Ct, WebhookEndpointSchema as D, ConsentSchema as Dt, WebhookEndpointRotateInputSchema as E, CheckoutStatusSchema as Et, PlanSetModeSchema as F, ProductSettingsPatchSchema as Ft, CheckoutSessionResponseSchema as G, AccountsRefreshResultSchema as Gt, AccountDetailSchema as H, applySettingsPatch as Ht, PlanSetResponseSchema as I, ProductSettingsSchema as It, HEADER_API_VERSION as J, CatalogSyncResultSchema as Jt, GrantCreateResponseSchema as K, CatalogSchema as Kt, PlanSetWarningSchema as L, ReturnOriginSchema as Lt, webhookEndpointUrlProblem as M, BrandingSchema as Mt, PLAN_SET_MODES as N, DEFAULT_CHECKOUT_TTL_MINUTES as Nt, WebhookEndpointStatusSchema as O, MAX_CHECKOUT_CONSENT_DOCS as Ot, PlanSetInputSchema as P, DEFAULT_PORTAL_TTL_MINUTES as Pt, InvoiceIssueResponseSchema as Q, PriceDefSchema as Qt, ResyncResultSchema as R, SETTINGS_LOCALES as Rt, WEBHOOK_ENDPOINT_STATUSES as S, CheckoutPrefillSchema as St, WebhookEndpointListSchema as T, CheckoutSessionSchema as Tt, AccountUpsertResponseSchema as U, isReturnUrlAllowed as Ut, isKnownErrorCode as V, WebUrlSchema as Vt, CancelSubscriptionResponseSchema as W, returnOriginsOf as Wt, HEADER_REQUEST_ID as X, FeatureTypeSchema as Xt, HEADER_IDEMPOTENCY_KEY as Y, FeatureDefSchema as Yt, HEADER_SDK as Z, PlanDefSchema as Zt, emailReadinessOf as _, PORTAL_SESSION_ERRORS as _t, DOCTOR_SEVERITIES as a, InvoiceVoidInputSchema as at, DEFAULT_ENDPOINT_EVENT_TYPES as b, CHECKOUT_ERRORS as bt, DoctorEndpointSchema as c, ManualInvoiceLineInputSchema as ct, DoctorReportSchema as d, CancelSubscriptionInputSchema as dt, ManualInvoiceResponseSchema as et, DoctorSettingsSchema as f, GrantCreateInputSchema as ft, buildDoctorReport as g, SubscriptionSchema as gt, DoctorWarningSchema as h, StatsSchema as ht, DOCTOR_REQUIRED_LEGAL_LINKS as i, InvoiceStatusSchema as it, generateWebhookSecret as j, BrandingPatchSchema as jt, WebhookLastDeliverySchema as k, ProviderPayloadSchema as kt, DoctorHostedSchema as l, manualInvoiceTotals as lt, DoctorUnsellablePriceSchema as m, PaymentSchema as mt, DOCTOR_PROVIDER_ENVIRONMENTS as n, InvoiceLineSchema as nt, DoctorCatalogSchema as o, MANUAL_INVOICE_MAX_AMOUNT_MINOR as ot, DoctorSeveritySchema as p, GrantSchema as pt, HEADER_ACTOR_REF as q, CatalogSyncInputSchema as qt, DOCTOR_REQUIRED_BRANDING as r, InvoiceSchema as rt, DoctorEmailsSchema as s, ManualInvoiceInputSchema as st, DOCTOR_PENDING_BACKLOG_MS as t, InvoiceIssueInputSchema as tt, DoctorProviderSchema as u, splitInclusiveTax as ut, endpointDiagnosisOf as v, PortalSessionCreateInputSchema as vt, WebhookEndpointCreateInputSchema as w, CheckoutSessionCreatedSchema as wt, EventTypeSchema as x, CheckoutConsentInputSchema as xt, hostedReadinessOf as y, PortalSessionSchema as yt, ERROR_CODES as z, SettingsLocaleSchema as zt };
2024
+ export { rolloverOf as $, MAX_CATALOG_BENEFITS as $n, CheckoutSessionCreatedSchema as $t, WebhookEndpointRotateInputSchema as A, MeteringSettingsPatchSchema as An, InvoiceWaiveInputSchema as At, PlanSetResponseSchema as B, applySettingsPatch as Bn, PaymentSchema as Bt, hostedReadinessOf as C, DEFAULT_PORTAL_TTL_MINUTES as Cn, InvoiceIssueInputSchema as Ct, WEBHOOK_SECRET_PREFIX as D, LateGraceMinutesSchema as Dn, InvoiceSchema as Dt, WEBHOOK_ENDPOINT_STATUSES as E, EventsSettingsSchema as En, InvoicePaymentSessionSchema as Et, generateWebhookSecret as F, ReturnOriginSchema as Fn, splitInclusiveTax as Ft, contributionOf as G, returnOriginsOf as Gn, PortalSessionCreateInputSchema as Gt, ResyncResultSchema as H, eventsSettingsOf as Hn, SubscriptionChangeInputSchema as Ht, webhookEndpointUrlProblem as I, SETTINGS_LOCALES as In, CancelSubscriptionInputSchema as It, meterCreditsOf as J, CatalogSchema as Jn, BankTransferRecordedSchema as Jt, matchesFilter as K, AccountsRefreshResultSchema as Kn, PortalSessionSchema as Kt, PLAN_SET_MODES as L, SettingsLocaleSchema as Ln, GrantCreateInputSchema as Lt, WebhookEndpointStatusSchema as M, PaymentDueDaysSchema as Mn, ManualInvoiceInputSchema as Mt, WebhookLastDeliverySchema as N, ProductSettingsPatchSchema as Nn, ManualInvoiceLineInputSchema as Nt, WebhookEndpointCreateInputSchema as O, METER_CHARGE_MODES as On, InvoiceStatusSchema as Ot, WebhookSecretSchema as P, ProductSettingsSchema as Pn, manualInvoiceTotals as Pt, rateOverage as Q, FeatureTypeSchema as Qn, CheckoutSessionCreateInputSchema as Qt, PlanSetInputSchema as R, TtlMinutesSchema as Rn, GrantSchema as Rt, endpointDiagnosisOf as S, DEFAULT_PAYMENT_DUE_DAYS as Sn, UncancelSubscriptionResponseSchema as St, EventTypeSchema as T, EventsSettingsPatchSchema as Tn, InvoicePaymentSessionCreateInputSchema as Tt, addUtcMonths as U, isReturnUrlAllowed as Un, SubscriptionSchema as Ut, PlanSetWarningSchema as V, billingSettingsOf as Vn, StatsSchema as Vt, consumptionOf as W, meteringSettingsOf as Wn, PORTAL_SESSION_ERRORS as Wt, overageOf as X, CatalogSyncResultSchema as Xn, CheckoutConsentInputSchema as Xt, overageCapReached as Y, CatalogSyncInputSchema as Yn, CHECKOUT_ERRORS as Yt, periodBoundsOf as Z, FeatureDefSchema as Zn, CheckoutPrefillSchema as Zt, DoctorSeveritySchema as _, DEFAULT_CHECKOUT_TTL_MINUTES as _n, InvoiceVoidResponseSchema as _t, DOCTOR_ROLLUP_LAG_WARN_SECONDS as a, PAYMENT_METHODS as an, PriceDefSchema as ar, AccountUpsertResponseSchema as at, buildDoctorReport as b, DEFAULT_METER_CHARGE_MODE as bn, MeResponseSchema as bt, DoctorCatalogSchema as c, checkoutRedirectUrl as cn, CheckoutSessionResponseSchema as ct, DoctorEventsSchema as d, BankTransferSettingsSchema as dn, HEADER_ACTOR_REF as dt, CheckoutSessionSchema as en, MAX_CATALOG_METERED_PRICES as er, standingOf as et, DoctorHostedSchema as f, BillingSettingsPatchSchema as fn, HEADER_API_VERSION as ft, DoctorSettingsSchema as g, DEFAULT_BANK_TRANSFER_PAYMENT_DAYS as gn, InvoiceIssueResponseSchema as gt, DoctorReportSchema as h, BrandingSchema as hn, HEADER_SDK as ht, DOCTOR_REQUIRED_LEGAL_LINKS as i, MAX_CHECKOUT_CONSENT_DOCS as in, PlanDefSchema as ir, AccountDetailSchema as it, WebhookEndpointSchema as j, MeteringSettingsSchema as jn, MANUAL_INVOICE_MAX_AMOUNT_MINOR as jt, WebhookEndpointListSchema as k, MeterChargeModeSchema as kn, InvoiceVoidInputSchema as kt, DoctorEmailsSchema as l, BankTransferPaymentDaysSchema as ln, CreditGrantResponseSchema as lt, DoctorProviderSchema as m, BrandingPatchSchema as mn, HEADER_REQUEST_ID as mt, DOCTOR_PROVIDER_ENVIRONMENTS as n, ConsentSchema as nn, MAX_PLAN_BENEFITS as nr, errorCodeSpec as nt, DOCTOR_SEVERITIES as o, PaymentMethodSchema as on, PriceSchema as or, CancelSubscriptionResponseSchema as ot, DoctorOverageSchema as p, BillingSettingsSchema as pn, HEADER_IDEMPOTENCY_KEY as pt, meterChargeTotals as q, CatalogPlanSchema as qn, BankTransferRecordInputSchema as qt, DOCTOR_REQUIRED_BRANDING as r, INDIVIDUAL_IDENTITY_PLACEHOLDER as rn, METERED_PRICE_TAX_MISMATCH as rr, isKnownErrorCode as rt, DoctorBillingSchema as s, ProviderPayloadSchema as sn, ChangeSubscriptionResponseSchema as st, DOCTOR_PENDING_BACKLOG_MS as t, CheckoutStatusSchema as tn, MAX_CATALOG_METERS as tr, ERROR_CODES as tt, DoctorEndpointSchema as u, BankTransferSettingsPatchSchema as un, GrantCreateResponseSchema as ut, DoctorUnsellablePriceSchema as v, DEFAULT_EVENT_RETENTION_DAYS as vn, InvoiceWaiveResponseSchema as vt, DEFAULT_ENDPOINT_EVENT_TYPES as w, EventRetentionDaysSchema as wn, InvoiceLineSchema as wt, emailReadinessOf as x, DEFAULT_MIN_INVOICE_MINOR as xn, STEWARD_CAPABILITIES as xt, DoctorWarningSchema as y, DEFAULT_LATE_GRACE_MINUTES as yn, ManualInvoiceResponseSchema as yt, PlanSetModeSchema as z, WebUrlSchema as zn, MAX_GRANT_BENEFITS as zt };