@stewardhq/sdk 0.2.0 → 0.5.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 (39) hide show
  1. package/README.md +123 -88
  2. package/dist/_chunks/errors.js +1 -1
  3. package/dist/_chunks/events.d.ts +315 -4
  4. package/dist/_chunks/events.js +1155 -9
  5. package/dist/_chunks/index.d.ts +3473 -90
  6. package/dist/_chunks/locale.d.ts +269 -1
  7. package/dist/_chunks/src.js +842 -24
  8. package/dist/_chunks/validators.d.ts +45 -1
  9. package/dist/_chunks/validators.js +128 -27
  10. package/dist/contract.d.ts +4 -4
  11. package/dist/contract.js +4 -4
  12. package/dist/index.d.ts +436 -22
  13. package/dist/index.js +1659 -26
  14. package/dist/server.d.ts +44 -6
  15. package/dist/server.js +29 -2
  16. package/package.json +4 -10
  17. package/dist/_chunks/steward.d.ts +0 -189
  18. package/dist/_chunks/steward.js +0 -560
  19. package/dist/testing/fixtures/events/LOCK.json +0 -27
  20. package/dist/testing/fixtures/events/account.deleted.json +0 -36
  21. package/dist/testing/fixtures/events/account.state_changed.json +0 -53
  22. package/dist/testing/fixtures/events/account.updated.json +0 -37
  23. package/dist/testing/fixtures/events/checkout.completed.json +0 -36
  24. package/dist/testing/fixtures/events/checkout.expired.json +0 -26
  25. package/dist/testing/fixtures/events/checkout.failed.json +0 -27
  26. package/dist/testing/fixtures/events/invoice.created.json +0 -37
  27. package/dist/testing/fixtures/events/invoice.issued.json +0 -38
  28. package/dist/testing/fixtures/events/invoice.voided.json +0 -39
  29. package/dist/testing/fixtures/events/subscription.activated.json +0 -40
  30. package/dist/testing/fixtures/events/subscription.cancel_scheduled.json +0 -38
  31. package/dist/testing/fixtures/events/subscription.canceled.json +0 -29
  32. package/dist/testing/fixtures/events/subscription.expired.json +0 -27
  33. package/dist/testing/fixtures/events/subscription.payment_failed.json +0 -38
  34. package/dist/testing/fixtures/events/subscription.reactivated.json +0 -36
  35. package/dist/testing/fixtures/events/subscription.renewed.json +0 -39
  36. package/dist/testing/fixtures/events/subscription.suspended.json +0 -36
  37. package/dist/testing/fixtures/events/subscription.terminated.json +0 -28
  38. package/dist/testing.d.ts +0 -626
  39. package/dist/testing.js +0 -3638
@@ -23,6 +23,31 @@ const EntitlementValueSchema = z.union([
23
23
  ]);
24
24
  const EntitlementsSchema = z.record(CodeSchema, EntitlementValueSchema);
25
25
  const MetadataSchema = z.record(z.string().max(64), z.string().max(500));
