@stewardhq/sdk 0.1.0-rc.0 → 0.3.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 CHANGED
@@ -17,7 +17,7 @@ ESM, Node ≥ 22. Tek çalışma zamanı bağımlılığı zod.
17
17
 
18
18
  | Import | İçerik |
19
19
  | --- | --- |
20
- | `@stewardhq/sdk` | Tüm şema/tipler + istemciler (`createSteward`, `createCatalogCache`, `stateCache`, fırlatmayan `createBillingClient`) + yardımcılar: hata sınıfları, `httpStatusFor`, `errorMessages`/`messageFor`, `defineCatalog`, `formatPrice`/`taxBreakdown`, `isValidTckn`/`isValidVkn`/`isE164` |
20
+ | `@stewardhq/sdk` | Tüm şema/tipler + fırlatan istemci `createSteward`, hesap durumu önbelleği `stateCache`, `defineCatalog`, hata sınıfları + `httpStatusFor`, `isValidTckn`/`isValidVkn`/`isE164` |
21
21
  | `@stewardhq/sdk/contract` | Yalnızca şemalar ve tipler |
22
22
  | `@stewardhq/sdk/webhook` | `signWebhook` / `verifyWebhook` / `verifyWebhookRaw` (Node HMAC, senkron) |
23
23
  | `@stewardhq/sdk/webhook/web` | Aynı API WebCrypto ile (async; edge/workerd/tarayıcı) |
