@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 +27 -33
- package/dist/_chunks/errors.js +1 -1
- package/dist/_chunks/events.d.ts +15 -1
- package/dist/_chunks/events.js +172 -4
- package/dist/_chunks/index.d.ts +357 -3631
- package/dist/_chunks/src.js +85 -157
- package/dist/_chunks/steward.d.ts +5 -2404
- package/dist/_chunks/steward.js +12 -26
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +3 -3
- package/dist/index.d.ts +4 -66
- package/dist/index.js +4 -267
- package/dist/server.d.ts +6 -6
- package/dist/server.js +13 -2
- package/dist/testing.d.ts +60 -33
- package/dist/testing.js +70 -145
- package/package.json +4 -5
- package/dist/_chunks/locale.js +0 -17
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 +
|
|
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
|
|
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. `
|
|
159
|
-
|
|
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}`), `
|
|
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`).
|
|
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
|
|
307
|
+
Hatalar — ürün API'sinin durum kodu:
|
|
321
308
|
|
|
322
309
|
```ts
|
|
323
|
-
import { httpStatusFor,
|
|
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
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
## `@
|
|
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`
|
|
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
|
|
431
|
+
(`.github/workflows/sdk-publish.yml`).
|
package/dist/_chunks/errors.js
CHANGED
package/dist/_chunks/events.d.ts
CHANGED
|
@@ -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 {
|
|
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 };
|
package/dist/_chunks/events.js
CHANGED
|
@@ -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
|
-
/**
|
|
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 {
|
|
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 };
|