26
+ /**
27
+ * Why an invoice was written (Polar `billing_reason`): `subscription_create` first period,
28
+ * `subscription_cycle` renewal (phase 2 adds overage lines to it), `subscription_update` plan
29
+ * change, `purchase` reserved (one-off purchases are deferred), `manual` staff-written.
30
+ */
31
+ const INVOICE_BILLING_REASONS = [
32
+ "subscription_create",
33
+ "subscription_cycle",
34
+ "subscription_update",
35
+ "purchase",
36
+ "manual"
37
+ ];
38
+ const InvoiceBillingReasonSchema = z.enum(INVOICE_BILLING_REASONS);
39
+ /**
40
+ * Collection state of an invoice written before payment: `unpaid` (open, has `dueAt`), `paid`,
41
+ * `uncollectible` (given up after dunning), `waived` (operator forgave it). Invoices written the
42
+ * pre-billing-core way (after a payment, manual invoices) carry no payment status.
43
+ */
44
+ const INVOICE_PAYMENT_STATUSES = [
45
+ "unpaid",
46
+ "paid",
47
+ "uncollectible",
48
+ "waived"
49
+ ];
50
+ const InvoicePaymentStatusSchema = z.enum(INVOICE_PAYMENT_STATUSES);
26
51
  const ERROR_CATEGORIES = [
27
52
  "validation",
28
53
  "not_found",
@@ -46,10 +71,1011 @@ const ErrorResponseSchema = z.object({
46
71
  requestId: z.string().optional().catch(void 0)
47
72
  });
48
73
  //#endregion
74
+ //#region ../contract/src/ingest.ts
75
+ /** Most events in one ingest batch (M11). */
76
+ const EVENTS_INGEST_MAX_EVENTS = 1e3;
77
+ /** Largest ingest request body in bytes (M11; enforced on the route, not by the schema). */
78
+ const EVENTS_INGEST_MAX_BYTES = 1048576;
79
+ const EVENT_NAME_MAX_LENGTH = 128;
80
+ const EVENT_METADATA_MAX_PAIRS = 50;
81
+ const EVENT_METADATA_KEY_MAX_LENGTH = 40;
82
+ const EVENT_METADATA_STRING_MAX_LENGTH = 500;
83
+ /** How far back an event `timestamp` may lie, relative to its receipt (M18). */
84
+ const EVENT_TIMESTAMP_MAX_AGE_DAYS = 35;
85
+ /** How far ahead an event `timestamp` may lie, relative to its receipt (clock skew, M18). */
86
+ const EVENT_TIMESTAMP_MAX_SKEW_MINUTES = 5;
87
+ /** Whether a string contains C0 control characters or DEL (kept out of names and keys shown in logs and doctor). */
88
+ function hasControlCharacters(value) {
89
+ for (let i = 0; i < value.length; i++) {
90
+ const c = value.charCodeAt(i);
91
+ if (c < 32 || c === 127) return true;
92
+ }
93
+ return false;
94
+ }
95
+ const noControlCharacters = (value) => !hasControlCharacters(value);
96
+ /** Event name (free text, e.g. `ai.completion`); meters select events by it. */
97
+ const EventNameSchema = z.string().min(1).max(128).refine(noControlCharacters, "no control characters");
98
+ /** Product-chosen dedup key of an event, unique within the product (`cmpl_<id>`, `req_<id>`; M10). */
99
+ const EventExternalIdSchema = z.string().regex(/^[A-Za-z0-9_.:-]{8,200}$/, "8–200 characters: letters, digits and _ . : -");
100
+ /** Metadata key; a meter filter reads it as `metadata.<key>`. `__proto__` is reserved. */
101
+ const EventMetadataKeySchema = z.string().min(1).max(40).refine(noControlCharacters, "no control characters").refine((key) => key !== "__proto__", "reserved key");
102
+ const EventMetadataValueSchema = z.union([
103
+ z.string().max(500),
104
+ z.number(),
105
+ z.boolean()
106
+ ]);
107
+ const EventMetadataSchema = z.record(EventMetadataKeySchema, EventMetadataValueSchema).refine((metadata) => Object.keys(metadata).length <= 50, `at most 50 metadata pairs`);
108
+ /** One event the product sends (product → steward; not a webhook `BillingEvent`). */
109
+ const IngestEventSchema = z.object({
110
+ /** The account's `externalRef`; the account must exist (`accounts.upsert` first, M12). */
111
+ accountRef: ExternalRefSchema,
112
+ name: EventNameSchema,
113
+ externalId: EventExternalIdSchema,
114
+ /** When it happened; default: when steward received it. Assigns the event to a meter period (M18). */
115
+ timestamp: DateTimeSchema.optional(),
116
+ metadata: EventMetadataSchema.optional()
117
+ });
118
+ /** `POST /v1/events/ingest` body. */
119
+ const EventsIngestInputSchema = z.object({ events: z.array(IngestEventSchema).min(1).max(EVENTS_INGEST_MAX_EVENTS) });
120
+ /** Per-event rejection codes this version knows (M12). The result schema accepts others too. */
121
+ const INGEST_REJECT_CODES = [
122
+ "account_not_found",
123
+ "account_deleted",
124
+ "timestamp_out_of_range"
125
+ ];
126
+ const IngestRejectCodeSchema = z.enum(INGEST_REJECT_CODES);
127
+ /**
128
+ * Rejection code in a result: a known `IngestRejectCode` or a code a newer steward added (open
129
+ * set, so an older client never fails a whole ingest result on it).
130
+ */
131
+ const RejectCodeReadSchema = z.string().min(1).max(100);
132
+ const IngestRejectionSchema = z.object({
133
+ /** Position of the event in the request's `events`. */
134
+ index: z.number().int().nonnegative(),
135
+ externalId: z.string(),
136
+ code: RejectCodeReadSchema
137
+ });
138
+ /**
139
+ * `POST /v1/events/ingest` result (200): `inserted` new events, `duplicates` already stored
140
+ * (`externalId` seen before), `rejected` per event (sorted by `index`; the SDK does not retry them).
141
+ */
142
+ const EventsIngestResultSchema = z.object({
143
+ inserted: z.number().int().nonnegative(),
144
+ duplicates: z.number().int().nonnegative(),
145
+ rejected: z.array(IngestRejectionSchema)
146
+ });
147
+ const DAY_MS = 864e5;
148
+ const MINUTE_MS = 6e4;
149
+ /**
150
+ * Whether an event `timestamp` is accepted for an event received at `receivedAt` (M18):
151
+ * [received − 35 days, received + 5 minutes], both ends inclusive. Outside: `timestamp_out_of_range`.
152
+ */
153
+ function isEventTimestampInRange(timestamp, receivedAt) {
154
+ const at = timestamp.getTime();
155
+ const received = receivedAt.getTime();
156
+ if (Number.isNaN(at) || Number.isNaN(received)) return false;
157
+ return at >= received - 35 * DAY_MS && at <= received + 5 * MINUTE_MS;
158
+ }
159
+ //#endregion
160
+ //#region ../contract/src/micro-units.ts
161
+ /** Decimal places of a meter unit value (10⁻⁶). */
162
+ const UNIT_DECIMALS = 6;
163
+ /** Micro units in one unit. */
164
+ const MICRO_UNITS_PER_UNIT = 1000000n;
165
+ /** A unit value that is not a finite, non-negative decimal (M4 `invalid_value`). */
166
+ var InvalidUnitValueError = class extends RangeError {
167
+ code = "invalid_value";
168
+ constructor(value) {
169
+ super(`invalid unit value: ${typeof value === "string" ? JSON.stringify(value.slice(0, 50)) : String(value)}`);
170
+ this.name = "InvalidUnitValueError";
171
+ }
172
+ };
173
+ /** Decimal text: optional minus, digits, optional fraction, optional exponent (`1e-7`, `1.5e+21`). */
174
+ const DECIMAL_TEXT = /^(-)?(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
175
+ /** Longest decimal string accepted (a `numeric(30,6)` text is 32 characters). */
176
+ const MAX_TEXT_LENGTH = 400;
177
+ /** Exponents beyond this are outside the double range (a JS number never reaches them). */
178
+ const MAX_EXPONENT = 400;
179
+ function decimalParts(text) {
180
+ if (text.length > MAX_TEXT_LENGTH) return null;
181
+ const m = DECIMAL_TEXT.exec(text);
182
+ if (!m) return null;
183
+ const exponent = m[4] === void 0 ? 0 : Number(m[4]);
184
+ if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > MAX_EXPONENT) return null;
185
+ const fraction = m[3] ?? "";
186
+ return {
187
+ negative: m[1] === "-",
188
+ digits: BigInt(`${m[2]}${fraction}`),
189
+ scale: fraction.length - exponent
190
+ };
191
+ }
192
+ function numberParts(value) {
193
+ return Number.isFinite(value) ? decimalParts(String(value)) : null;
194
+ }
195
+ const pow10 = (n) => 10n ** BigInt(n);
196
+ /** `digits × 10^(6 - scale)`, rounded half up when digits are dropped. */
197
+ function microOf(parts) {
198
+ const shift = 6 - parts.scale;
199
+ if (shift >= 0) return parts.digits * pow10(shift);
200
+ const divisor = pow10(-shift);
201
+ const quotient = parts.digits / divisor;
202
+ return 2n * (parts.digits % divisor) >= divisor ? quotient + 1n : quotient;
203
+ }
204
+ /**
205
+ * Micro units (10⁻⁶) of a finite, non-negative decimal, or `null` when the value is not one.
206
+ * A number is read from its canonical decimal text (`String(value)`); a string must be a plain
207
+ * decimal (`"12"`, `"0.25"`, `"1e-7"`, Postgres `numeric` text). Beyond 6 decimals: half up.
208
+ */
209
+ function parseMicroUnits(value) {
210
+ const parts = typeof value === "number" ? numberParts(value) : typeof value === "string" ? decimalParts(value) : null;
211
+ if (parts === null) return null;
212
+ if (parts.negative && parts.digits !== 0n) return null;
213
+ return microOf(parts);
214
+ }
215
+ /**
216
+ * Micro units (10⁻⁶) of a finite, non-negative decimal: `toMicroUnits(0.25) === 250000n`,
217
+ * `toMicroUnits("123.4567895") === 123456790n` (half up), `toMicroUnits(1e-7) === 0n`.
218
+ * Throws `InvalidUnitValueError` (`code: "invalid_value"`) for a negative, non-finite or
219
+ * non-decimal value.
220
+ */
221
+ function toMicroUnits(value) {
222
+ const micro = parseMicroUnits(value);
223
+ if (micro === null) throw new InvalidUnitValueError(value);
224
+ return micro;
225
+ }
226
+ /**
227
+ * Canonical decimal text of micro units: no exponent, no leading zeros, no trailing fraction zeros
228
+ * (`1500000000n` → `"1500"`, `250000n` → `"0.25"`, `-1500000n` → `"-1.5"`). Invoice line `units`.
229
+ */
230
+ function microToDecimalString(micro) {
231
+ const negative = micro < 0n;
232
+ const abs = negative ? -micro : micro;
233
+ const whole = (abs / MICRO_UNITS_PER_UNIT).toString();
234
+ const fraction = (abs % MICRO_UNITS_PER_UNIT).toString().padStart(6, "0").replace(/0+$/, "");
235
+ return `${negative ? "-" : ""}${whole}${fraction === "" ? "" : `.${fraction}`}`;
236
+ }
237
+ /**
238
+ * Micro units as a JSON number (API responses): the double nearest to the exact decimal, so any
239
+ * value up to 15 significant digits round-trips (`toMicroUnits(fromMicroUnits(x)) === x`).
240
+ */
241
+ function fromMicroUnits(micro) {
242
+ return Number(microToDecimalString(micro));
243
+ }
244
+ /**
245
+ * Plain decimal text of a finite number, without exponent: `1e21` → `"1000000000000000000000"`,
246
+ * `1e-7` → `"0.0000001"`, `-0` → `"0"`. Throws for a non-finite number.
247
+ */
248
+ function plainDecimalOf(value) {
249
+ const parts = numberParts(value);
250
+ if (parts === null) throw new InvalidUnitValueError(value);
251
+ if (parts.digits === 0n) return "0";
252
+ const sign = parts.negative ? "-" : "";
253
+ if (parts.scale <= 0) return `${sign}${(parts.digits * pow10(-parts.scale)).toString()}`;
254
+ const text = parts.digits.toString().padStart(parts.scale + 1, "0");
255
+ const whole = text.slice(0, text.length - parts.scale).replace(/^0+(?=\d)/, "");
256
+ const fraction = text.slice(text.length - parts.scale).replace(/0+$/, "");
257
+ return `${sign}${whole}${fraction === "" ? "" : `.${fraction}`}`;
258
+ }
259
+ /**
260
+ * Whether a number is a finite value with at most 6 decimals, i.e. exactly representable in micro
261
+ * units (catalog credits, credit grants and `lowAt` must be; event values are rounded instead).
262
+ */
263
+ function isMicroPrecise(value) {
264
+ const parts = numberParts(value);
265
+ if (parts === null) return false;
266
+ if (parts.scale <= 6) return true;
267
+ return parts.digits % pow10(parts.scale - 6) === 0n;
268
+ }
269
+ //#endregion
270
+ //#region ../contract/src/meters.ts
271
+ const METER_FILTER_OPERATORS = [
272
+ "eq",
273
+ "ne",
274
+ "gt",
275
+ "gte",
276
+ "lt",
277
+ "lte",
278
+ "like",
279
+ "not_like"
280
+ ];
281
+ /** Deepest nesting of `and`/`or` groups. */
282
+ const METER_FILTER_MAX_DEPTH = 3;
283
+ /** Most conditions in one filter. */
284
+ const METER_FILTER_MAX_CONDITIONS = 20;
285
+ const METADATA_PREFIX = "metadata.";
286
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
287
+ const isOperator = (value) => METER_FILTER_OPERATORS.includes(value);
288
+ function valueIssue(property, operator, value) {
289
+ const isName = property === "name";
290
+ if (isName && ![
291
+ "eq",
292
+ "ne",
293
+ "like",
294
+ "not_like"
295
+ ].includes(operator)) return `operator ${operator} does not apply to name`;
296
+ switch (operator) {
297
+ case "eq":
298
+ case "ne":
299
+ if (isName) return typeof value === "string" && value.length <= 128 ? null : `name ${operator} needs a string of at most 128 characters`;
300
+ if (typeof value === "string") return value.length <= 500 ? null : `at most 500 characters`;
301
+ if (typeof value === "number") return Number.isFinite(value) ? null : "a finite number";
302
+ return typeof value === "boolean" ? null : `${operator} needs a string, number or boolean`;
303
+ case "gt":
304
+ case "gte":
305
+ case "lt":
306
+ case "lte": return typeof value === "number" && Number.isFinite(value) ? null : `${operator} needs a finite number`;
307
+ case "like":
308
+ case "not_like": return typeof value === "string" && value.length >= 1 && value.length <= 500 ? null : `${operator} needs a non-empty string of at most 500 characters`;
309
+ }
310
+ }
311
+ function propertyIssue(property) {
312
+ if (property === "name") return null;
313
+ if (!property.startsWith(METADATA_PREFIX)) return `unknown property ${JSON.stringify(property.slice(0, 50))}: use name or metadata.<key>`;
314
+ return EventMetadataKeySchema.safeParse(property.slice(9)).success ? null : `invalid metadata key in ${JSON.stringify(property.slice(0, 60))}`;
315
+ }
316
+ /**
317
+ * The explicit form of a condition, or `null` when it is not a valid condition (the schema reports
318
+ * why). `{name: "x"}` → `{property: "name", operator: "eq", value: "x"}`.
319
+ */
320
+ function meterFilterClauseOf(condition) {
321
+ if (!isPlainObject(condition)) return null;
322
+ const keys = Object.keys(condition);
323
+ if (keys.length !== 1) return null;
324
+ const property = keys[0];
325
+ if (property === "and" || property === "or" || propertyIssue(property) !== null) return null;
326
+ const raw = condition[property];
327
+ let operator = "eq";
328
+ let value = raw;
329
+ if (isPlainObject(raw)) {
330
+ const ops = Object.keys(raw);
331
+ if (ops.length !== 1 || !isOperator(ops[0])) return null;
332
+ operator = ops[0];
333
+ value = raw[operator];
334
+ }
335
+ if (valueIssue(property, operator, value) !== null) return null;
336
+ return {
337
+ property,
338
+ operator,
339
+ value
340
+ };
341
+ }
342
+ /** Structural issues of a filter (empty when valid). */
343
+ function meterFilterIssues(filter) {
344
+ const issues = [];
345
+ let conditions = 0;
346
+ let deepest = 0;
347
+ const visit = (node, path, depth) => {
348
+ if (!isPlainObject(node)) {
349
+ issues.push({
350
+ path,
351
+ message: "a filter is an object: a condition or an and/or group"
352
+ });
353
+ return;
354
+ }
355
+ const keys = Object.keys(node);
356
+ if (keys.includes("and") || keys.includes("or")) {
357
+ if (keys.length !== 1) {
358
+ issues.push({
359
+ path,
360
+ message: "a group has exactly one key: and or or"
361
+ });
362
+ return;
363
+ }
364
+ const key = keys[0];
365
+ const members = node[key];
366
+ deepest = Math.max(deepest, depth + 1);
367
+ if (!Array.isArray(members) || members.length === 0) {
368
+ issues.push({
369
+ path: [...path, key],
370
+ message: `${key} needs a non-empty array of filters`
371
+ });
372
+ return;
373
+ }
374
+ if (depth + 1 > 3) return;
375
+ members.forEach((member, i) => visit(member, [
376
+ ...path,
377
+ key,
378
+ i
379
+ ], depth + 1));
380
+ return;
381
+ }
382
+ conditions += 1;
383
+ if (keys.length !== 1) {
384
+ issues.push({
385
+ path,
386
+ message: "a condition has exactly one property (combine conditions with and)"
387
+ });
388
+ return;
389
+ }
390
+ const property = keys[0];
391
+ const problem = propertyIssue(property);
392
+ if (problem !== null) {
393
+ issues.push({
394
+ path: [...path, property],
395
+ message: problem
396
+ });
397
+ return;
398
+ }
399
+ const raw = node[property];
400
+ if (isPlainObject(raw)) {
401
+ const ops = Object.keys(raw);
402
+ if (ops.length !== 1 || !isOperator(ops[0])) {
403
+ issues.push({
404
+ path: [...path, property],
405
+ message: `exactly one operator of ${METER_FILTER_OPERATORS.join(", ")}`
406
+ });
407
+ return;
408
+ }
409
+ const issue = valueIssue(property, ops[0], raw[ops[0]]);
410
+ if (issue !== null) issues.push({
411
+ path: [
412
+ ...path,
413
+ property,
414
+ ops[0]
415
+ ],
416
+ message: issue
417
+ });
418
+ return;
419
+ }
420
+ const issue = valueIssue(property, "eq", raw);
421
+ if (issue !== null) issues.push({
422
+ path: [...path, property],
423
+ message: issue
424
+ });
425
+ };
426
+ visit(filter, [], 0);
427
+ if (deepest > 3) issues.push({
428
+ path: [],
429
+ message: `and/or groups nest at most 3 deep`
430
+ });
431
+ if (conditions > 20) issues.push({
432
+ path: [],
433
+ message: `at most 20 conditions`
434
+ });
435
+ return issues;
436
+ }
437
+ /** Meter filter (see the syntax above). Validates structure; the value is kept as given. */
438
+ const MeterFilterSchema = z.custom().superRefine((value, ctx) => {
439
+ for (const issue of meterFilterIssues(value)) ctx.addIssue({
440
+ code: "custom",
441
+ path: issue.path,
442
+ message: issue.message
443
+ });
444
+ });
445
+ /**
446
+ * Canonical form of a valid filter: every condition explicit (`{name: "x"}` → `{name: {eq: "x"}}`),
447
+ * a fresh object. Two filters that differ only by the shorthand have the same canonical JSON
448
+ * (the lock check, M4, compares canonical filters). Throws on an invalid filter.
449
+ */
450
+ function canonicalMeterFilter(filter) {
451
+ if ("and" in filter && Array.isArray(filter.and)) return { and: filter.and.map(canonicalMeterFilter) };
452
+ if ("or" in filter && Array.isArray(filter.or)) return { or: filter.or.map(canonicalMeterFilter) };
453
+ const clause = meterFilterClauseOf(filter);
454
+ if (clause === null) throw new Error("invalid meter filter condition");
455
+ return { [clause.property]: { [clause.operator]: clause.value } };
456
+ }
457
+ /**
458
+ * What a meter computes over its matching events in a period (M4; no `min`/`avg`):
459
+ * `count` events; `sum` / `max` of the finite, non-negative number at `metadata.<key>` (decimals
460
+ * allowed); `unique` count of distinct values at `metadata.<key>`. An event without a usable
461
+ * value adds nothing to that meter (`invalid_value`, shown by doctor).
462
+ */
463
+ const METER_AGGREGATION_KINDS = [
464
+ "count",
465
+ "sum",
466
+ "max",
467
+ "unique"
468
+ ];
469
+ const MeterAggregationSchema = z.discriminatedUnion("kind", [
470
+ z.object({ kind: z.literal("count") }),
471
+ z.object({
472
+ kind: z.literal("sum"),
473
+ key: EventMetadataKeySchema
474
+ }),
475
+ z.object({
476
+ kind: z.literal("max"),
477
+ key: EventMetadataKeySchema
478
+ }),
479
+ z.object({
480
+ kind: z.literal("unique"),
481
+ key: EventMetadataKeySchema
482
+ })
483
+ ]);
484
+ /** Largest unit amount in a definition (credit, rollover cap, credit grant): exact as a JSON number. */
485
+ const MAX_METER_UNITS = 0x38d7ea4c68000;
486
+ /** Default `lowAt` (M14): standing becomes `low` at 80% of the credit. */
487
+ const DEFAULT_METER_LOW_AT = .8;
488
+ const microPrecise = (value) => isMicroPrecise(value);
489
+ /** A positive unit amount in a definition: at most 6 decimals (M4), at most `MAX_METER_UNITS`. */
490
+ const MeterUnitsSchema = z.number().positive().max(MAX_METER_UNITS).refine(microPrecise, "at most 6 decimal places");
491
+ const LabelTextSchema = z.string().trim().min(1).max(100).refine((value) => !hasControlCharacters(value), "no control characters");
492
+ /** Customer-facing text per locale (hosted checkout and portal). Both locales are required. */
493
+ const MeterLabelSchema = z.object({
494
+ tr: LabelTextSchema,
495
+ en: LabelTextSchema
496
+ });
497
+ /** Share of the credit at which standing becomes `low`: 0 < x ≤ 1, at most 6 decimals. */
498
+ const MeterLowAtSchema = z.number().gt(0).lte(1).refine(microPrecise, "at most 6 decimal places");
499
+ const MeterDefFieldsSchema = z.object({
500
+ /** Shorthand: only events with this name (`filter` `{name: {eq: event}}`); with `filter`, both must hold. */
501
+ event: EventNameSchema.optional(),
502
+ filter: MeterFilterSchema.optional(),
503
+ aggregation: MeterAggregationSchema,
504
+ label: MeterLabelSchema,
505
+ /** Unit name, e.g. `{tr: "token", en: "tokens"}`. */
506
+ unitLabel: MeterLabelSchema.optional(),
507
+ /** Display divisor on hosted pages, e.g. 1000 → "12.5K tokens" (display only). */
508
+ displayPer: z.number().int().min(1).max(1e9).optional(),
509
+ /** Standing `low` threshold (M14); default 0.8; `null` disables `low`. */
510
+ lowAt: MeterLowAtSchema.nullable().default(DEFAULT_METER_LOW_AT)
511
+ });
512
+ function combinedFilter(event, filter) {
513
+ const byName = { name: { eq: event ?? "" } };
514
+ if (filter === void 0) return byName;
515
+ const canonical = canonicalMeterFilter(filter);
516
+ if (event === void 0) return canonical;
517
+ return "and" in canonical && Array.isArray(canonical.and) ? { and: [byName, ...canonical.and] } : { and: [byName, canonical] };
518
+ }
519
+ /**
520
+ * Meter definition (catalog sync input; the record key is the meter code). Input takes `event`
521
+ * and/or `filter`; the output has only `filter`, canonical, `event` folded in (`{and: [{name:
522
+ * {eq: event}}, …filter]}`). Filter and aggregation lock once the meter's first period opened
523
+ * (`422 meter_locked`; M4): a change means a new code.
524
+ */
525
+ const MeterDefSchema = MeterDefFieldsSchema.superRefine((def, ctx) => {
526
+ if (def.event === void 0 && def.filter === void 0) {
527
+ ctx.addIssue({
528
+ code: "custom",
529
+ path: ["filter"],
530
+ message: "event or filter is required"
531
+ });
532
+ return;
533
+ }
534
+ if (def.event !== void 0 && def.filter !== void 0 && meterFilterIssues(def.filter).length === 0) for (const issue of meterFilterIssues(combinedFilter(def.event, def.filter))) ctx.addIssue({
535
+ code: "custom",
536
+ path: ["filter", ...issue.path],
537
+ message: `with event: ${issue.message}`
538
+ });
539
+ }).transform(({ event, filter, ...rest }) => ({
540
+ filter: combinedFilter(event, filter),
541
+ ...rest
542
+ }));
543
+ /**
544
+ * A meter as returned by `GET /v1/catalog` (active meters). Lenient for forward compatibility: a
545
+ * newer steward may use filter or aggregation forms this version does not define; narrow `filter`
546
+ * with `MeterFilterSchema` and `aggregation` with `MeterAggregationSchema`.
547
+ */
548
+ const CatalogMeterSchema = z.object({
549
+ filter: z.unknown(),
550
+ aggregation: z.looseObject({
551
+ kind: z.string().min(1).max(64),
552
+ key: z.string().optional()
553
+ }),
554
+ label: MeterLabelSchema,
555
+ unitLabel: MeterLabelSchema.optional(),
556
+ displayPer: z.number().int().positive().optional(),
557
+ lowAt: z.number().nullable()
558
+ });
559
+ /**
560
+ * `meter_credit` benefit properties (M5, M6): `units` granted at the start of every meter period
561
+ * (plan + add-ons for the same meter add up; the default plan's credit only when no other layer
562
+ * credits the meter — `meterCreditsOf`); `rolloverCapUnits` turns rollover on — the unused
563
+ * balance carries over up to the cap (absent: unused credit expires; debt never carries).
564
+ */
565
+ const MeterCreditPropertiesSchema = z.object({
566
+ meter: CodeSchema,
567
+ units: MeterUnitsSchema,
568
+ rolloverCapUnits: MeterUnitsSchema.optional()
569
+ });
570
+ /** Largest `perUnits` of a metered price. */
571
+ const MAX_METERED_PRICE_PER_UNITS = 0xe8d4a51000;
572
+ /**
573
+ * Metered price (catalog sync input; MF5, M20): overage above the credit is billed
574
+ * `amountMinor` per `perUnits` units ("₺12,50 / 1.000 token" = `{amountMinor: 1250, perUnits:
575
+ * 1000}`; an integer pair, no decimal unit price). Key `(planCode, meter, currency)`. Tax terms
576
+ * must equal the plan's base prices in that currency (`metered_price_tax_mismatch`). `capMinor`
577
+ * caps the overage amount per meter period. Mutable in place like base prices (M36); a
578
+ * subscription locks the rate it started with.
579
+ */
580
+ const MeteredPriceDefSchema = z.object({
581
+ planCode: CodeSchema,
582
+ meter: CodeSchema,
583
+ currency: CurrencySchema,
584
+ amountMinor: AmountMinorSchema,
585
+ perUnits: z.number().int().min(1).max(MAX_METERED_PRICE_PER_UNITS),
586
+ /** Same meaning as `PriceDef.taxInclusive`; must equal the plan's base price. */
587
+ taxInclusive: z.boolean(),
588
+ /** Basis points (20% = 2000); must equal the plan's base price. */
589
+ taxRateBps: z.number().int().min(0).max(1e4),
590
+ capMinor: AmountMinorSchema.optional()
591
+ });
592
+ /** Metered price as returned by `GET /v1/catalog` (active ones). */
593
+ const MeteredPriceSchema = MeteredPriceDefSchema.extend({
594
+ id: IdSchema,
595
+ active: z.boolean(),
596
+ capMinor: AmountMinorSchema.nullable().optional(),
597
+ createdAt: DateTimeSchema
598
+ });
599
+ /**
600
+ * Credit standing of a meter in its period (M14): `low` once consumption reaches `lowAt` × credit,
601
+ * `exhausted` once the balance is ≤ 0; a meter without credit is always `ok`. Standing describes
602
+ * the credit only: a billable meter (metered price) keeps working past `exhausted` (M15).
603
+ */
604
+ const METER_STANDINGS = [
605
+ "ok",
606
+ "low",
607
+ "exhausted"
608
+ ];
609
+ const MeterStandingSchema = z.enum(METER_STANDINGS);
610
+ /** Units in a response: a JSON number with at most 6 decimals (`fromMicroUnits`). */
611
+ const UnitsNumberSchema = z.number();
612
+ const NonNegativeUnitsNumberSchema = z.number().nonnegative();
613
+ /**
614
+ * Units as a decimal string (invoice lines, meter charges): non-negative, at most 6 decimals.
615
+ * Steward writes the canonical form (`microToDecimalString`: no exponent, no trailing zeros).
616
+ */
617
+ const DecimalUnitsStringSchema = z.string().regex(/^\d{1,24}(\.\d{1,6})?$/, "non-negative decimal with at most 6 decimals");
618
+ /** One meter of `GET /v1/accounts/:ref/meters`: the meter's OPEN period (periods are per meter, M7). */
619
+ const AccountMeterSchema = z.object({
620
+ periodStart: DateTimeSchema,
621
+ periodEnd: DateTimeSchema,
622
+ /**
623
+ * Units the account's `meter_credit` benefits grant per period (`meterCreditsOf`); `null` when
624
+ * none does — such a meter is not gated by credit (M15).
625
+ */
626
+ includedUnits: NonNegativeUnitsNumberSchema.nullable(),
627
+ consumedUnits: NonNegativeUnitsNumberSchema,
628
+ /** Credit of the period: cycle grants, rollover, plan-change top-ups, manual grants. */
629
+ creditedUnits: NonNegativeUnitsNumberSchema,
630
+ /** `creditedUnits − consumedUnits`; negative past the credit. */
631
+ balance: UnitsNumberSchema,
632
+ /** `max(0, consumedUnits − creditedUnits)`. */
633
+ overageUnits: NonNegativeUnitsNumberSchema,
634
+ standing: MeterStandingSchema,
635
+ /** A live subscription has a metered rate for this meter: overage is billed, the gate stays open (M15) until `capReached`. */
636
+ billable: z.boolean(),
637
+ /** Estimated overage amount of the open period (billable meters); otherwise null. */
638
+ amountDueMinor: AmountMinorSchema.nullable(),
639
+ currency: CurrencySchema.nullable(),
640
+ /**
641
+ * The period's priced overage reached the locked rate's `capMinor` (`overageCapReached`): further
642
+ * use is not billed, so the gate closes (M15). False when not billable or the rate has no cap.
643
+ * Absent from an older steward (read as false).
644
+ */
645
+ capReached: z.boolean().optional()
646
+ });
647
+ /**
648
+ * `GET /v1/accounts/:ref/meters` (M13): a meter READ, not a second state model — no version, no
649
+ * events. ETag = the account's `meters_seq` (rollup and credit writes); `If-None-Match` → 304.
650
+ * `rolledUpThrough`: events received before this moment are counted.
651
+ */
652
+ const AccountMetersSchema = z.object({
653
+ ref: ExternalRefSchema,
654
+ asOf: DateTimeSchema,
655
+ rolledUpThrough: DateTimeSchema,
656
+ meters: z.record(CodeSchema, AccountMeterSchema)
657
+ });
658
+ const METER_PERIOD_STATUSES = [
659
+ "open",
660
+ "closing",
661
+ "closed"
662
+ ];
663
+ const MeterPeriodStatusSchema = z.enum(METER_PERIOD_STATUSES);
664
+ /** A meter period (`GET /v1/accounts/:ref/meters/periods`; closed periods, newest first). */
665
+ const MeterPeriodSchema = z.object({
666
+ meter: CodeSchema,
667
+ periodStart: DateTimeSchema,
668
+ periodEnd: DateTimeSchema,
669
+ status: MeterPeriodStatusSchema,
670
+ /** Subscription the period is anchored to (M7); null for the account anchor. */
671
+ subscriptionId: IdSchema.nullable(),
672
+ consumedUnits: NonNegativeUnitsNumberSchema,
673
+ creditedUnits: NonNegativeUnitsNumberSchema,
674
+ overageUnits: NonNegativeUnitsNumberSchema,
675
+ closedAt: DateTimeSchema.nullable()
676
+ });
677
+ const MeterPeriodListSchema = z.object({
678
+ ref: ExternalRefSchema,
679
+ periods: z.array(MeterPeriodSchema)
680
+ });
681
+ /**
682
+ * `POST /v1/accounts/:ref/credits` (M5): a one-off credit for the meter's open period (goodwill,
683
+ * campaign). Needs no benefit; recorded in the append-only credit ledger with the actor.
684
+ * Unknown meter: `422 meter_not_found`.
685
+ */
686
+ const CreditGrantInputSchema = z.object({
687
+ meter: CodeSchema,
688
+ units: MeterUnitsSchema,
689
+ reason: z.string().trim().min(3).max(500)
690
+ });
691
+ /** Credit ledger entry kinds (M5). Entries are never rewritten. */
692
+ const METER_CREDIT_ENTRY_KINDS = [
693
+ "cycle_grant",
694
+ "rollover",
695
+ "plan_change",
696
+ "manual_grant",
697
+ "adjustment"
698
+ ];
699
+ const MeterCreditEntryKindSchema = z.enum(METER_CREDIT_ENTRY_KINDS);
700
+ const MeterCreditEntrySchema = z.object({
701
+ id: IdSchema,
702
+ meter: CodeSchema,
703
+ kind: MeterCreditEntryKindSchema,
704
+ /** Signed units (an adjustment may be negative). */
705
+ units: UnitsNumberSchema,
706
+ periodStart: DateTimeSchema,
707
+ reason: z.string().nullable(),
708
+ actorRef: z.string().nullable(),
709
+ createdAt: DateTimeSchema
710
+ });
711
+ const METER_CHARGE_STATUSES = [
712
+ "dry_run",
713
+ "carried",
714
+ "invoiced"
715
+ ];
716
+ const MeterChargeStatusSchema = z.enum(METER_CHARGE_STATUSES);
717
+ /** One priced meter of a charge (M21): overage × locked rate, capped. Units are decimal strings. */
718
+ const MeterChargeLineSchema = z.object({
719
+ meter: CodeSchema,
720
+ consumedUnits: DecimalUnitsStringSchema,
721
+ creditedUnits: DecimalUnitsStringSchema,
722
+ overageUnits: DecimalUnitsStringSchema,
723
+ priceAmountMinor: AmountMinorSchema,
724
+ pricePerUnits: z.number().int().min(1),
725
+ capMinor: AmountMinorSchema.nullable(),
726
+ /** `rateOverage(...)`: tax terms of the price (gross when tax-inclusive). */
727
+ amountMinor: AmountMinorSchema
728
+ });
729
+ /**
730
+ * Pricing record of a closed meter period (M21; money data, never rewritten). Totals use the
731
+ * invoice rule (`meterChargeTotals`, one rounding). `status`: `dry_run` (chargeMode `dry_run`:
732
+ * recorded, not invoiced), `carried` (below `billing.minInvoiceMinor` on an overage-only invoice:
733
+ * carried into the next invoice), `invoiced` (`invoiceId` set). `carriedInMinor`: earlier carried
734
+ * amounts included here.
735
+ */
736
+ const MeterChargeSchema = z.object({
737
+ id: IdSchema,
738
+ subscriptionId: IdSchema.nullable(),
739
+ periodStart: DateTimeSchema,
740
+ periodEnd: DateTimeSchema,
741
+ currency: CurrencySchema,
742
+ taxInclusive: z.boolean(),
743
+ taxRateBps: z.number().int().min(0).max(1e4),
744
+ lines: z.array(MeterChargeLineSchema),
745
+ subtotalMinor: AmountMinorSchema,
746
+ taxMinor: AmountMinorSchema,
747
+ totalMinor: AmountMinorSchema,
748
+ carriedInMinor: AmountMinorSchema,
749
+ status: MeterChargeStatusSchema,
750
+ invoiceId: IdSchema.nullable(),
751
+ createdAt: DateTimeSchema
752
+ });
753
+ /** `GET /v1/accounts/:ref/meter-charges` (newest first). */
754
+ const MeterChargeListSchema = z.object({
755
+ ref: ExternalRefSchema,
756
+ charges: z.array(MeterChargeSchema)
757
+ });
758
+ //#endregion
759
+ //#region ../contract/src/benefits.ts
760
+ /** Benefit types this contract version defines. */
761
+ const BENEFIT_TYPES = ["feature_flag", "meter_credit"];
762
+ const BenefitTypeSchema = z.enum(BENEFIT_TYPES);
763
+ const DescriptionTextSchema = z.string().trim().min(1).max(500);
764
+ /** Customer-facing description per locale (hosted checkout and portal "what's included"). */
765
+ const BenefitDescriptionSchema = z.object({
766
+ tr: DescriptionTextSchema.optional(),
767
+ en: DescriptionTextSchema.optional()
768
+ });
769
+ /** `feature_flag` properties: catalog feature key → value (K4 typing; checked against `features` on catalog sync). */
770
+ const FeatureFlagPropertiesSchema = z.record(CodeSchema, EntitlementValueSchema);
771
+ const FeatureFlagBenefitDefSchema = z.object({
772
+ type: z.literal("feature_flag"),
773
+ properties: FeatureFlagPropertiesSchema,
774
+ description: BenefitDescriptionSchema.optional()
775
+ });
776
+ /** `meter_credit` definition (metering phase 2): the meter must be a catalog meter (checked on catalog sync). */
777
+ const MeterCreditBenefitDefSchema = z.object({
778
+ type: z.literal("meter_credit"),
779
+ properties: MeterCreditPropertiesSchema,
780
+ description: BenefitDescriptionSchema.optional()
781
+ });
782
+ /** Catalog benefit definition (catalog sync input), discriminated by `type`. */
783
+ const BenefitDefSchema = z.discriminatedUnion("type", [FeatureFlagBenefitDefSchema, MeterCreditBenefitDefSchema]);
784
+ /**
785
+ * Benefit definition as returned by `GET /v1/catalog`. `type` is an open set (a newer steward may
786
+ * return types this version does not define); narrow with `type === "feature_flag"`.
787
+ */
788
+ const CatalogBenefitSchema = z.object({
789
+ type: z.string().min(1).max(64),
790
+ properties: z.record(z.string(), z.unknown()),
791
+ description: BenefitDescriptionSchema.optional()
792
+ });
793
+ /**
794
+ * Where a granted benefit comes from, e.g. `plan:<code>` (default plan or a live subscription's
795
+ * plan), `grant:<id>` (a grant), `override` (account overrides). Open set: informational.
796
+ */
797
+ const BenefitSourceSchema = z.string().min(1).max(200);
798
+ /**
799
+ * A benefit granted to the account (`AccountState.benefits[]`; Polar granted benefit).
800
+ * `code` is the catalog benefit code; `null` for an implicit benefit that has no catalog code
801
+ * (a plan's entitlement matrix, account overrides). `type` is an open set (see
802
+ * `CatalogBenefitSchema`); `properties` are the values granted.
803
+ */
804
+ const AccountBenefitSchema = z.object({
805
+ code: CodeSchema.nullable(),
806
+ type: z.string().min(1).max(64),
807
+ properties: z.record(z.string(), z.unknown()),
808
+ source: BenefitSourceSchema,
809
+ grantedAt: DateTimeSchema
810
+ });
811
+ /** Narrows a granted benefit to `feature_flag` (type and property values checked). */
812
+ function isFeatureFlagBenefit(benefit) {
813
+ return benefit.type === "feature_flag" && FeatureFlagPropertiesSchema.safeParse(benefit.properties).success;
814
+ }
815
+ /** Narrows a granted benefit to `meter_credit` (type and properties checked). */
816
+ function isMeterCreditBenefit(benefit) {
817
+ return benefit.type === "meter_credit" && MeterCreditPropertiesSchema.safeParse(benefit.properties).success;
818
+ }
819
+ /**
820
+ * Whether a value fits a feature type (K4): `limit` → integer ≥ 0 or `null` (unlimited),
821
+ * `boolean` → boolean, `string` → string. `null` is only valid for `limit`.
822
+ */
823
+ function featureValueMatchesType(type, value) {
824
+ switch (type) {
825
+ case "limit": return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
826
+ case "boolean": return typeof value === "boolean";
827
+ case "string": return typeof value === "string";
828
+ }
829
+ }
830
+ //#endregion
831
+ //#region ../contract/src/emails.ts
832
+ const EMAIL_TEMPLATES = [
833
+ "subscription_started",
834
+ "payment_receipt",
835
+ "payment_failed",
836
+ "cancel_scheduled",
837
+ "subscription_ended",
838
+ "access_suspended",
839
+ "subscription_terminated",
840
+ "invoice_issued",
841
+ "invoice_payment_due",
842
+ "price_change_notice"
843
+ ];
844
+ const EmailTemplateSchema = z.enum(EMAIL_TEMPLATES);
845
+ /** `invoice_payment_due`: summary of an unpaid invoice, its due date and the hosted pay page link. */
846
+ const InvoicePaymentDueEmailSchema = z.object({
847
+ invoiceId: IdSchema,
848
+ /** Invoice number once issued; drafts have none. */
849
+ number: z.string().nullable(),
850
+ billingReason: InvoiceBillingReasonSchema,
851
+ planName: z.string().nullable(),
852
+ totalMinor: AmountMinorSchema,
853
+ currency: CurrencySchema,
854
+ periodStart: DateTimeSchema.nullable(),
855
+ periodEnd: DateTimeSchema.nullable(),
856
+ dueAt: DateTimeSchema,
857
+ /** Steward's hosted pay page for this invoice. */
858
+ payUrl: z.url({ protocol: /^https?$/ }),
859
+ /** false: the invoice was just written; true: a reminder before the due date. */
860
+ reminder: z.boolean()
861
+ });
862
+ /** A metered (overage) rate as a notice shows it: `amountMinor` per `perUnits` units, at most `capMinor` per usage period. */
863
+ const NoticedMeteredRateSchema = z.object({
864
+ amountMinor: AmountMinorSchema,
865
+ perUnits: z.number().int().positive(),
866
+ capMinor: AmountMinorSchema.nullable()
867
+ });
868
+ /**
869
+ * One meter whose overage rate changes with the move to the current terms (metering M20, M36): the
870
+ * subscription's locked rate and the plan's current one, in the notice's currency and tax terms.
871
+ * The meter's labels as they were when the notice was written (the e-mail is never recomputed).
872
+ */
873
+ const PriceChangeNoticeMeteredRateSchema = z.object({
874
+ meter: CodeSchema,
875
+ label: MeterLabelSchema,
876
+ unitLabel: MeterLabelSchema.nullable(),
877
+ /** The meter's `displayPer` (price lines are shown per that many units); null: per the rate's `perUnits`. */
878
+ displayPer: z.number().int().positive().nullable(),
879
+ /** The locked rate; null: the meter is not billed yet (a metered price added to the plan later). */
880
+ oldRate: NoticedMeteredRateSchema.nullable(),
881
+ /** The current rate; null: the meter is no longer billed. */
882
+ newRate: NoticedMeteredRateSchema.nullable()
883
+ });
884
+ /**
885
+ * `price_change_notice`: a subscription moves to the current terms of its plan from `effectiveAt` —
886
+ * its per-period amount and/or its metered rates (`meteredRates`: only the meters whose rate
887
+ * changes; absent when none does). Old and new amount are equal when only metered rates change.
888
+ */
889
+ const PriceChangeNoticeEmailSchema = z.object({
890
+ subscriptionId: IdSchema,
891
+ planCode: CodeSchema,
892
+ planName: z.string(),
893
+ interval: IntervalSchema,
894
+ currency: CurrencySchema,
895
+ oldAmountMinor: AmountMinorSchema,
896
+ newAmountMinor: AmountMinorSchema,
897
+ /** Both amounts are VAT inclusive (true) or exclusive (false), as the price. */
898
+ taxInclusive: z.boolean(),
899
+ effectiveAt: DateTimeSchema,
900
+ meteredRates: z.array(PriceChangeNoticeMeteredRateSchema).optional()
901
+ });
902
+ /** Payload schema per billing core template. */
903
+ const EMAIL_PAYLOAD_SCHEMAS = Object.freeze({
904
+ invoice_payment_due: InvoicePaymentDueEmailSchema,
905
+ price_change_notice: PriceChangeNoticeEmailSchema
906
+ });
907
+ /**
908
+ * Which event triggers which template. Events not listed send no email. The billing core
909
+ * templates (`invoice_payment_due`, `price_change_notice`) are not triggered by an event type alone.
910
+ */
911
+ const EMAIL_TEMPLATE_BY_EVENT = Object.freeze({
912
+ "subscription.activated": "subscription_started",
913
+ "subscription.renewed": "payment_receipt",
914
+ "subscription.payment_failed": "payment_failed",
915
+ "subscription.cancel_scheduled": "cancel_scheduled",
916
+ "subscription.canceled": "subscription_ended",
917
+ "subscription.expired": "subscription_ended",
918
+ "subscription.suspended": "access_suspended",
919
+ "subscription.terminated": "subscription_terminated",
920
+ "invoice.issued": "invoice_issued"
921
+ });
922
+ function emailTemplateOf(type) {
923
+ return EMAIL_TEMPLATE_BY_EVENT[type] ?? null;
924
+ }
925
+ const EMAIL_LOCALES = ["tr", "en"];
926
+ /** Upper bound of product-provided recipients per account. */
927
+ const MAX_NOTIFICATION_EMAILS = 10;
928
+ const NotificationEmailsSchema = z.array(z.email().max(254)).max(10).transform((emails) => uniqueEmails(emails));
929
+ /** Case-insensitive de-duplication; keeps the first spelling and the order. */
930
+ function uniqueEmails(emails) {
931
+ const seen = /* @__PURE__ */ new Set();
932
+ const out = [];
933
+ for (const email of emails) {
934
+ const key = email.trim().toLowerCase();
935
+ if (key === "" || seen.has(key)) continue;
936
+ seen.add(key);
937
+ out.push(email.trim());
938
+ }
939
+ return out;
940
+ }
941
+ /** Profile email first, then the account's notification emails. */
942
+ function emailRecipients(profileEmail, notificationEmails) {
943
+ return uniqueEmails([...profileEmail ? [profileEmail] : [], ...notificationEmails]);
944
+ }
945
+ function emailLocaleOf(accountLocale, lastCheckoutLocale, defaultLocale) {
946
+ const known = (value) => EMAIL_LOCALES.includes(value ?? "");
947
+ if (known(accountLocale)) return accountLocale;
948
+ if (known(lastCheckoutLocale)) return lastCheckoutLocale;
949
+ return defaultLocale;
950
+ }
951
+ const ExtraTextValueSchema = z.string().trim().min(1).max(1e3);
952
+ const EmailExtraTextSchema = z.object({
953
+ tr: ExtraTextValueSchema.optional(),
954
+ en: ExtraTextValueSchema.optional()
955
+ });
956
+ const EmailTemplateSettingsSchema = z.object({
957
+ /** false: steward does not send this template (the product sends its own). */
958
+ enabled: z.boolean().default(true),
959
+ /** Plain-text paragraph appended to the template body, per locale. */
960
+ extraText: EmailExtraTextSchema.optional()
961
+ });
962
+ const EmailTemplatesSchema = z.object(Object.fromEntries(EMAIL_TEMPLATES.map((t) => [t, EmailTemplateSettingsSchema.optional()])));
963
+ const EmailSenderSchema = z.object({
964
+ /** Display name; defaults to `branding.name`. */
965
+ name: z.string().trim().min(1).max(100).optional(),
966
+ /** Sender address; defaults to the deployment's `STEWARD_EMAIL_FROM`. Its domain needs SPF/DKIM. */
967
+ address: z.email().max(254).optional()
968
+ });
969
+ /** Placeholders a billing path may use. */
970
+ const BILLING_PATH_PLACEHOLDERS = ["{LOCALE}", "{ACCOUNT_REF}"];
971
+ 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");
972
+ const EmailSettingsSchema = z.object({
973
+ from: EmailSenderSchema.optional(),
974
+ /** Reply-To; defaults to `branding.supportEmail`. */
975
+ replyTo: z.email().max(254).optional(),
976
+ /** Product billing page under `appUrl`, e.g. `/{LOCALE}/app/orgs/{ACCOUNT_REF}/billing`; without it buttons open `appUrl`. */
977
+ billingPath: BillingPathSchema.optional(),
978
+ templates: EmailTemplatesSchema.default({})
979
+ });
980
+ const EmailSettingsPatchSchema = z.object({
981
+ from: EmailSenderSchema.strict().nullable().optional(),
982
+ replyTo: z.email().max(254).nullable().optional(),
983
+ billingPath: BillingPathSchema.nullable().optional(),
984
+ templates: z.object(Object.fromEntries(EMAIL_TEMPLATES.map((t) => [t, z.object({
985
+ enabled: z.boolean().optional(),
986
+ extraText: EmailExtraTextSchema.strict().optional()
987
+ }).strict().nullable().optional()]))).strict().optional()
988
+ }).strict();
989
+ /** Pure merge of a validated patch (see the patch rules above). */
990
+ function applyEmailSettingsPatch(current, patch) {
991
+ if (patch === null) return EmailSettingsSchema.parse({});
992
+ const next = {
993
+ ...current,
994
+ templates: { ...current.templates }
995
+ };
996
+ for (const [key, value] of Object.entries(patch)) {
997
+ if (value === void 0) continue;
998
+ if (key === "templates") {
999
+ const templates = next.templates;
1000
+ for (const [template, settings] of Object.entries(value)) {
1001
+ if (settings === void 0) continue;
1002
+ if (settings === null) delete templates[template];
1003
+ else templates[template] = settings;
1004
+ }
1005
+ } else if (value === null) delete next[key];
1006
+ else next[key] = value;
1007
+ }
1008
+ return EmailSettingsSchema.parse(next);
1009
+ }
1010
+ function emailTemplateSettings(settings, template) {
1011
+ return settings.templates[template] ?? { enabled: true };
1012
+ }
1013
+ /**
1014
+ * Button target: `appUrl` + `billingPath` with placeholders filled (the ref is URL-encoded).
1015
+ * Null without `appUrl`, or when the result would leave the `appUrl` origin.
1016
+ */
1017
+ function billingPageUrl(appUrl, billingPath, locale, accountRef) {
1018
+ if (appUrl === void 0) return null;
1019
+ let base;
1020
+ try {
1021
+ base = new URL(appUrl);
1022
+ } catch {
1023
+ return null;
1024
+ }
1025
+ if (billingPath === void 0) return base.toString();
1026
+ const path = billingPath.replaceAll("{LOCALE}", locale).replaceAll("{ACCOUNT_REF}", encodeURIComponent(accountRef));
1027
+ try {
1028
+ const url = new URL(path, base.origin);
1029
+ return url.origin === base.origin ? url.toString() : null;
1030
+ } catch {
1031
+ return null;
1032
+ }
1033
+ }
1034
+ /**
1035
+ * The event's subscription among the account's live subscriptions at send time, or null when it
1036
+ * is not live. Looked up by id in `subscriptions` (billing core, M34: any live subscription, not
1037
+ * only the primary one); a state without that list (older steward) has only the primary
1038
+ * `subscription`.
1039
+ */
1040
+ function liveSubscriptionOf(state, subscriptionId) {
1041
+ if (subscriptionId === null) return null;
1042
+ if (state.subscriptions !== void 0) return state.subscriptions.find((s) => s.id === subscriptionId) ?? null;
1043
+ return state.subscription?.id === subscriptionId ? state.subscription : null;
1044
+ }
1045
+ /**
1046
+ * Whether the email still describes the account at send time. Receipts, the welcome email
1047
+ * (legal confirmation) and the final termination notice always go; the others only while
1048
+ * the situation they describe still holds for the event's subscription (which need not be the
1049
+ * primary one when the account has several).
1050
+ */
1051
+ function isEmailRelevant(template, facts) {
1052
+ const sub = liveSubscriptionOf(facts.state, facts.subscriptionId);
1053
+ switch (template) {
1054
+ case "subscription_started":
1055
+ case "payment_receipt":
1056
+ case "subscription_terminated":
1057
+ case "price_change_notice": return true;
1058
+ case "invoice_payment_due": return facts.invoicePaymentStatus === "unpaid";
1059
+ case "payment_failed": return sub?.status === "past_due";
1060
+ case "cancel_scheduled": return sub?.cancelAtPeriodEnd === true;
1061
+ case "subscription_ended": return facts.state.subscriptions !== void 0 ? sub === null : facts.state.subscription === null;
1062
+ case "access_suspended": return sub?.status === "suspended";
1063
+ case "invoice_issued": return facts.invoiceStatus === "issued";
1064
+ }
1065
+ }
1066
+ //#endregion
49
1067
  //#region ../contract/src/accounts.ts
1068
+ const AccountLocaleSchema = z.enum(EMAIL_LOCALES);
50
1069
  const AccountUpsertInputSchema = z.object({
51
1070
  displayName: z.string().min(1).max(200),
52
- metadata: MetadataSchema.default({})
1071
+ metadata: MetadataSchema.default({}),
1072
+ /**
1073
+ * Extra recipients of billing emails besides the billing profile email (e.g. the
1074
+ * organization owners). Omitted: unchanged (older clients keep working); `[]` clears.
1075
+ */
1076
+ notificationEmails: NotificationEmailsSchema.optional(),
1077
+ /** Language of billing emails. Omitted: unchanged; `null`: latest checkout's, else the product default. */
1078
+ locale: AccountLocaleSchema.nullable().optional()
53
1079
  });
54
1080
  const BillingProfileKindSchema = z.enum(["individual", "company"]);
55
1081
  const BillingProfileSchema = z.object({
@@ -92,6 +1118,10 @@ const SubscriptionSummarySchema = z.object({
92
1118
  status: SubscriptionStatusSchema,
93
1119
  planCode: CodeSchema,
94
1120
  interval: IntervalSchema,
1121
+ /**
1122
+ * Amount the subscription pays per period. Billing core (M36): the subscription's LOCKED amount
1123
+ * (copied when it started); a later in-place price change does not alter it.
1124
+ */
95
1125
  amountMinor: AmountMinorSchema,
96
1126
  currency: CurrencySchema,
97
1127
  currentPeriodEnd: DateTimeSchema.nullable(),
@@ -111,11 +1141,16 @@ const AccountDunningStateSchema = z.object({
111
1141
  /** Sıradaki dunning aksiyonunun zamanı; kalmadıysa null. */
112
1142
  nextActionAt: DateTimeSchema.nullable()
113
1143
  });
114
- /** Dönem sonunda uygulanacak plan değişikliği. F4b'ye kadar her zaman null. */
1144
+ /**
1145
+ * Change applied at the period end (`POST /v1/subscriptions/:id/change`, MF9). Null until then.
1146
+ * `price: "current"` on the same plan shows the same `planCode`/`interval` with the new `amountMinor`.
1147
+ */
115
1148
  const AccountScheduledChangeSchema = z.object({
116
1149
  planCode: CodeSchema,
117
1150
  interval: IntervalSchema,
118
- effectiveAt: DateTimeSchema
1151
+ effectiveAt: DateTimeSchema,
1152
+ /** Amount locked from `effectiveAt` (billing core; absent from older steward). */
1153
+ amountMinor: AmountMinorSchema.optional()
119
1154
  });
120
1155
  const AccountSubscriptionStateSchema = SubscriptionSummarySchema.extend({
121
1156
  cancelAtPeriodEnd: z.boolean(),
@@ -124,7 +1159,10 @@ const AccountSubscriptionStateSchema = SubscriptionSummarySchema.extend({
124
1159
  currentPeriodStart: DateTimeSchema.nullable(),
125
1160
  /** Abonelik dunning'de değilse null. */
126
1161
  dunning: AccountDunningStateSchema.nullable(),
127
- /** F4b (`POST /v1/subscriptions/:id/change`) gelene kadar her zaman null. */
1162
+ /**
1163
+ * The pending plan/interval/price change (billing core MF9: `POST /v1/subscriptions/:id/change`,
1164
+ * the portal's Change plan, the operator's `reprice-subscriptions`); null when none.
1165
+ */
128
1166
  scheduledChange: AccountScheduledChangeSchema.nullable()
129
1167
  });
130
1168
  /** Hesabın açık (tamamlanmamış, süresi dolmamış) checkout oturumu. */
@@ -137,6 +1175,38 @@ const AccountOpenCheckoutSchema = z.object({
137
1175
  amountMinor: AmountMinorSchema,
138
1176
  expiresAt: DateTimeSchema
139
1177
  });
1178
+ /**
1179
+ * An unpaid invoice of the account (billing core, M22/M34): renewal or final invoices written
1180
+ * before payment. Account level (not under a subscription) so the final invoice of an ended
1181
+ * subscription stays visible; the product can show a banner before `dueAt`.
1182
+ */
1183
+ const AccountOpenInvoiceSchema = z.object({
1184
+ id: IdSchema,
1185
+ /** Null for an invoice not tied to a subscription. */
1186
+ subscriptionId: IdSchema.nullable(),
1187
+ billingReason: InvoiceBillingReasonSchema,
1188
+ totalMinor: AmountMinorSchema,
1189
+ currency: CurrencySchema,
1190
+ dueAt: DateTimeSchema
1191
+ });
1192
+ /**
1193
+ * Low-churn meter facts of the account state (metering, M13; Polar `active_meters`, coarse). One
1194
+ * entry per catalog meter. Balances and period bounds are NOT here (they change with every event;
1195
+ * read them with `GET /v1/accounts/:ref/meters`): this block changes when the plan changes, a
1196
+ * threshold is crossed or a new period resets the standing — never per event.
1197
+ */
1198
+ const AccountMeterStateSchema = z.object({
1199
+ /**
1200
+ * Units the account's `meter_credit` benefits grant per period (`meterCreditsOf`: benefits sum;
1201
+ * the default plan's credit only when no other layer credits the meter); `null`: no credit (not
1202
+ * gated by credit, M15).
1203
+ */
1204
+ includedUnits: z.number().nonnegative().nullable(),
1205
+ /** A live subscription bills overage of this meter (metered rate): the gate stays open (M15) until the rate's cap is reached. */
1206
+ billable: z.boolean(),
1207
+ standing: MeterStandingSchema
1208
+ });
1209
+ const AccountMetersStateSchema = z.record(CodeSchema, AccountMeterStateSchema);
140
1210
  /** Fatura profili özeti: yalnızca var mı ve türü (alanlar PII, burada yok). */
141
1211
  const AccountProfileStateSchema = z.object({
142
1212
  present: z.boolean(),
@@ -155,14 +1225,27 @@ const AccountStateSchema = EntitlementSnapshotSchema.extend({
155
1225
  catalogVersion: z.string().nullable(),
156
1226
  subscription: AccountSubscriptionStateSchema.nullable(),
157
1227
  openCheckout: AccountOpenCheckoutSchema.nullable(),
158
- profile: AccountProfileStateSchema
1228
+ profile: AccountProfileStateSchema,
1229
+ subscriptions: z.array(AccountSubscriptionStateSchema).optional(),
1230
+ openInvoices: z.array(AccountOpenInvoiceSchema).optional(),
1231
+ benefits: z.array(AccountBenefitSchema).optional(),
1232
+ /**
1233
+ * Metering phase 2 (M13): per catalog meter `{includedUnits, billable, standing}`. Present only
1234
+ * for a product whose catalog defines meters (the state of other products, and so its version
1235
+ * and fingerprint, stays exactly as before).
1236
+ */
1237
+ meters: AccountMetersStateSchema.optional()
159
1238
  });
160
1239
  const AccountSchema = z.object({
161
1240
  ref: ExternalRefSchema,
162
1241
  displayName: z.string(),
163
1242
  metadata: MetadataSchema,
164
1243
  createdAt: DateTimeSchema,
165
- snapshot: EntitlementSnapshotSchema
1244
+ snapshot: EntitlementSnapshotSchema,
1245
+ /** Extra billing email recipients (older steward: absent → `[]`). */
1246
+ notificationEmails: z.array(z.string()).default([]),
1247
+ /** Billing email language set by the product; null: not set. */
1248
+ locale: AccountLocaleSchema.nullable().default(null)
166
1249
  });
167
1250
  const AccountDeleteResponseSchema = z.object({
168
1251
  ref: ExternalRefSchema,
@@ -181,6 +1264,11 @@ const ACCOUNT_INCLUDES = [
181
1264
  const EventIdSchema = z.string().regex(/^evt_[0-9a-f-]{36}$/);
182
1265
  const ApiVersionSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
183
1266
  const sub = { subscriptionId: IdSchema };
1267
+ /** `benefit_grant.*` detail base: the grant log row id and the granted benefit (as in `AccountState.benefits[]`). */
1268
+ const benefitGrant = {
1269
+ grantId: IdSchema,
1270
+ benefit: AccountBenefitSchema
1271
+ };
184
1272
  const EVENT_DETAIL_SCHEMAS = {
185
1273
  "account.updated": z.object({
186
1274
  /** Snapshot'ın hangi alanları değişti (planCode, accessState, entitlements, subscription). */
@@ -214,7 +1302,9 @@ changed: z.array(z.string()) }),
214
1302
  paymentId: IdSchema.nullable(),
215
1303
  /** Dunning'in kaçıncı başarısız denemesi (1'den başlar). */
216
1304
  attempt: z.number().int().positive(),
217
- nextRetryAt: DateTimeSchema.nullable()
1305
+ nextRetryAt: DateTimeSchema.nullable(),
1306
+ /** Billing core (M23): the unpaid invoice past its due date that started dunning; absent on the legacy provider path. */
1307
+ invoiceId: IdSchema.optional()
218
1308
  }),
219
1309
  "subscription.suspended": z.object({
220
1310
  ...sub,
@@ -271,6 +1361,51 @@ changed: z.array(z.string()) }),
271
1361
  "account.deleted": z.object({
272
1362
  deletedAt: DateTimeSchema,
273
1363
  actorRef: z.string().nullable()
1364
+ }),
1365
+ "invoice.paid": z.object({
1366
+ invoiceId: IdSchema,
1367
+ subscriptionId: IdSchema.nullable(),
1368
+ paymentId: IdSchema,
1369
+ /** Null for an invoice written the pre-billing-core way (after a payment). */
1370
+ billingReason: InvoiceBillingReasonSchema.nullable(),
1371
+ totalMinor: AmountMinorSchema,
1372
+ currency: CurrencySchema,
1373
+ paidAt: DateTimeSchema
1374
+ }),
1375
+ "benefit_grant.created": z.object({ ...benefitGrant }),
1376
+ "benefit_grant.updated": z.object({
1377
+ ...benefitGrant,
1378
+ previousProperties: z.record(z.string(), z.unknown())
1379
+ }),
1380
+ "benefit_grant.revoked": z.object({
1381
+ ...benefitGrant,
1382
+ revokedAt: DateTimeSchema
1383
+ }),
1384
+ "benefit_grant.cycled": z.object({
1385
+ ...benefitGrant,
1386
+ periodStart: DateTimeSchema,
1387
+ periodEnd: DateTimeSchema
1388
+ }),
1389
+ "meter.threshold_crossed": z.object({
1390
+ meter: CodeSchema,
1391
+ standing: MeterStandingSchema,
1392
+ previous: MeterStandingSchema,
1393
+ periodStart: DateTimeSchema,
1394
+ periodEnd: DateTimeSchema,
1395
+ consumedUnits: z.number().nonnegative(),
1396
+ creditedUnits: z.number().nonnegative()
1397
+ }),
1398
+ "meter.period_closed": z.object({
1399
+ periodStart: DateTimeSchema,
1400
+ periodEnd: DateTimeSchema,
1401
+ subscriptionId: IdSchema.nullable(),
1402
+ meters: z.array(z.object({
1403
+ meter: CodeSchema,
1404
+ consumedUnits: z.number().nonnegative(),
1405
+ creditedUnits: z.number().nonnegative(),
1406
+ overageUnits: z.number().nonnegative()
1407
+ })).min(1),
1408
+ meterChargeId: IdSchema.nullable()
274
1409
  })
275
1410
  };
276
1411
  /** Tüm event tipleri (yenileri dahil). */
@@ -296,7 +1431,18 @@ const LEGACY_EVENT_TYPES = Object.freeze([
296
1431
  "invoice.created",
297
1432
  "invoice.issued"
298
1433
  ]);
299
- const FULL_STATE_EVENT_TYPES = /* @__PURE__ */ new Set(["account.state_changed", "account.deleted"]);
1434
+ /** Event types whose `data.account` is the full `AccountState` (the others: the entitlement snapshot). */
1435
+ const FULL_STATE_EVENT_TYPES = /* @__PURE__ */ new Set([
1436
+ "account.state_changed",
1437
+ "account.deleted",
1438
+ "invoice.paid",
1439
+ "benefit_grant.created",
1440
+ "benefit_grant.updated",
1441
+ "benefit_grant.revoked",
1442
+ "benefit_grant.cycled",
1443
+ "meter.threshold_crossed",
1444
+ "meter.period_closed"
1445
+ ]);
300
1446
  function accountSchemaOf(type) {
301
1447
  return FULL_STATE_EVENT_TYPES.has(type) ? AccountStateSchema : EntitlementSnapshotSchema;
302
1448
  }
@@ -347,4 +1493,4 @@ const PingSchema = z.object({
347
1493
  endpointId: IdSchema
348
1494
  });
349
1495
  //#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 };
1496
+ export { emailRecipients as $, canonicalMeterFilter as $t, SubscriptionStatusSchema as A, isEventTimestampInRange as An, METER_FILTER_MAX_DEPTH as At, EmailSettingsPatchSchema as B, ErrorCategorySchema as Bn, MeterCreditEntrySchema as Bt, AccountSchema as C, EventsIngestInputSchema as Cn, DecimalUnitsStringSchema as Ct, BillingProfileKindSchema as D, IngestRejectCodeSchema as Dn, METER_CHARGE_STATUSES as Dt, AccountUpsertInputSchema as E, IngestEventSchema as En, METER_AGGREGATION_KINDS as Et, EMAIL_PAYLOAD_SCHEMAS as F, CurrencySchema as Fn, MeterChargeLineSchema as Ft, MAX_NOTIFICATION_EMAILS as G, IdSchema as Gn, MeterLowAtSchema as Gt, EmailTemplateSchema as H, ExternalRefSchema as Hn, MeterDefSchema as Ht, EMAIL_TEMPLATES as I, DateTimeSchema as In, MeterChargeListSchema as It, PriceChangeNoticeEmailSchema as J, InvoicePaymentStatusSchema as Jn, MeterPeriodStatusSchema as Jt, NoticedMeteredRateSchema as K, IntervalSchema as Kn, MeterPeriodListSchema as Kt, EMAIL_TEMPLATE_BY_EVENT as L, ERROR_CATEGORIES as Ln, MeterChargeSchema as Lt, BILLING_PATH_PLACEHOLDERS as M, ActorRefSchema as Mn, METER_PERIOD_STATUSES as Mt, BillingPathSchema as N, AmountMinorSchema as Nn, METER_STANDINGS as Nt, BillingProfileSchema as O, IngestRejectionSchema as On, METER_CREDIT_ENTRY_KINDS as Ot, EMAIL_LOCALES as P, CodeSchema as Pn, MeterAggregationSchema as Pt, emailLocaleOf as Q, MeteredPriceSchema as Qt, EmailExtraTextSchema as R, EntitlementValueSchema as Rn, MeterChargeStatusSchema as Rt, AccountScheduledChangeSchema as S, EventNameSchema as Sn, DEFAULT_METER_LOW_AT as St, AccountSubscriptionStateSchema as T, INGEST_REJECT_CODES as Tn, MAX_METER_UNITS as Tt, EmailTemplateSettingsSchema as U, INVOICE_BILLING_REASONS as Un, MeterFilterSchema as Ut, EmailSettingsSchema as V, ErrorResponseSchema as Vn, MeterCreditPropertiesSchema as Vt, InvoicePaymentDueEmailSchema as W, INVOICE_PAYMENT_STATUSES as Wn, MeterLabelSchema as Wt, applyEmailSettingsPatch as X, MeterUnitsSchema as Xt, PriceChangeNoticeMeteredRateSchema as Y, MetadataSchema as Yn, MeterStandingSchema as Yt, billingPageUrl as Z, MeteredPriceDefSchema as Zt, AccountMeterStateSchema as _, EVENT_TIMESTAMP_MAX_SKEW_MINUTES as _n, isMeterCreditBenefit as _t, EVENT_TYPES as a, isMicroPrecise as an, AccountBenefitSchema as at, AccountOpenInvoiceSchema as b, EventMetadataSchema as bn, CatalogMeterSchema as bt, LEGACY_EVENT_TYPES as c, plainDecimalOf as cn, BenefitDescriptionSchema as ct, isEventType as d, EVENTS_INGEST_MAX_EVENTS as dn, CatalogBenefitSchema as dt, meterFilterClauseOf as en, emailTemplateOf as et, ACCOUNT_INCLUDES as f, EVENT_METADATA_KEY_MAX_LENGTH as fn, FeatureFlagBenefitDefSchema as ft, AccountLocaleSchema as g, EVENT_TIMESTAMP_MAX_AGE_DAYS as gn, isFeatureFlagBenefit as gt, AccountDunningStateSchema as h, EVENT_NAME_MAX_LENGTH as hn, featureValueMatchesType as ht, EVENT_DETAIL_SCHEMAS as i, fromMicroUnits as in, uniqueEmails as it, SubscriptionSummarySchema as j, API_VERSION as jn, METER_FILTER_OPERATORS as jt, EntitlementSnapshotSchema as k, hasControlCharacters as kn, METER_FILTER_MAX_CONDITIONS as kt, PING_EVENT_TYPE as l, toMicroUnits as ln, BenefitSourceSchema as lt, AccountDeleteResponseSchema as m, EVENT_METADATA_STRING_MAX_LENGTH as mn, MeterCreditBenefitDefSchema as mt, ApiVersionSchema as n, MICRO_UNITS_PER_UNIT as nn, isEmailRelevant as nt, EventIdSchema as o, microToDecimalString as on, BENEFIT_TYPES as ot, AccessStateSchema as p, EVENT_METADATA_MAX_PAIRS as pn, FeatureFlagPropertiesSchema as pt, NotificationEmailsSchema as q, InvoiceBillingReasonSchema as qn, MeterPeriodSchema as qt, BillingEventSchema as r, UNIT_DECIMALS as rn, liveSubscriptionOf as rt, FULL_STATE_EVENT_TYPES as s, parseMicroUnits as sn, BenefitDefSchema as st, AnyBillingEventSchema as t, InvalidUnitValueError as tn, emailTemplateSettings as tt, PingSchema as u, EVENTS_INGEST_MAX_BYTES as un, BenefitTypeSchema as ut, AccountMetersStateSchema as v, EventExternalIdSchema as vn, AccountMeterSchema as vt, AccountStateSchema as w, EventsIngestResultSchema as wn, MAX_METERED_PRICE_PER_UNITS as wt, AccountProfileStateSchema as x, EventMetadataValueSchema as xn, CreditGrantInputSchema as xt, AccountOpenCheckoutSchema as y, EventMetadataKeySchema as yn, AccountMetersSchema as yt, EmailSenderSchema as z, EntitlementsSchema as zn, MeterCreditEntryKindSchema as zt };