@@ -127,7 +127,7 @@ import { Checkout } from "@stewardhq/sdk/server";
127
127
  const checkout = Checkout({
128
128
  steward, cache: state,
129
129
  // mutlak; origin steward ayarında izinli (appUrl + extraReturnOrigins). Yer tutucular varsa
130
- // steward yalnızca onları doldurur, yoksa ?session=&status= ekler.
130
+ // steward yalnızca onları doldurur; yoksa adres aynen kullanılır (parametre eklenmez).
131
131
  successUrl: `${process.env.APP_URL}/{LOCALE}/app/orgs/{ACCOUNT_REF}/billing?checkout={CHECKOUT_ID}`,
132
132
  consents: (locale) => legalDocs(locale), // [{document, version, url}] ya da dizi; sayfada onaylatılır
133
133
  // cancelUrl (yoksa ayar appUrl), locale ve currency (yoksa ürün ayarı)
@@ -155,9 +155,8 @@ app.get("/v1/orgs/:orgId/billing/checkout/:id", async (c) => // dönü
155
155
  | Yanıtta `url` yok (F2b öncesi steward) | `StewardContractError` |
156
156
 
157
157
  Kullanıcı sayfayı kapatırsa oturum süresi dolunca (`checkoutTtlMinutes`) steward kapatır;
158
- ürün sonucu `account.state_changed` ile öğrenir. `profile` + `identityNumber` taşıyan eski gövde
159
- (`steward.checkoutSessions.create`) açılışta sağlayıcı formu döndürmeye devam eder (N3 + 1
160
- release sonra kaldırılır).
158
+ ürün sonucu `account.state_changed` ile öğrenir. `steward.checkoutSessions.create` de yalnızca
159
+ hosted gövdeyi kabul eder; fatura profili yalnızca hosted checkout ve portal sayfalarında yazılır.
161
160
 
162
161
  Hosted müşteri portalı (`/server`) — abonelik durumu, dönem sonunda iptal, ödemeyi yeniden dene,
163
162
  fatura listesi/PDF ve kurumsal fatura profili steward'ın sayfasında; ürün yalnızca yetkiyi
@@ -211,22 +210,20 @@ await steward.grants.create(accountRef, { planCode: "team", reason: "kurumsal s
211
210
 
212
211
  | Kaynak | Metotlar |
213
212
  | --- | --- |
214
- | `accounts` | `upsert(ref, input)`, `get(ref, {include?})`, `state(ref)`, `readState(ref, {etag?})` (304 → `{notModified: true}`), `entitlements(ref)`, `putBillingProfile(ref, profile)`, `setPlan(ref, {planCode \| null, reason, mode?, endsAt?, overrides?})` → `{grant, account, warnings}` (canlı abonelik planı hedefi geçiyorsa `mode: "ensure"` uyarı, `"strict"` `subscription_plan_higher`), `resync(ref)` → `{queued, eventId}` (saklı durum `account.state_changed`, `cause: "resync"`), `delete(ref)` → `{ref, deletedAt}` (KVKK anonimleştirme; faturalar kalır; önce canlı abonelik `immediate` iptal — yoksa `subscription_active`, formu açık checkout'ta `checkout_in_progress`; sonra hesabın her ucu `account_deleted` 410) |
213
+ | `accounts` | `upsert(ref, input)`, `get(ref, {include?})`, `state(ref)`, `readState(ref, {etag?})` (304 → `{notModified: true}`), `setPlan(ref, {planCode \| null, reason, mode?, endsAt?, overrides?})` → `{grant, account, warnings}` (canlı abonelik planı hedefi geçiyorsa `mode: "ensure"` uyarı, `"strict"` `subscription_plan_higher`), `resync(ref)` → `{queued, eventId}` (saklı durum `account.state_changed`, `cause: "resync"`), `delete(ref)` → `{ref, deletedAt}` (KVKK anonimleştirme; faturalar kalır; önce canlı abonelik `immediate` iptal — yoksa `subscription_active`, formu açık checkout'ta `checkout_in_progress`; sonra hesabın her ucu `account_deleted` 410) |
215
214
  | `checkoutSessions` | `create(ref, input)`, `get(id)` |
216
215
  | `portalSessions` | `create(ref, {locale?, returnUrl?})` → `{id, url, expiresAt}` (hosted müşteri portalı; `CustomerPortal()` bunu sarar) |
217
216
  | `subscriptions` | `cancel(id, input)` |
218
217
  | `grants` | `create(ref, input)`, `revoke(id)` |
219
218
  | `invoices` | `issue(id, input)`, `document(id)` → `{contentType, bytes}`, `createManual(ref, {lines, currency, taxRateBps, taxInclusive?, dueAt?, note?})` (manuel taslak; profil gerekir, `issue` ile kesilir), `void(id, {reason})` (yalnızca manuel fatura; `invoice.voided` yalnızca filtresinde isteyen endpoint'e) |
220
219
  | `catalog` | `get()`, `read({etag?})` |
221
- | `events` | `list({accountRef?, cursor?, limit?})`, `redeliver(id)` |
222
220
  | — | `stats()`, `me()`, `doctor()` → kurulum teşhisi (`GET /v1/doctor`; aşağıda) |
223
221
  | `admin` | `catalog.sync(input)`, `accounts.refresh()`, `settings.{get(), update(patch)}`, `endpoints.{list(), create(input), rotate(id, {secret}), deactivate(id)}` — `STEWARD_ADMIN_URL` + `STEWARD_ADMIN_API_KEY` (ya da `adminBaseUrl`/`adminApiKey`) ister; yoksa çağrıda `StewardConfigError` |
224
- | `raw` | Fırlatmayan, tekrar denemeyen sözleşme istemcisi (`createBillingClient`; `{ ok, data }` / `{ ok: false, status, error }`) |
225
222
 
226
223
  - **Mutasyonlar** son argümanda `{ idempotencyKey?, actorRef? }` alır. Anahtar
227
224
  verilmezse çağrı başına üretilir; `actorRef` verilmezse `createSteward`'daki varsayılan gider.
228
225
  İkisi de yoksa ya da biçimi geçersizse (`tür[:kimlik]`) istek **gitmez**: `StewardConfigError`
229
- (`code` servisinkiyle aynı: `actor_ref_required` / `invalid_actor_ref`). `raw` bu denetimi yapmaz.
226
+ (`code` servisinkiyle aynı: `actor_ref_required` / `invalid_actor_ref`).
230
227
  - **Yalnızca yönetim:** `STEWARD_URL` ve `STEWARD_API_KEY` ikisi de yoksa ve `STEWARD_ADMIN_URL` +
231
228
  `STEWARD_ADMIN_API_KEY` (ya da `adminBaseUrl`/`adminApiKey`) verilmişse istemci kurulur (CLI, katalog
232
229
  senkronu): `admin.*` çalışır, iki kapsama açık `doctor()` ve `me()` admin anahtarıyla gider, ürün uçları
@@ -279,16 +276,6 @@ const report = await steward.doctor();
279
276
  Kodlar ve teşhisler açık kümedir (bilinmeyeni yalnızca gösterin). `provider.healthy` ve
280
277
  `lastError` yanıtı veren steward pod'unun gözlemidir.
281
278
 
282
- Katalog önbelleği — TTL içinde bellekten, sonra `If-None-Match` (304 gövdeyi korur);
283
- eşzamanlı çağrılar tek istek; başarılı okumadan sonraki hatada bayat değer + `logger.warn`:
284
-
285
- ```ts
286
- import { createCatalogCache } from "@stewardhq/sdk";
287
-
288
- const catalogCache = createCatalogCache(steward, { ttlMs: 60_000, logger });
289
- const { plans, prices } = await catalogCache.get();
290
- ```
291
-
292
279
  Katalog — kodda tipli tanım; eksik/fazla hak, yanlış tip (`limit` → tam sayı ya da
293
280
  `null` = sınırsız, `flag` → boolean, `text` → string) ve tanımsız plana fiyat derleme
294
281
  hatasıdır, tipler atlatılırsa tanım anında `StewardConfigError` fırlatır (TypeScript ≥ 5.4):
@@ -317,25 +304,24 @@ const placeholder = catalog.stateOf("org_1", "team", { accessState: "active", di
317
304
  katalogda yoksa varsayılan plandan) alır, asla "sınırsız" üretmez; `onFallback`
318
305
  ile alarm verilebilir. Snapshot yoksa varsayılan planın hakları.
319
306
 
320
- Hatalar ve metinler — ürün API'sinin durum kodu ve kullanıcı metni:
307
+ Hatalar — ürün API'sinin durum kodu:
321
308
 
322
309
  ```ts
323
- import { httpStatusFor, messageFor, StewardError } from "@stewardhq/sdk";
310
+ import { httpStatusFor, StewardError } from "@stewardhq/sdk";
324
311
 
325
312
  if (error instanceof StewardError) {
326
313
  const { status, headers } = httpStatusFor(error, { logger }); // already_subscribed 409, rate_limited 503 + Retry-After…
327
- return Response.json({ error: error.code, message: messageFor(error.code, locale) }, { status, headers });
314
+ return Response.json({ error: error.code }, { status, headers });
328
315
  }
329
316
  ```
330
317
 
331
318
  Tabloda olmayan kod `category`'ye göre eşlenir (`not_found` 404, `conflict` 409,
332
319
  `provider` 502, `unavailable` 503; `validation`/`config` 500 + ERROR log).
333
- `errorMessages.{tr,en}` sözleşmenin `ERROR_CODES` kaydındaki her kodu ve istemci
334
- kodlarını (`network_error`, `timeout`, `invalid_response`, `config_error`) kapsar.
320
+ Kullanıcıya gösterilen metin ürünündür (`error.code` → kendi i18n'iniz); `error.message`
321
+ log içindir (steward'ın mesajı, yoksa kod). Fiyat gösterimi hosted checkout/portal
322
+ sayfalarındadır; SDK para biçimlendirme yardımcısı taşımaz.
335
323
 
336
- Para ve doğrulayıcılar: `formatPrice(price, "tr")` → `₺1.250,00 · KDV dahil`;
337
- `taxBreakdown(price)` servisin faturadaki matrah/KDV ayrıştırmasıyla kuruşu kuruşuna
338
- aynıdır. `isValidTckn`, `isValidVkn` (sağlama hanesi), `isE164(value, {country: "TR"}?)`
324
+ Doğrulayıcılar: `isValidTckn`, `isValidVkn` (sağlama hanesi), `isE164(value, {country: "TR"}?)`
339
325
  değeri olduğu gibi denetler (boşluk kırpmaz).
340
326
 
341
327
  Testler (`/testing`, Node) — sahte steward sunucu açmaz, `fetch` verir; başlıkları
@@ -352,7 +338,7 @@ const fake = createFakeSteward({
352
338
  });
353
339
  fake.connect((req) => webhooks.handle(req)); // alıcı sonradan (ya da webhook.handler); uygulama fake'ten sonra kurulabilir
354
340
  const steward = fake.steward; // ya da ürünün kendi createSteward({ fetch: fake.fetch, baseUrl: fake.baseUrl, … })
355
- await steward.accounts.upsert("org_1", { displayName: "Org" });
341
+ await steward.accounts.upsert("org_1", { displayName: "Org", notificationEmails: ["owner@acme.com"] }); // billing email recipients (optional)
356
342
  const session = await steward.checkoutSessions.create("org_1", input);
357
343
  await fake.simulate.checkoutCompleted(session.id); // ya da checkoutFailed; hosted oturumda checkoutCanceled da
358
344
 
@@ -378,19 +364,25 @@ await fake.simulate.renewal("org_1", { fail: true }); // saat dönem sonuna; pa
378
364
  await fake.simulate.advance({ days: 7 }); // zamanlanmış işler kendi anında (dunning, dönem sonu iptali, checkout süresi, grant penceresi, portal oturumu silme)
379
365
  fake.simulate.outage(true, { mode: "503" }); // ya da "network": fetch reddeder
380
366
  await fake.simulate.deliver("account.state_changed", { ref: "org_1", duplicate: true, outOfOrder: true });
367
+ fake.simulate.redeliver(eventId); // operatörün pod CLI `redeliver-event`'i: aynı event sonraki flush'ta yeniden
368
+
369
+ fake.emails("org_1"); // billing emails steward would send: [{ template, to, locale, eventId, … }]
381
370
 
382
371
  const { request } = signedEvent("subscription.renewed", { accountRef: "org_1" }, { secret });
383
372
  await webhooks.handle(request());
384
373
  ```
385
374
 
386
- `fake.requests` gelen istekleri, `fake.events(ref?)` yayınlanan event'leri tutar. Endpoint `webhook`
375
+ `fake.requests` gelen istekleri, `fake.events(ref?)` yayınlanan event'leri tutar. `fake.emails(ref?)`
376
+ steward'ın göndereceği faturalandırma e-postalarını servisle aynı kurallarla (şablon, gönderim anındaki
377
+ alaka, alıcılar, dil, kapatılmış şablon) listeler; içerik render edilmez ve gönderim gecikmesizdir
378
+ ([e-postalar](../docs/integrate/emails.md#test)). Endpoint `webhook`
387
379
  seçeneğiyle oluşturmada açılır; filtre verilmezse yönetim ucunun yeni ürüne verdiği
388
380
  `["account.state_changed"]`, `eventTypes: "legacy"` pod CLI'nın filtresiz (15 eski tip) endpoint'i.
389
381
  Alıcı bağlanmadan `flush`/`deliver` fırlatır (event'ler outbox'ta bekler); `connect` alıcıyı değiştirir.
390
382
  Sahtenin bilerek farklı olduğu yerler: teslimat asenkron değil (`flush()`), `dead`
391
383
  olmaz ve geri çekilme beklenmez; sağlayıcı hata vermez; dönüş origin'i (`settings.appUrl`,
392
384
  `settings.extraReturnOrigins` ya da kısayolu `returnOrigins`) hiç ayarlanmazsa her
393
- `returnUrl` kabul; `admin.endpoints` ile açılan endpoint yalnızca kayıttır (teslimat
385
+ `successUrl`/`cancelUrl`/`returnUrl` kabul; `admin.endpoints` ile açılan endpoint yalnızca kayıttır (teslimat
394
386
  `webhook` seçeneğinin alıcısına gider, `lastDelivery` hep null; `doctor()`'da teşhisi
395
387
  `no_deliveries`, sağlayıcı `fake` ve sağlıklı); dunning'de yeniden deneme
396
388
  sonucu `renewal` ile gelir; her yanıt `X-Request-Id` (hata gövdesinde `requestId`) taşır;
@@ -419,14 +411,16 @@ sonuçlandırmaz: servisteki gibi sonuç sonradan gelir — `simulate.renewal(re
419
411
  siparişi başarılı sayar (`subscription.reactivated`), `{fail: true}` yine başarısız.
420
412
  `simulate.portalCancel/portalRetryPayment` bilinmeyen ya da süresi geçmiş oturumda fırlatır.
421
413
 
422
- ## `@z9cloud/steward-contract`'tan geçiş
414
+ ## `@steward/contract`'tan geçiş
423
415
 
424
416
  | Eski | Yeni |
425
417
  | --- | --- |
426
418
  | `@z9cloud/steward-contract` | `@stewardhq/sdk` (ya da yalnız şema için `@stewardhq/sdk/contract`) |
427
- | `@z9cloud/steward-contract/client` | `@stewardhq/sdk` (`createBillingClient` aynen; fırlatan istemci `createSteward`) |
419
+ | `@z9cloud/steward-contract/client` | `@stewardhq/sdk` `createSteward` (fırlatan; fırlatmayan `createBillingClient` dışa açılmaz) |
428
420
  | `@z9cloud/steward-contract/webhook` | `@stewardhq/sdk/webhook` |
429
421
  | `@z9cloud/steward-contract/fixtures/events/*.json` | `@stewardhq/sdk/testing/fixtures/events/*.json` |
422
+ | `getEntitlements(ref)` / `accounts.entitlements(ref)` | `accounts.state(ref)` ya da `stateCache` (tek okuma modeli `AccountState`; uç 2026-09-16'da kaldırıldı) |
423
+ | `listEvents` / `redeliverEvent` / `events.*` | Yok (2026-09-16): kaçırılan durum için `accounts.resync(ref)`; dead teslimatı operatör pod CLI `redeliver-event` ile yeniden gönderir (testte `fake.simulate.redeliver`) |
430
424
 
431
425
  GitHub Packages `.npmrc` satırı ve Docker `--secret` bağlaması artık gerekmez.
432
426
 
@@ -434,4 +428,4 @@ GitHub Packages `.npmrc` satırı ve Docker `--secret` bağlaması artık gerekm
434
428
 
435
429
  Semver. Alan kaldırma/yeniden adlandırma kırıcıdır (major; 1.0 öncesi minor).
436
430
  Yayın: `sdk/package.json` sürümü artırılır, `sdk-v<sürüm>` etiketi push edilir
437
- (`.github/workflows/sdk-publish.yml`, npm provenance ile).
431
+ (`.github/workflows/sdk-publish.yml`).
@@ -1,4 +1,4 @@
1
- import { R as errorCodeSpec } from "./src.js";
1
+ import { B as errorCodeSpec } from "./src.js";
2
2
  //#region src/errors.ts
3
3
  var StewardError = class extends Error {
4
4
  code;
@@ -1,8 +1,17 @@
1
1
  import { z } from "zod";
2
2
  //#region ../contract/src/accounts.d.ts
3
+ declare const AccountLocaleSchema: z.ZodEnum<{
4
+ tr: "tr";
5
+ en: "en";
6
+ }>;
3
7
  declare const AccountUpsertInputSchema: z.ZodObject<{
4
8
  displayName: z.ZodString;
5
9
  metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
10
+ notificationEmails: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodEmail>, z.ZodTransform<string[], string[]>>>;
11
+ locale: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
12
+ tr: "tr";
13
+ en: "en";
14
+ }>>>;
6
15
  }, z.core.$strip>;
7
16
  type AccountUpsertInput = z.input<typeof AccountUpsertInputSchema>;
8
17
  declare const BillingProfileKindSchema: z.ZodEnum<{
@@ -263,6 +272,11 @@ declare const AccountSchema: z.ZodObject<{
263
272
  cancelAt: z.ZodNullable<z.ZodISODateTime>;
264
273
  }, z.core.$strip>>;
265
274
  }, z.core.$strip>;
275
+ notificationEmails: z.ZodDefault<z.ZodArray<z.ZodString>>;
276
+ locale: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
277
+ tr: "tr";
278
+ en: "en";
279
+ }>>>;
266
280
  }, z.core.$strip>;
267
281
  type Account = z.infer<typeof AccountSchema>;
268
282
  declare const AccountDeleteResponseSchema: z.ZodObject<{
@@ -534,4 +548,4 @@ declare const PingSchema: z.ZodObject<{
534
548
  }, z.core.$strip>;
535
549
  type Ping = z.infer<typeof PingSchema>;
536
550
  //#endregion
537
- export { AccountProfileState as A, AccountUpsertInputSchema as B, AccountDeleteResponse as C, AccountInclude as D, AccountDunningStateSchema as E, AccountState as F, EntitlementSnapshot as G, BillingProfileInput as H, AccountStateSchema as I, SubscriptionStatusSchema as J, EntitlementSnapshotSchema as K, AccountSubscriptionState as L, AccountScheduledChange as M, AccountScheduledChangeSchema as N, AccountOpenCheckout as O, AccountSchema as P, AccountSubscriptionStateSchema as R, Account as S, AccountDunningState as T, BillingProfileKindSchema as U, BillingProfile as V, BillingProfileSchema as W, SubscriptionSummarySchema as X, SubscriptionSummary as Y, PingSchema as _, BillingEvent as a, AccessState as b, EVENT_DETAIL_SCHEMAS as c, EventIdSchema as d, EventType as f, Ping as g, PING_EVENT_TYPE as h, ApiVersionSchema as i, AccountProfileStateSchema as j, AccountOpenCheckoutSchema as k, EVENT_TYPES as l, LegacyEventType as m, AnyBillingEvent as n, BillingEventOf as o, LEGACY_EVENT_TYPES as p, SubscriptionStatus as q, AnyBillingEventSchema as r, BillingEventSchema as s, AccountStateChangeCause as t, EventDetail as u, isEventType as v, AccountDeleteResponseSchema as w, AccessStateSchema as x, ACCOUNT_INCLUDES as y, AccountUpsertInput as z };
551
+ export { AccountOpenCheckoutSchema as A, AccountUpsertInput as B, AccountDeleteResponse as C, AccountInclude as D, AccountDunningStateSchema as E, AccountSchema as F, BillingProfileSchema as G, BillingProfile as H, AccountState as I, SubscriptionStatus as J, EntitlementSnapshot as K, AccountStateSchema as L, AccountProfileStateSchema as M, AccountScheduledChange as N, AccountLocaleSchema as O, AccountScheduledChangeSchema as P, AccountSubscriptionState as R, Account as S, AccountDunningState as T, BillingProfileInput as U, AccountUpsertInputSchema as V, BillingProfileKindSchema as W, SubscriptionSummary as X, SubscriptionStatusSchema as Y, SubscriptionSummarySchema as Z, PingSchema as _, BillingEvent as a, AccessState as b, EVENT_DETAIL_SCHEMAS as c, EventIdSchema as d, EventType as f, Ping as g, PING_EVENT_TYPE as h, ApiVersionSchema as i, AccountProfileState as j, AccountOpenCheckout as k, EVENT_TYPES as l, LegacyEventType as m, AnyBillingEvent as n, BillingEventOf as o, LEGACY_EVENT_TYPES as p, EntitlementSnapshotSchema as q, AnyBillingEventSchema as r, BillingEventSchema as s, AccountStateChangeCause as t, EventDetail as u, isEventType as v, AccountDeleteResponseSchema as w, AccessStateSchema as x, ACCOUNT_INCLUDES as y, AccountSubscriptionStateSchema as z };
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  //#region ../contract/src/common.ts
3
3
  const API_VERSION = "2026-10-01";
4
4
  const CodeSchema = z.string().regex(/^[a-z][a-z0-9_]{0,62}$/, "küçük harf, rakam ve _ (harfle başlar)");
5
- /** Ürün tarafındaki değişmez kimlik (ör. nixscale'de ağın shortId'si). */
5
+ /** Immutable product-side identity (e.g. the id of an org in the product). */
6
6
  const ExternalRefSchema = z.string().min(1).max(200).regex(/^[A-Za-z0-9_.:-]+$/, "yalnızca harf, rakam ve _ . : -");
7
7
  const IdSchema = z.uuid();
8
8
  const CurrencySchema = z.string().regex(/^[A-Z]{3}$/, "ISO-4217 kodu");
@@ -46,10 +46,174 @@ const ErrorResponseSchema = z.object({
46
46
  requestId: z.string().optional().catch(void 0)
47
47
  });
48
48
  //#endregion
49
+ //#region ../contract/src/emails.ts
50
+ const EMAIL_TEMPLATES = [
51
+ "subscription_started",
52
+ "payment_receipt",
53
+ "payment_failed",
54
+ "cancel_scheduled",
55
+ "subscription_ended",
56
+ "access_suspended",
57
+ "subscription_terminated",
58
+ "invoice_issued"
59
+ ];
60
+ const EmailTemplateSchema = z.enum(EMAIL_TEMPLATES);
61
+ /** Which event triggers which template. Events not listed send no email. */
62
+ const EMAIL_TEMPLATE_BY_EVENT = Object.freeze({
63
+ "subscription.activated": "subscription_started",
64
+ "subscription.renewed": "payment_receipt",
65
+ "subscription.payment_failed": "payment_failed",
66
+ "subscription.cancel_scheduled": "cancel_scheduled",
67
+ "subscription.canceled": "subscription_ended",
68
+ "subscription.expired": "subscription_ended",
69
+ "subscription.suspended": "access_suspended",
70
+ "subscription.terminated": "subscription_terminated",
71
+ "invoice.issued": "invoice_issued"
72
+ });
73
+ function emailTemplateOf(type) {
74
+ return EMAIL_TEMPLATE_BY_EVENT[type] ?? null;
75
+ }
76
+ const EMAIL_LOCALES = ["tr", "en"];
77
+ /** Upper bound of product-provided recipients per account. */
78
+ const MAX_NOTIFICATION_EMAILS = 10;
79
+ const NotificationEmailsSchema = z.array(z.email().max(254)).max(10).transform((emails) => uniqueEmails(emails));
80
+ /** Case-insensitive de-duplication; keeps the first spelling and the order. */
81
+ function uniqueEmails(emails) {
82
+ const seen = /* @__PURE__ */ new Set();
83
+ const out = [];
84
+ for (const email of emails) {
85
+ const key = email.trim().toLowerCase();
86
+ if (key === "" || seen.has(key)) continue;
87
+ seen.add(key);
88
+ out.push(email.trim());
89
+ }
90
+ return out;
91
+ }
92
+ /** Profile email first, then the account's notification emails. */
93
+ function emailRecipients(profileEmail, notificationEmails) {
94
+ return uniqueEmails([...profileEmail ? [profileEmail] : [], ...notificationEmails]);
95
+ }
96
+ function emailLocaleOf(accountLocale, lastCheckoutLocale, defaultLocale) {
97
+ const known = (value) => EMAIL_LOCALES.includes(value ?? "");
98
+ if (known(accountLocale)) return accountLocale;
99
+ if (known(lastCheckoutLocale)) return lastCheckoutLocale;
100
+ return defaultLocale;
101
+ }
102
+ const ExtraTextValueSchema = z.string().trim().min(1).max(1e3);
103
+ const EmailExtraTextSchema = z.object({
104
+ tr: ExtraTextValueSchema.optional(),
105
+ en: ExtraTextValueSchema.optional()
106
+ });
107
+ const EmailTemplateSettingsSchema = z.object({
108
+ /** false: steward does not send this template (the product sends its own). */
109
+ enabled: z.boolean().default(true),
110
+ /** Plain-text paragraph appended to the template body, per locale. */
111
+ extraText: EmailExtraTextSchema.optional()
112
+ });
113
+ const EmailTemplatesSchema = z.object(Object.fromEntries(EMAIL_TEMPLATES.map((t) => [t, EmailTemplateSettingsSchema.optional()])));
114
+ const EmailSenderSchema = z.object({
115
+ /** Display name; defaults to `branding.name`. */
116
+ name: z.string().trim().min(1).max(100).optional(),
117
+ /** Sender address; defaults to the deployment's `STEWARD_EMAIL_FROM`. Its domain needs SPF/DKIM. */
118
+ address: z.email().max(254).optional()
119
+ });
120
+ /** Placeholders a billing path may use. */
121
+ const BILLING_PATH_PLACEHOLDERS = ["{LOCALE}", "{ACCOUNT_REF}"];
122
+ const BillingPathSchema = z.string().max(500).regex(/^\/(?!\/)[^\s\\]*$/, "an absolute path under appUrl (starts with a single /)").refine((path) => (path.match(/\{[^}]*\}/g) ?? []).every((p) => BILLING_PATH_PLACEHOLDERS.includes(p)), "only {LOCALE} and {ACCOUNT_REF} placeholders");
123
+ const EmailSettingsSchema = z.object({
124
+ from: EmailSenderSchema.optional(),
125
+ /** Reply-To; defaults to `branding.supportEmail`. */
126
+ replyTo: z.email().max(254).optional(),
127
+ /** Product billing page under `appUrl`, e.g. `/{LOCALE}/app/orgs/{ACCOUNT_REF}/billing`; without it buttons open `appUrl`. */
128
+ billingPath: BillingPathSchema.optional(),
129
+ templates: EmailTemplatesSchema.default({})
130
+ });
131
+ const EmailSettingsPatchSchema = z.object({
132
+ from: EmailSenderSchema.strict().nullable().optional(),
133
+ replyTo: z.email().max(254).nullable().optional(),
134
+ billingPath: BillingPathSchema.nullable().optional(),
135
+ templates: z.object(Object.fromEntries(EMAIL_TEMPLATES.map((t) => [t, z.object({
136
+ enabled: z.boolean().optional(),
137
+ extraText: EmailExtraTextSchema.strict().optional()
138
+ }).strict().nullable().optional()]))).strict().optional()
139
+ }).strict();
140
+ /** Pure merge of a validated patch (see the patch rules above). */
141
+ function applyEmailSettingsPatch(current, patch) {
142
+ if (patch === null) return EmailSettingsSchema.parse({});
143
+ const next = {
144
+ ...current,
145
+ templates: { ...current.templates }
146
+ };
147
+ for (const [key, value] of Object.entries(patch)) {
148
+ if (value === void 0) continue;
149
+ if (key === "templates") {
150
+ const templates = next.templates;
151
+ for (const [template, settings] of Object.entries(value)) {
152
+ if (settings === void 0) continue;
153
+ if (settings === null) delete templates[template];
154
+ else templates[template] = settings;
155
+ }
156
+ } else if (value === null) delete next[key];
157
+ else next[key] = value;
158
+ }
159
+ return EmailSettingsSchema.parse(next);
160
+ }
161
+ function emailTemplateSettings(settings, template) {
162
+ return settings.templates[template] ?? { enabled: true };
163
+ }
164
+ /**
165
+ * Button target: `appUrl` + `billingPath` with placeholders filled (the ref is URL-encoded).
166
+ * Null without `appUrl`, or when the result would leave the `appUrl` origin.
167
+ */
168
+ function billingPageUrl(appUrl, billingPath, locale, accountRef) {
169
+ if (appUrl === void 0) return null;
170
+ let base;
171
+ try {
172
+ base = new URL(appUrl);
173
+ } catch {
174
+ return null;
175
+ }
176
+ if (billingPath === void 0) return base.toString();
177
+ const path = billingPath.replaceAll("{LOCALE}", locale).replaceAll("{ACCOUNT_REF}", encodeURIComponent(accountRef));
178
+ try {
179
+ const url = new URL(path, base.origin);
180
+ return url.origin === base.origin ? url.toString() : null;
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+ /**
186
+ * Whether the email still describes the account at send time. Receipts, the welcome email
187
+ * (legal confirmation) and the final termination notice always go; the others only while
188
+ * the situation they describe still holds.
189
+ */
190
+ function isEmailRelevant(template, facts) {
191
+ const live = facts.state.subscription;
192
+ const same = live !== null && live.id === facts.subscriptionId;
193
+ switch (template) {
194
+ case "subscription_started":
195
+ case "payment_receipt":
196
+ case "subscription_terminated": return true;
197
+ case "payment_failed": return same && live.status === "past_due";
198
+ case "cancel_scheduled": return same && live.cancelAtPeriodEnd;
199
+ case "subscription_ended": return live === null;
200
+ case "access_suspended": return same && live.status === "suspended";
201
+ case "invoice_issued": return facts.invoiceStatus === "issued";
202
+ }
203
+ }
204
+ //#endregion
49
205
  //#region ../contract/src/accounts.ts
206
+ const AccountLocaleSchema = z.enum(EMAIL_LOCALES);
50
207
  const AccountUpsertInputSchema = z.object({
51
208
  displayName: z.string().min(1).max(200),
52
- metadata: MetadataSchema.default({})
209
+ metadata: MetadataSchema.default({}),
210
+ /**
211
+ * Extra recipients of billing emails besides the billing profile email (e.g. the
212
+ * organization owners). Omitted: unchanged (older clients keep working); `[]` clears.
213
+ */
214
+ notificationEmails: NotificationEmailsSchema.optional(),
215
+ /** Language of billing emails. Omitted: unchanged; `null`: latest checkout's, else the product default. */
216
+ locale: AccountLocaleSchema.nullable().optional()
53
217
  });
54
218
  const BillingProfileKindSchema = z.enum(["individual", "company"]);
55
219
  const BillingProfileSchema = z.object({
@@ -162,7 +326,11 @@ const AccountSchema = z.object({
162
326
  displayName: z.string(),
163
327
  metadata: MetadataSchema,
164
328
  createdAt: DateTimeSchema,
165
- snapshot: EntitlementSnapshotSchema
329
+ snapshot: EntitlementSnapshotSchema,
330
+ /** Extra billing email recipients (older steward: absent → `[]`). */
331
+ notificationEmails: z.array(z.string()).default([]),
332
+ /** Billing email language set by the product; null: not set. */
333
+ locale: AccountLocaleSchema.nullable().default(null)
166
334
  });
167
335
  const AccountDeleteResponseSchema = z.object({
168
336
  ref: ExternalRefSchema,
@@ -347,4 +515,4 @@ const PingSchema = z.object({
347
515
  endpointId: IdSchema
348
516
  });
349
517
  //#endregion
350
- export { CodeSchema as A, IntervalSchema as B, BillingProfileSchema as C, API_VERSION as D, SubscriptionSummarySchema as E, EntitlementsSchema as F, ErrorCategorySchema as I, ErrorResponseSchema as L, DateTimeSchema as M, ERROR_CATEGORIES as N, ActorRefSchema as O, EntitlementValueSchema as P, ExternalRefSchema as R, BillingProfileKindSchema as S, SubscriptionStatusSchema as T, MetadataSchema as V, AccountScheduledChangeSchema as _, EVENT_TYPES as a, AccountSubscriptionStateSchema as b, PING_EVENT_TYPE as c, ACCOUNT_INCLUDES as d, AccessStateSchema as f, AccountProfileStateSchema as g, AccountOpenCheckoutSchema as h, EVENT_DETAIL_SCHEMAS as i, CurrencySchema as j, AmountMinorSchema as k, PingSchema as l, AccountDunningStateSchema as m, ApiVersionSchema as n, EventIdSchema as o, AccountDeleteResponseSchema as p, BillingEventSchema as r, LEGACY_EVENT_TYPES as s, AnyBillingEventSchema as t, isEventType as u, AccountSchema as v, EntitlementSnapshotSchema as w, AccountUpsertInputSchema as x, AccountStateSchema as y, IdSchema as z };
518
+ export { CurrencySchema as $, EMAIL_LOCALES as A, NotificationEmailsSchema as B, BillingProfileKindSchema as C, SubscriptionSummarySchema as D, SubscriptionStatusSchema as E, EmailSettingsPatchSchema as F, emailTemplateOf as G, billingPageUrl as H, EmailSettingsSchema as I, uniqueEmails as J, emailTemplateSettings as K, EmailTemplateSchema as L, EMAIL_TEMPLATE_BY_EVENT as M, EmailExtraTextSchema as N, BILLING_PATH_PLACEHOLDERS as O, EmailSenderSchema as P, CodeSchema as Q, EmailTemplateSettingsSchema as R, AccountUpsertInputSchema as S, EntitlementSnapshotSchema as T, emailLocaleOf as U, applyEmailSettingsPatch as V, emailRecipients as W, ActorRefSchema as X, API_VERSION as Y, AmountMinorSchema as Z, AccountProfileStateSchema as _, EVENT_TYPES as a, ErrorResponseSchema as at, AccountStateSchema as b, PING_EVENT_TYPE as c, IntervalSchema as ct, ACCOUNT_INCLUDES as d, DateTimeSchema as et, AccessStateSchema as f, AccountOpenCheckoutSchema as g, AccountLocaleSchema as h, EVENT_DETAIL_SCHEMAS as i, ErrorCategorySchema as it, EMAIL_TEMPLATES as j, BillingPathSchema as k, PingSchema as l, MetadataSchema as lt, AccountDunningStateSchema as m, ApiVersionSchema as n, EntitlementValueSchema as nt, EventIdSchema as o, ExternalRefSchema as ot, AccountDeleteResponseSchema as p, isEmailRelevant as q, BillingEventSchema as r, EntitlementsSchema as rt, LEGACY_EVENT_TYPES as s, IdSchema as st, AnyBillingEventSchema as t, ERROR_CATEGORIES as tt, isEventType as u, AccountScheduledChangeSchema as v, BillingProfileSchema as w, AccountSubscriptionStateSchema as x, AccountSchema as y, MAX_NOTIFICATION_EMAILS as z };