@stewardhq/sdk 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +332 -273
- package/dist/_chunks/errors.js +1 -1
- package/dist/_chunks/events.d.ts +386 -78
- package/dist/_chunks/events.js +1035 -19
- package/dist/_chunks/index.d.ts +3752 -305
- package/dist/_chunks/locale.d.ts +270 -2
- package/dist/_chunks/src.js +925 -31
- package/dist/_chunks/validators.d.ts +46 -2
- package/dist/_chunks/validators.js +128 -27
- package/dist/_chunks/webhook-core.d.ts +2 -2
- package/dist/contract.d.ts +4 -4
- package/dist/contract.js +4 -4
- package/dist/index.d.ts +447 -33
- package/dist/index.js +1659 -26
- package/dist/server.d.ts +66 -16
- package/dist/server.js +35 -4
- package/package.json +14 -13
- package/dist/_chunks/steward.d.ts +0 -189
- package/dist/_chunks/steward.js +0 -564
- package/dist/testing/fixtures/events/LOCK.json +0 -27
- package/dist/testing/fixtures/events/account.deleted.json +0 -36
- package/dist/testing/fixtures/events/account.state_changed.json +0 -53
- package/dist/testing/fixtures/events/account.updated.json +0 -37
- package/dist/testing/fixtures/events/checkout.completed.json +0 -36
- package/dist/testing/fixtures/events/checkout.expired.json +0 -26
- package/dist/testing/fixtures/events/checkout.failed.json +0 -27
- package/dist/testing/fixtures/events/invoice.created.json +0 -37
- package/dist/testing/fixtures/events/invoice.issued.json +0 -38
- package/dist/testing/fixtures/events/invoice.voided.json +0 -39
- package/dist/testing/fixtures/events/subscription.activated.json +0 -40
- package/dist/testing/fixtures/events/subscription.cancel_scheduled.json +0 -38
- package/dist/testing/fixtures/events/subscription.canceled.json +0 -29
- package/dist/testing/fixtures/events/subscription.expired.json +0 -27
- package/dist/testing/fixtures/events/subscription.payment_failed.json +0 -38
- package/dist/testing/fixtures/events/subscription.reactivated.json +0 -36
- package/dist/testing/fixtures/events/subscription.renewed.json +0 -39
- package/dist/testing/fixtures/events/subscription.suspended.json +0 -36
- package/dist/testing/fixtures/events/subscription.terminated.json +0 -28
- package/dist/testing.d.ts +0 -656
- package/dist/testing.js +0 -3684
package/dist/_chunks/events.js
CHANGED
|
@@ -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,6 +71,763 @@ 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
|
+
for (const [i, member] of members.entries()) 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
|
|
49
831
|
//#region ../contract/src/emails.ts
|
|
50
832
|
const EMAIL_TEMPLATES = [
|
|
51
833
|
"subscription_started",
|
|
@@ -55,10 +837,97 @@ const EMAIL_TEMPLATES = [
|
|
|
55
837
|
"subscription_ended",
|
|
56
838
|
"access_suspended",
|
|
57
839
|
"subscription_terminated",
|
|
58
|
-
"invoice_issued"
|
|
840
|
+
"invoice_issued",
|
|
841
|
+
"invoice_payment_due",
|
|
842
|
+
"price_change_notice",
|
|
843
|
+
"bank_transfer_instructions"
|
|
59
844
|
];
|
|
60
845
|
const EmailTemplateSchema = z.enum(EMAIL_TEMPLATES);
|
|
61
|
-
/**
|
|
846
|
+
/** `invoice_payment_due`: summary of an unpaid invoice, its due date and the hosted pay page link. */
|
|
847
|
+
const InvoicePaymentDueEmailSchema = z.object({
|
|
848
|
+
invoiceId: IdSchema,
|
|
849
|
+
/** Invoice number once issued; drafts have none. */
|
|
850
|
+
number: z.string().nullable(),
|
|
851
|
+
billingReason: InvoiceBillingReasonSchema,
|
|
852
|
+
planName: z.string().nullable(),
|
|
853
|
+
totalMinor: AmountMinorSchema,
|
|
854
|
+
currency: CurrencySchema,
|
|
855
|
+
periodStart: DateTimeSchema.nullable(),
|
|
856
|
+
periodEnd: DateTimeSchema.nullable(),
|
|
857
|
+
dueAt: DateTimeSchema,
|
|
858
|
+
/** Steward's hosted pay page for this invoice. */
|
|
859
|
+
payUrl: z.url({ protocol: /^https?$/ }),
|
|
860
|
+
/** false: the invoice was just written; true: a reminder before the due date. */
|
|
861
|
+
reminder: z.boolean()
|
|
862
|
+
});
|
|
863
|
+
/** A metered (overage) rate as a notice shows it: `amountMinor` per `perUnits` units, at most `capMinor` per usage period. */
|
|
864
|
+
const NoticedMeteredRateSchema = z.object({
|
|
865
|
+
amountMinor: AmountMinorSchema,
|
|
866
|
+
perUnits: z.number().int().positive(),
|
|
867
|
+
capMinor: AmountMinorSchema.nullable()
|
|
868
|
+
});
|
|
869
|
+
/**
|
|
870
|
+
* One meter whose overage rate changes with the move to the current terms (metering M20, M36): the
|
|
871
|
+
* subscription's locked rate and the plan's current one, in the notice's currency and tax terms.
|
|
872
|
+
* The meter's labels as they were when the notice was written (the e-mail is never recomputed).
|
|
873
|
+
*/
|
|
874
|
+
const PriceChangeNoticeMeteredRateSchema = z.object({
|
|
875
|
+
meter: CodeSchema,
|
|
876
|
+
label: MeterLabelSchema,
|
|
877
|
+
unitLabel: MeterLabelSchema.nullable(),
|
|
878
|
+
/** The meter's `displayPer` (price lines are shown per that many units); null: per the rate's `perUnits`. */
|
|
879
|
+
displayPer: z.number().int().positive().nullable(),
|
|
880
|
+
/** The locked rate; null: the meter is not billed yet (a metered price added to the plan later). */
|
|
881
|
+
oldRate: NoticedMeteredRateSchema.nullable(),
|
|
882
|
+
/** The current rate; null: the meter is no longer billed. */
|
|
883
|
+
newRate: NoticedMeteredRateSchema.nullable()
|
|
884
|
+
});
|
|
885
|
+
/**
|
|
886
|
+
* `price_change_notice`: a subscription moves to the current terms of its plan from `effectiveAt` —
|
|
887
|
+
* its per-period amount and/or its metered rates (`meteredRates`: only the meters whose rate
|
|
888
|
+
* changes; absent when none does). Old and new amount are equal when only metered rates change.
|
|
889
|
+
*/
|
|
890
|
+
const PriceChangeNoticeEmailSchema = z.object({
|
|
891
|
+
subscriptionId: IdSchema,
|
|
892
|
+
planCode: CodeSchema,
|
|
893
|
+
planName: z.string(),
|
|
894
|
+
interval: IntervalSchema,
|
|
895
|
+
currency: CurrencySchema,
|
|
896
|
+
oldAmountMinor: AmountMinorSchema,
|
|
897
|
+
newAmountMinor: AmountMinorSchema,
|
|
898
|
+
/** Both amounts are VAT inclusive (true) or exclusive (false), as the price. */
|
|
899
|
+
taxInclusive: z.boolean(),
|
|
900
|
+
effectiveAt: DateTimeSchema,
|
|
901
|
+
meteredRates: z.array(PriceChangeNoticeMeteredRateSchema).optional()
|
|
902
|
+
});
|
|
903
|
+
/**
|
|
904
|
+
* `bank_transfer_instructions` (bank transfer plan H12): how to pay a checkout by bank transfer —
|
|
905
|
+
* the deployment's bank account for the currency, the exact amount, the account's order code to
|
|
906
|
+
* write in the description and the deadline. `reminder`: sent the day before the deadline.
|
|
907
|
+
*/
|
|
908
|
+
const BankTransferInstructionsEmailSchema = z.object({
|
|
909
|
+
checkoutSessionId: IdSchema,
|
|
910
|
+
planName: z.string(),
|
|
911
|
+
totalMinor: AmountMinorSchema,
|
|
912
|
+
currency: CurrencySchema,
|
|
913
|
+
beneficiary: z.string(),
|
|
914
|
+
bank: z.string().nullable(),
|
|
915
|
+
iban: z.string(),
|
|
916
|
+
/** The account's order code (`XXXX-XXXX-X`). */
|
|
917
|
+
code: z.string(),
|
|
918
|
+
deadline: DateTimeSchema,
|
|
919
|
+
reminder: z.boolean()
|
|
920
|
+
});
|
|
921
|
+
/** Payload schema per billing core template. */
|
|
922
|
+
const EMAIL_PAYLOAD_SCHEMAS = Object.freeze({
|
|
923
|
+
bank_transfer_instructions: BankTransferInstructionsEmailSchema,
|
|
924
|
+
invoice_payment_due: InvoicePaymentDueEmailSchema,
|
|
925
|
+
price_change_notice: PriceChangeNoticeEmailSchema
|
|
926
|
+
});
|
|
927
|
+
/**
|
|
928
|
+
* Which event triggers which template. Events not listed send no email. The billing core
|
|
929
|
+
* templates (`invoice_payment_due`, `price_change_notice`) are not triggered by an event type alone.
|
|
930
|
+
*/
|
|
62
931
|
const EMAIL_TEMPLATE_BY_EVENT = Object.freeze({
|
|
63
932
|
"subscription.activated": "subscription_started",
|
|
64
933
|
"subscription.renewed": "payment_receipt",
|
|
@@ -183,21 +1052,35 @@ function billingPageUrl(appUrl, billingPath, locale, accountRef) {
|
|
|
183
1052
|
}
|
|
184
1053
|
}
|
|
185
1054
|
/**
|
|
1055
|
+
* The event's subscription among the account's live subscriptions at send time, or null when it
|
|
1056
|
+
* is not live. Looked up by id in `subscriptions` (billing core, M34: any live subscription, not
|
|
1057
|
+
* only the primary one); a state without that list (older steward) has only the primary
|
|
1058
|
+
* `subscription`.
|
|
1059
|
+
*/
|
|
1060
|
+
function liveSubscriptionOf(state, subscriptionId) {
|
|
1061
|
+
if (subscriptionId === null) return null;
|
|
1062
|
+
if (state.subscriptions !== void 0) return state.subscriptions.find((s) => s.id === subscriptionId) ?? null;
|
|
1063
|
+
return state.subscription?.id === subscriptionId ? state.subscription : null;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
186
1066
|
* Whether the email still describes the account at send time. Receipts, the welcome email
|
|
187
1067
|
* (legal confirmation) and the final termination notice always go; the others only while
|
|
188
|
-
* the situation they describe still holds
|
|
1068
|
+
* the situation they describe still holds for the event's subscription (which need not be the
|
|
1069
|
+
* primary one when the account has several).
|
|
189
1070
|
*/
|
|
190
1071
|
function isEmailRelevant(template, facts) {
|
|
191
|
-
const
|
|
192
|
-
const same = live !== null && live.id === facts.subscriptionId;
|
|
1072
|
+
const sub = liveSubscriptionOf(facts.state, facts.subscriptionId);
|
|
193
1073
|
switch (template) {
|
|
194
1074
|
case "subscription_started":
|
|
195
1075
|
case "payment_receipt":
|
|
196
|
-
case "subscription_terminated":
|
|
197
|
-
case "
|
|
198
|
-
case "
|
|
199
|
-
case "
|
|
200
|
-
case "
|
|
1076
|
+
case "subscription_terminated":
|
|
1077
|
+
case "price_change_notice": return true;
|
|
1078
|
+
case "invoice_payment_due": return facts.invoicePaymentStatus === "unpaid";
|
|
1079
|
+
case "bank_transfer_instructions": return facts.transferPending === true;
|
|
1080
|
+
case "payment_failed": return sub?.status === "past_due";
|
|
1081
|
+
case "cancel_scheduled": return sub?.cancelAtPeriodEnd === true;
|
|
1082
|
+
case "subscription_ended": return facts.state.subscriptions !== void 0 ? sub === null : facts.state.subscription === null;
|
|
1083
|
+
case "access_suspended": return sub?.status === "suspended";
|
|
201
1084
|
case "invoice_issued": return facts.invoiceStatus === "issued";
|
|
202
1085
|
}
|
|
203
1086
|
}
|
|
@@ -256,6 +1139,10 @@ const SubscriptionSummarySchema = z.object({
|
|
|
256
1139
|
status: SubscriptionStatusSchema,
|
|
257
1140
|
planCode: CodeSchema,
|
|
258
1141
|
interval: IntervalSchema,
|
|
1142
|
+
/**
|
|
1143
|
+
* Amount the subscription pays per period. Billing core (M36): the subscription's LOCKED amount
|
|
1144
|
+
* (copied when it started); a later in-place price change does not alter it.
|
|
1145
|
+
*/
|
|
259
1146
|
amountMinor: AmountMinorSchema,
|
|
260
1147
|
currency: CurrencySchema,
|
|
261
1148
|
currentPeriodEnd: DateTimeSchema.nullable(),
|
|
@@ -275,11 +1162,16 @@ const AccountDunningStateSchema = z.object({
|
|
|
275
1162
|
/** Sıradaki dunning aksiyonunun zamanı; kalmadıysa null. */
|
|
276
1163
|
nextActionAt: DateTimeSchema.nullable()
|
|
277
1164
|
});
|
|
278
|
-
/**
|
|
1165
|
+
/**
|
|
1166
|
+
* Change applied at the period end (`POST /v1/subscriptions/:id/change`, MF9). Null until then.
|
|
1167
|
+
* `price: "current"` on the same plan shows the same `planCode`/`interval` with the new `amountMinor`.
|
|
1168
|
+
*/
|
|
279
1169
|
const AccountScheduledChangeSchema = z.object({
|
|
280
1170
|
planCode: CodeSchema,
|
|
281
1171
|
interval: IntervalSchema,
|
|
282
|
-
effectiveAt: DateTimeSchema
|
|
1172
|
+
effectiveAt: DateTimeSchema,
|
|
1173
|
+
/** Amount locked from `effectiveAt` (billing core; absent from older steward). */
|
|
1174
|
+
amountMinor: AmountMinorSchema.optional()
|
|
283
1175
|
});
|
|
284
1176
|
const AccountSubscriptionStateSchema = SubscriptionSummarySchema.extend({
|
|
285
1177
|
cancelAtPeriodEnd: z.boolean(),
|
|
@@ -288,7 +1180,10 @@ const AccountSubscriptionStateSchema = SubscriptionSummarySchema.extend({
|
|
|
288
1180
|
currentPeriodStart: DateTimeSchema.nullable(),
|
|
289
1181
|
/** Abonelik dunning'de değilse null. */
|
|
290
1182
|
dunning: AccountDunningStateSchema.nullable(),
|
|
291
|
-
/**
|
|
1183
|
+
/**
|
|
1184
|
+
* The pending plan/interval/price change (billing core MF9: `POST /v1/subscriptions/:id/change`,
|
|
1185
|
+
* the portal's Change plan, the operator's `reprice-subscriptions`); null when none.
|
|
1186
|
+
*/
|
|
292
1187
|
scheduledChange: AccountScheduledChangeSchema.nullable()
|
|
293
1188
|
});
|
|
294
1189
|
/** Hesabın açık (tamamlanmamış, süresi dolmamış) checkout oturumu. */
|
|
@@ -299,8 +1194,46 @@ const AccountOpenCheckoutSchema = z.object({
|
|
|
299
1194
|
intervalCount: z.number().int().min(1),
|
|
300
1195
|
currency: CurrencySchema,
|
|
301
1196
|
amountMinor: AmountMinorSchema,
|
|
302
|
-
expiresAt: DateTimeSchema
|
|
1197
|
+
expiresAt: DateTimeSchema,
|
|
1198
|
+
/**
|
|
1199
|
+
* Bank transfer plan (H6): `bank_transfer` when the session waits for the customer's transfer
|
|
1200
|
+
* until `expiresAt` (the product shows "payment pending" with `AccountState.bankTransferCode`).
|
|
1201
|
+
* Absent otherwise (a card checkout's state is unchanged).
|
|
1202
|
+
*/
|
|
1203
|
+
paymentMethod: z.enum(["card", "bank_transfer"]).optional()
|
|
1204
|
+
});
|
|
1205
|
+
/**
|
|
1206
|
+
* An unpaid invoice of the account (billing core, M22/M34): renewal or final invoices written
|
|
1207
|
+
* before payment. Account level (not under a subscription) so the final invoice of an ended
|
|
1208
|
+
* subscription stays visible; the product can show a banner before `dueAt`.
|
|
1209
|
+
*/
|
|
1210
|
+
const AccountOpenInvoiceSchema = z.object({
|
|
1211
|
+
id: IdSchema,
|
|
1212
|
+
/** Null for an invoice not tied to a subscription. */
|
|
1213
|
+
subscriptionId: IdSchema.nullable(),
|
|
1214
|
+
billingReason: InvoiceBillingReasonSchema,
|
|
1215
|
+
totalMinor: AmountMinorSchema,
|
|
1216
|
+
currency: CurrencySchema,
|
|
1217
|
+
dueAt: DateTimeSchema
|
|
303
1218
|
});
|
|
1219
|
+
/**
|
|
1220
|
+
* Low-churn meter facts of the account state (metering, M13; Polar `active_meters`, coarse). One
|
|
1221
|
+
* entry per catalog meter. Balances and period bounds are NOT here (they change with every event;
|
|
1222
|
+
* read them with `GET /v1/accounts/:ref/meters`): this block changes when the plan changes, a
|
|
1223
|
+
* threshold is crossed or a new period resets the standing — never per event.
|
|
1224
|
+
*/
|
|
1225
|
+
const AccountMeterStateSchema = z.object({
|
|
1226
|
+
/**
|
|
1227
|
+
* Units the account's `meter_credit` benefits grant per period (`meterCreditsOf`: benefits sum;
|
|
1228
|
+
* the default plan's credit only when no other layer credits the meter); `null`: no credit (not
|
|
1229
|
+
* gated by credit, M15).
|
|
1230
|
+
*/
|
|
1231
|
+
includedUnits: z.number().nonnegative().nullable(),
|
|
1232
|
+
/** A live subscription bills overage of this meter (metered rate): the gate stays open (M15) until the rate's cap is reached. */
|
|
1233
|
+
billable: z.boolean(),
|
|
1234
|
+
standing: MeterStandingSchema
|
|
1235
|
+
});
|
|
1236
|
+
const AccountMetersStateSchema = z.record(CodeSchema, AccountMeterStateSchema);
|
|
304
1237
|
/** Fatura profili özeti: yalnızca var mı ve türü (alanlar PII, burada yok). */
|
|
305
1238
|
const AccountProfileStateSchema = z.object({
|
|
306
1239
|
present: z.boolean(),
|
|
@@ -319,7 +1252,21 @@ const AccountStateSchema = EntitlementSnapshotSchema.extend({
|
|
|
319
1252
|
catalogVersion: z.string().nullable(),
|
|
320
1253
|
subscription: AccountSubscriptionStateSchema.nullable(),
|
|
321
1254
|
openCheckout: AccountOpenCheckoutSchema.nullable(),
|
|
322
|
-
profile: AccountProfileStateSchema
|
|
1255
|
+
profile: AccountProfileStateSchema,
|
|
1256
|
+
subscriptions: z.array(AccountSubscriptionStateSchema).optional(),
|
|
1257
|
+
openInvoices: z.array(AccountOpenInvoiceSchema).optional(),
|
|
1258
|
+
benefits: z.array(AccountBenefitSchema).optional(),
|
|
1259
|
+
/**
|
|
1260
|
+
* Metering phase 2 (M13): per catalog meter `{includedUnits, billable, standing}`. Present only
|
|
1261
|
+
* for a product whose catalog defines meters (the state of other products, and so its version
|
|
1262
|
+
* and fingerprint, stays exactly as before).
|
|
1263
|
+
*/
|
|
1264
|
+
meters: AccountMetersStateSchema.optional(),
|
|
1265
|
+
/**
|
|
1266
|
+
* Bank transfer plan (H4, H6): the account's order code the customer writes in the transfer
|
|
1267
|
+
* description (`XXXX-XXXX-X`); present once the account chose bank transfer, then permanent.
|
|
1268
|
+
*/
|
|
1269
|
+
bankTransferCode: z.string().optional()
|
|
323
1270
|
});
|
|
324
1271
|
const AccountSchema = z.object({
|
|
325
1272
|
ref: ExternalRefSchema,
|
|
@@ -349,6 +1296,11 @@ const ACCOUNT_INCLUDES = [
|
|
|
349
1296
|
const EventIdSchema = z.string().regex(/^evt_[0-9a-f-]{36}$/);
|
|
350
1297
|
const ApiVersionSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
|
351
1298
|
const sub = { subscriptionId: IdSchema };
|
|
1299
|
+
/** `benefit_grant.*` detail base: the grant log row id and the granted benefit (as in `AccountState.benefits[]`). */
|
|
1300
|
+
const benefitGrant = {
|
|
1301
|
+
grantId: IdSchema,
|
|
1302
|
+
benefit: AccountBenefitSchema
|
|
1303
|
+
};
|
|
352
1304
|
const EVENT_DETAIL_SCHEMAS = {
|
|
353
1305
|
"account.updated": z.object({
|
|
354
1306
|
/** Snapshot'ın hangi alanları değişti (planCode, accessState, entitlements, subscription). */
|
|
@@ -368,7 +1320,13 @@ changed: z.array(z.string()) }),
|
|
|
368
1320
|
interval: IntervalSchema,
|
|
369
1321
|
amountMinor: AmountMinorSchema,
|
|
370
1322
|
currency: CurrencySchema,
|
|
371
|
-
actorRef: z.string().nullable()
|
|
1323
|
+
actorRef: z.string().nullable(),
|
|
1324
|
+
/**
|
|
1325
|
+
* Upgrade (bank transfer plan H16): the subscription this one replaced — canceled immediately in
|
|
1326
|
+
* the same transaction (`cancelReason` `replaced_by_checkout`, no `subscription.canceled` of its
|
|
1327
|
+
* own). Absent when the checkout replaced nothing.
|
|
1328
|
+
*/
|
|
1329
|
+
replacedSubscriptionId: IdSchema.optional()
|
|
372
1330
|
}),
|
|
373
1331
|
"subscription.renewed": z.object({
|
|
374
1332
|
...sub,
|
|
@@ -382,7 +1340,9 @@ changed: z.array(z.string()) }),
|
|
|
382
1340
|
paymentId: IdSchema.nullable(),
|
|
383
1341
|
/** Dunning'in kaçıncı başarısız denemesi (1'den başlar). */
|
|
384
1342
|
attempt: z.number().int().positive(),
|
|
385
|
-
nextRetryAt: DateTimeSchema.nullable()
|
|
1343
|
+
nextRetryAt: DateTimeSchema.nullable(),
|
|
1344
|
+
/** Billing core (M23): the unpaid invoice past its due date that started dunning; absent on the legacy provider path. */
|
|
1345
|
+
invoiceId: IdSchema.optional()
|
|
386
1346
|
}),
|
|
387
1347
|
"subscription.suspended": z.object({
|
|
388
1348
|
...sub,
|
|
@@ -439,6 +1399,51 @@ changed: z.array(z.string()) }),
|
|
|
439
1399
|
"account.deleted": z.object({
|
|
440
1400
|
deletedAt: DateTimeSchema,
|
|
441
1401
|
actorRef: z.string().nullable()
|
|
1402
|
+
}),
|
|
1403
|
+
"invoice.paid": z.object({
|
|
1404
|
+
invoiceId: IdSchema,
|
|
1405
|
+
subscriptionId: IdSchema.nullable(),
|
|
1406
|
+
paymentId: IdSchema,
|
|
1407
|
+
/** Null for an invoice written the pre-billing-core way (after a payment). */
|
|
1408
|
+
billingReason: InvoiceBillingReasonSchema.nullable(),
|
|
1409
|
+
totalMinor: AmountMinorSchema,
|
|
1410
|
+
currency: CurrencySchema,
|
|
1411
|
+
paidAt: DateTimeSchema
|
|
1412
|
+
}),
|
|
1413
|
+
"benefit_grant.created": z.object({ ...benefitGrant }),
|
|
1414
|
+
"benefit_grant.updated": z.object({
|
|
1415
|
+
...benefitGrant,
|
|
1416
|
+
previousProperties: z.record(z.string(), z.unknown())
|
|
1417
|
+
}),
|
|
1418
|
+
"benefit_grant.revoked": z.object({
|
|
1419
|
+
...benefitGrant,
|
|
1420
|
+
revokedAt: DateTimeSchema
|
|
1421
|
+
}),
|
|
1422
|
+
"benefit_grant.cycled": z.object({
|
|
1423
|
+
...benefitGrant,
|
|
1424
|
+
periodStart: DateTimeSchema,
|
|
1425
|
+
periodEnd: DateTimeSchema
|
|
1426
|
+
}),
|
|
1427
|
+
"meter.threshold_crossed": z.object({
|
|
1428
|
+
meter: CodeSchema,
|
|
1429
|
+
standing: MeterStandingSchema,
|
|
1430
|
+
previous: MeterStandingSchema,
|
|
1431
|
+
periodStart: DateTimeSchema,
|
|
1432
|
+
periodEnd: DateTimeSchema,
|
|
1433
|
+
consumedUnits: z.number().nonnegative(),
|
|
1434
|
+
creditedUnits: z.number().nonnegative()
|
|
1435
|
+
}),
|
|
1436
|
+
"meter.period_closed": z.object({
|
|
1437
|
+
periodStart: DateTimeSchema,
|
|
1438
|
+
periodEnd: DateTimeSchema,
|
|
1439
|
+
subscriptionId: IdSchema.nullable(),
|
|
1440
|
+
meters: z.array(z.object({
|
|
1441
|
+
meter: CodeSchema,
|
|
1442
|
+
consumedUnits: z.number().nonnegative(),
|
|
1443
|
+
creditedUnits: z.number().nonnegative(),
|
|
1444
|
+
overageUnits: z.number().nonnegative()
|
|
1445
|
+
})).min(1),
|
|
1446
|
+
meterChargeId: IdSchema.nullable()
|
|
442
1447
|
})
|
|
443
1448
|
};
|
|
444
1449
|
/** Tüm event tipleri (yenileri dahil). */
|
|
@@ -464,7 +1469,18 @@ const LEGACY_EVENT_TYPES = Object.freeze([
|
|
|
464
1469
|
"invoice.created",
|
|
465
1470
|
"invoice.issued"
|
|
466
1471
|
]);
|
|
467
|
-
|
|
1472
|
+
/** Event types whose `data.account` is the full `AccountState` (the others: the entitlement snapshot). */
|
|
1473
|
+
const FULL_STATE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1474
|
+
"account.state_changed",
|
|
1475
|
+
"account.deleted",
|
|
1476
|
+
"invoice.paid",
|
|
1477
|
+
"benefit_grant.created",
|
|
1478
|
+
"benefit_grant.updated",
|
|
1479
|
+
"benefit_grant.revoked",
|
|
1480
|
+
"benefit_grant.cycled",
|
|
1481
|
+
"meter.threshold_crossed",
|
|
1482
|
+
"meter.period_closed"
|
|
1483
|
+
]);
|
|
468
1484
|
function accountSchemaOf(type) {
|
|
469
1485
|
return FULL_STATE_EVENT_TYPES.has(type) ? AccountStateSchema : EntitlementSnapshotSchema;
|
|
470
1486
|
}
|
|
@@ -515,4 +1531,4 @@ const PingSchema = z.object({
|
|
|
515
1531
|
endpointId: IdSchema
|
|
516
1532
|
});
|
|
517
1533
|
//#endregion
|
|
518
|
-
export {
|
|
1534
|
+
export { emailLocaleOf as $, MeteredPriceSchema as $t, SubscriptionStatusSchema as A, hasControlCharacters as An, METER_FILTER_MAX_CONDITIONS as At, EmailSenderSchema as B, EntitlementsSchema as Bn, MeterCreditEntryKindSchema as Bt, AccountSchema as C, EventNameSchema as Cn, DEFAULT_METER_LOW_AT as Ct, BillingProfileKindSchema as D, IngestEventSchema as Dn, METER_AGGREGATION_KINDS as Dt, AccountUpsertInputSchema as E, INGEST_REJECT_CODES as En, MAX_METER_UNITS as Et, EMAIL_LOCALES as F, CodeSchema as Fn, MeterAggregationSchema as Ft, InvoicePaymentDueEmailSchema as G, INVOICE_PAYMENT_STATUSES as Gn, MeterLabelSchema as Gt, EmailSettingsSchema as H, ErrorResponseSchema as Hn, MeterCreditPropertiesSchema as Ht, EMAIL_PAYLOAD_SCHEMAS as I, CurrencySchema as In, MeterChargeLineSchema as It, NotificationEmailsSchema as J, InvoiceBillingReasonSchema as Jn, MeterPeriodSchema as Jt, MAX_NOTIFICATION_EMAILS as K, IdSchema as Kn, MeterLowAtSchema as Kt, EMAIL_TEMPLATES as L, DateTimeSchema as Ln, MeterChargeListSchema as Lt, BILLING_PATH_PLACEHOLDERS as M, API_VERSION as Mn, METER_FILTER_OPERATORS as Mt, BankTransferInstructionsEmailSchema as N, ActorRefSchema as Nn, METER_PERIOD_STATUSES as Nt, BillingProfileSchema as O, IngestRejectCodeSchema as On, METER_CHARGE_STATUSES as Ot, BillingPathSchema as P, AmountMinorSchema as Pn, METER_STANDINGS as Pt, billingPageUrl as Q, MeteredPriceDefSchema as Qt, EMAIL_TEMPLATE_BY_EVENT as R, ERROR_CATEGORIES as Rn, MeterChargeSchema as Rt, AccountScheduledChangeSchema as S, EventMetadataValueSchema as Sn, CreditGrantInputSchema as St, AccountSubscriptionStateSchema as T, EventsIngestResultSchema as Tn, MAX_METERED_PRICE_PER_UNITS as Tt, EmailTemplateSchema as U, ExternalRefSchema as Un, MeterDefSchema as Ut, EmailSettingsPatchSchema as V, ErrorCategorySchema as Vn, MeterCreditEntrySchema as Vt, EmailTemplateSettingsSchema as W, INVOICE_BILLING_REASONS as Wn, MeterFilterSchema as Wt, PriceChangeNoticeMeteredRateSchema as X, MetadataSchema as Xn, MeterStandingSchema as Xt, PriceChangeNoticeEmailSchema as Y, InvoicePaymentStatusSchema as Yn, MeterPeriodStatusSchema as Yt, applyEmailSettingsPatch as Z, MeterUnitsSchema as Zt, AccountMeterStateSchema as _, EVENT_TIMESTAMP_MAX_AGE_DAYS as _n, isFeatureFlagBenefit as _t, EVENT_TYPES as a, fromMicroUnits as an, uniqueEmails as at, AccountOpenInvoiceSchema as b, EventMetadataKeySchema as bn, AccountMetersSchema as bt, LEGACY_EVENT_TYPES as c, parseMicroUnits as cn, BenefitDefSchema as ct, isEventType as d, EVENTS_INGEST_MAX_BYTES as dn, BenefitTypeSchema as dt, canonicalMeterFilter as en, emailRecipients as et, ACCOUNT_INCLUDES as f, EVENTS_INGEST_MAX_EVENTS as fn, CatalogBenefitSchema as ft, AccountLocaleSchema as g, EVENT_NAME_MAX_LENGTH as gn, featureValueMatchesType as gt, AccountDunningStateSchema as h, EVENT_METADATA_STRING_MAX_LENGTH as hn, MeterCreditBenefitDefSchema as ht, EVENT_DETAIL_SCHEMAS as i, UNIT_DECIMALS as in, liveSubscriptionOf as it, SubscriptionSummarySchema as j, isEventTimestampInRange as jn, METER_FILTER_MAX_DEPTH as jt, EntitlementSnapshotSchema as k, IngestRejectionSchema as kn, METER_CREDIT_ENTRY_KINDS as kt, PING_EVENT_TYPE as l, plainDecimalOf as ln, BenefitDescriptionSchema as lt, AccountDeleteResponseSchema as m, EVENT_METADATA_MAX_PAIRS as mn, FeatureFlagPropertiesSchema as mt, ApiVersionSchema as n, InvalidUnitValueError as nn, emailTemplateSettings as nt, EventIdSchema as o, isMicroPrecise as on, AccountBenefitSchema as ot, AccessStateSchema as p, EVENT_METADATA_KEY_MAX_LENGTH as pn, FeatureFlagBenefitDefSchema as pt, NoticedMeteredRateSchema as q, IntervalSchema as qn, MeterPeriodListSchema as qt, BillingEventSchema as r, MICRO_UNITS_PER_UNIT as rn, isEmailRelevant as rt, FULL_STATE_EVENT_TYPES as s, microToDecimalString as sn, BENEFIT_TYPES as st, AnyBillingEventSchema as t, meterFilterClauseOf as tn, emailTemplateOf as tt, PingSchema as u, toMicroUnits as un, BenefitSourceSchema as ut, AccountMetersStateSchema as v, EVENT_TIMESTAMP_MAX_SKEW_MINUTES as vn, isMeterCreditBenefit as vt, AccountStateSchema as w, EventsIngestInputSchema as wn, DecimalUnitsStringSchema as wt, AccountProfileStateSchema as x, EventMetadataSchema as xn, CatalogMeterSchema as xt, AccountOpenCheckoutSchema as y, EventExternalIdSchema as yn, AccountMeterSchema as yt, EmailExtraTextSchema as z, EntitlementValueSchema as zn, MeterChargeStatusSchema as zt };
|