@happyvertical/smrt-subscriptions 0.57.8 → 0.57.10
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/AGENTS.md +21 -0
- package/README.md +112 -3
- package/dist/chunks/SubscriptionPlanCollection-CqlYYtPg.js +229 -0
- package/dist/chunks/SubscriptionPlanCollection-CqlYYtPg.js.map +1 -0
- package/dist/chunks/SubscriptionPlanVersionCollection-CV0QPmp0.js +193 -0
- package/dist/chunks/SubscriptionPlanVersionCollection-CV0QPmp0.js.map +1 -0
- package/dist/chunks/plan-terms-BoeXvDhs.js +219 -0
- package/dist/chunks/plan-terms-BoeXvDhs.js.map +1 -0
- package/dist/chunks/plan-versioning-Bp0NqTJf.js +142 -0
- package/dist/chunks/plan-versioning-Bp0NqTJf.js.map +1 -0
- package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/collections/SubscriptionPlanVersionCollection.d.ts +33 -0
- package/dist/collections/SubscriptionPlanVersionCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +181 -349
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1114 -328
- package/dist/migrations/planVersions.d.ts +16 -0
- package/dist/migrations/planVersions.d.ts.map +1 -0
- package/dist/models/SubscriptionPlan.d.ts +39 -3
- package/dist/models/SubscriptionPlan.d.ts.map +1 -1
- package/dist/models/SubscriptionPlanVersion.d.ts +51 -0
- package/dist/models/SubscriptionPlanVersion.d.ts.map +1 -0
- package/dist/models/TenantSubscription.d.ts +49 -1
- package/dist/models/TenantSubscription.d.ts.map +1 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/plan-terms.d.ts +26 -0
- package/dist/plan-terms.d.ts.map +1 -0
- package/dist/plan-versioning.d.ts +45 -0
- package/dist/plan-versioning.d.ts.map +1 -0
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/subscription-resolver.d.ts +26 -0
- package/dist/services/subscription-resolver.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +547 -7
- package/dist/types.d.ts +72 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubscriptionPlanVersionCollection-CV0QPmp0.js","names":[],"sources":["../../src/models/SubscriptionPlanVersion.ts","../../src/collections/SubscriptionPlanVersionCollection.ts"],"sourcesContent":["import { foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport {\n TenantScoped,\n tenantId,\n withSystemContext,\n} from '@happyvertical/smrt-tenancy';\nimport {\n enabledFeatureKeys,\n extractPlanTerms,\n validThresholds,\n} from '../plan-terms.js';\nimport type {\n BillingInterval,\n PlanFeatureGrant,\n PlanTerms,\n PlanThreshold,\n PricingMode,\n SubscriptionPlanStatus,\n SubscriptionPlanVersionOptions,\n} from '../types.js';\nimport { normalizeFeatureGrants, parseJsonArray } from '../utils.js';\n\nconst PRICING_MODES: PricingMode[] = ['free', 'fixed', 'pay_what_you_can'];\n\n/**\n * An immutable, effective-dated snapshot of a plan's terms (#4032).\n *\n * `SubscriptionPlan` is the stable tier identity; each change to its terms\n * (status, price, pricing configuration, feature grants, thresholds) publishes\n * the next version. A version is in force from `effectiveFrom` until the next\n * version's `effectiveFrom`; before its first version's `effectiveFrom` a plan\n * resolves to that first version. Subscriptions pin the version in force when\n * their period starts, so a later amendment never changes a paid period.\n *\n * Versions are append-only: updates and deletes are refused.\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: '_smrt_subscription_plan_versions',\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n // plan_id is globally unique, so this key needs no (nullable) tenant_id.\n conflictColumns: ['plan_id', 'version'],\n hooks: { beforeDelete: 'refuseDelete' },\n})\nexport class SubscriptionPlanVersion extends SmrtObject implements PlanTerms { static __smrtPackage__ = \"@happyvertical/smrt-subscriptions\";\n /** Always the owning plan's tenant (null for a legacy global plan). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The stable tier identity this version belongs to. */\n @foreignKey('SubscriptionPlan')\n planId: string = '';\n\n /** The plan's natural key, copied for auditing and resolution output. */\n planKey: string = '';\n /** 1-based sequence within the plan; contiguous and monotonic. */\n version: number = 0;\n /** Start of this version's effective interval. */\n effectiveFrom: Date = new Date();\n /** Plan name when this version was published (display only). */\n name: string = '';\n status: SubscriptionPlanStatus = 'active';\n /** Integer minor units of {@link currency}. */\n priceAmount: number = 0;\n currency: string = 'USD';\n billingInterval: BillingInterval = 'month';\n pricingMode: PricingMode = 'fixed';\n /** Integer minor units. */\n minimumAmount: number = 0;\n /** Integer minor units. */\n suggestedAmount: number = 0;\n features: string = '[]';\n thresholds: string = '[]';\n\n constructor(options: SubscriptionPlanVersionOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.planId !== undefined) this.planId = options.planId;\n if (options.planKey !== undefined) this.planKey = options.planKey;\n if (options.version !== undefined) this.version = options.version;\n if (options.effectiveFrom !== undefined)\n this.effectiveFrom = options.effectiveFrom;\n if (options.name !== undefined) this.name = options.name;\n if (options.status !== undefined) this.status = options.status;\n if (options.priceAmount !== undefined)\n this.priceAmount = options.priceAmount;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.billingInterval !== undefined)\n this.billingInterval = options.billingInterval;\n if (options.pricingMode !== undefined)\n this.pricingMode = options.pricingMode;\n if (options.minimumAmount !== undefined)\n this.minimumAmount = options.minimumAmount;\n if (options.suggestedAmount !== undefined)\n this.suggestedAmount = options.suggestedAmount;\n if (options.features !== undefined) this.features = options.features;\n if (options.thresholds !== undefined) this.thresholds = options.thresholds;\n }\n\n isActive(): boolean {\n return this.status === 'active';\n }\n\n getTerms(): PlanTerms {\n return extractPlanTerms(this);\n }\n\n getFeatureGrants(): PlanFeatureGrant[] {\n return normalizeFeatureGrants(\n parseJsonArray<string | PlanFeatureGrant>(this.features),\n );\n }\n\n getFeatureKeys(): string[] {\n return enabledFeatureKeys(this);\n }\n\n getThresholds(): PlanThreshold[] {\n return validThresholds(this);\n }\n\n /** Append-only: a version is corrected by publishing the next one. */\n protected refuseDelete(): void {\n throw new Error(\n 'Subscription plan versions are append-only; publish a new version instead of deleting.',\n );\n }\n\n protected async validateBeforeSave(): Promise<void> {\n await super.validateBeforeSave();\n if (this.isPersisted) {\n throw new Error(\n 'Subscription plan versions are immutable; publish a new version instead.',\n );\n }\n // A colliding (tenant, plan, version) must fail, never adopt the row.\n this.requireInsertOnSave();\n if (!Number.isSafeInteger(this.version) || this.version < 1) {\n throw new Error('Plan version must be a positive integer');\n }\n if (\n !(this.effectiveFrom instanceof Date) ||\n Number.isNaN(this.effectiveFrom.getTime())\n ) {\n throw new Error('Plan version effectiveFrom must be a valid date');\n }\n if (!PRICING_MODES.includes(this.pricingMode)) {\n throw new Error('Invalid pricingMode');\n }\n for (const [name, value] of [\n ['priceAmount', this.priceAmount],\n ['minimumAmount', this.minimumAmount],\n ['suggestedAmount', this.suggestedAmount],\n ] as const) {\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new Error(\n `${name} must be a non-negative integer minor-unit amount`,\n );\n }\n }\n const { SubscriptionPlanCollection } = await import(\n '../collections/SubscriptionPlanCollection.js'\n );\n const { SubscriptionPlanVersionCollection } = await import(\n '../collections/SubscriptionPlanVersionCollection.js'\n );\n // Ownership reads name the owner explicitly, so a system-context read\n // cannot cross a tenant: the tenancy save interceptor has already bound\n // this.tenantId to the caller's tenant.\n const owner = this.tenantId ?? null;\n const [plan, latest] = await withSystemContext(async () => {\n const plans = await SubscriptionPlanCollection.create({ db: this.db });\n const versions = await SubscriptionPlanVersionCollection.create({\n db: this.db,\n });\n return Promise.all([\n plans.get({ id: this.planId, tenantId: owner }, { cache: false }),\n versions.findLatest(this.planId, owner),\n ]);\n });\n if (!plan || plan.planKey !== this.planKey) {\n throw new Error('Plan version must belong to an existing plan');\n }\n const expected = (latest?.version ?? 0) + 1;\n if (this.version !== expected) {\n throw new Error(`Next version of plan ${plan.planKey} is ${expected}`);\n }\n if (\n latest &&\n this.effectiveFrom.getTime() < latest.effectiveFrom.getTime()\n ) {\n throw new Error(\n `Plan ${plan.planKey} version ${latest.version} is effective from ` +\n `${latest.effectiveFrom.toISOString()}; a later version cannot start earlier`,\n );\n }\n }\n}\n\nexport default SubscriptionPlanVersion;\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport {\n getTenantId,\n isSystemContext,\n withSystemContext,\n} from '@happyvertical/smrt-tenancy';\nimport { SubscriptionPlanVersion } from '../models/SubscriptionPlanVersion.js';\n\n/**\n * Effective-dated plan versions (#4032). Reads follow the caller's tenancy;\n * a tenant caller additionally sees the versions of legacy global plans\n * (`tenantId: null`), the same bounded fallback subscriptions use for plans.\n */\nexport class SubscriptionPlanVersionCollection extends SmrtCollection<SubscriptionPlanVersion> {\n static readonly _itemClass = SubscriptionPlanVersion;\n\n /**\n * One version by id, through the caller's tenancy plus the bounded\n * fallback to legacy global plans' versions.\n */\n async findVisibleById(id: string): Promise<SubscriptionPlanVersion | null> {\n const rows = await this.withGlobalFallback((where) =>\n this.list({ where: { ...where, id }, limit: 1 }),\n );\n return rows[0] ?? null;\n }\n\n /** Every version of a plan, oldest first. */\n async listForPlan(planId: string): Promise<SubscriptionPlanVersion[]> {\n return this.withGlobalFallback((where) =>\n this.list({ where: { ...where, planId }, orderBy: 'version ASC' }),\n );\n }\n\n /**\n * The most recently published version of a plan. Pass `tenantId` to name\n * the owner explicitly (`null` for a global plan); omit it to read through\n * the caller's tenancy.\n */\n async findLatest(\n planId: string,\n tenantId?: string | null,\n ): Promise<SubscriptionPlanVersion | null> {\n const read = (where: Record<string, unknown>) =>\n this.list({\n where: { ...where, planId },\n orderBy: 'version DESC',\n limit: 1,\n });\n const rows =\n tenantId === undefined\n ? await this.withGlobalFallback(read)\n : await read({ tenantId });\n return rows[0] ?? null;\n }\n\n /**\n * The version of a plan in force at `at`: the latest version whose\n * `effectiveFrom` is at or before `at`. An instant before the first\n * version resolves to the first version, so subscriptions that predate\n * versioning keep the terms they were sold. Returns null only when the plan\n * has no versions.\n */\n async findInForce(\n planId: string,\n at: Date,\n ): Promise<SubscriptionPlanVersion | null> {\n const versions = await this.listForPlan(planId);\n const time = at.getTime();\n let inForce: SubscriptionPlanVersion | null = versions[0] ?? null;\n for (const version of versions) {\n if (version.effectiveFrom.getTime() <= time) inForce = version;\n }\n return inForce;\n }\n\n private async withGlobalFallback(\n read: (\n where: Record<string, unknown>,\n ) => Promise<SubscriptionPlanVersion[]>,\n ): Promise<SubscriptionPlanVersion[]> {\n const scoped = await read({});\n if (scoped.length > 0 || isSystemContext() || !getTenantId()) {\n return scoped;\n }\n return withSystemContext(() => read({ tenantId: null }));\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
var THRESHOLD_WINDOWS = [
|
|
3
|
+
"day",
|
|
4
|
+
"week",
|
|
5
|
+
"month",
|
|
6
|
+
"year",
|
|
7
|
+
"rolling"
|
|
8
|
+
];
|
|
9
|
+
var THRESHOLD_ENFORCEMENTS = [
|
|
10
|
+
"observe",
|
|
11
|
+
"warn",
|
|
12
|
+
"block"
|
|
13
|
+
];
|
|
14
|
+
async function deterministicUuid(parts) {
|
|
15
|
+
const bytes = new TextEncoder().encode(JSON.stringify(parts));
|
|
16
|
+
const uuid = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)).slice(0, 16);
|
|
17
|
+
uuid[6] = uuid[6] & 15 | 80;
|
|
18
|
+
uuid[8] = uuid[8] & 63 | 128;
|
|
19
|
+
const hex = Array.from(uuid, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
20
|
+
return [
|
|
21
|
+
hex.slice(0, 8),
|
|
22
|
+
hex.slice(8, 12),
|
|
23
|
+
hex.slice(12, 16),
|
|
24
|
+
hex.slice(16, 20),
|
|
25
|
+
hex.slice(20)
|
|
26
|
+
].join("-");
|
|
27
|
+
}
|
|
28
|
+
function parseJsonArray(value, fallback = []) {
|
|
29
|
+
if (!value) return fallback;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(value);
|
|
32
|
+
return Array.isArray(parsed) ? parsed : fallback;
|
|
33
|
+
} catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parseJsonObject(value, fallback) {
|
|
38
|
+
if (!value) return fallback;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(value);
|
|
41
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback;
|
|
42
|
+
} catch {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function stringifyJson(value) {
|
|
47
|
+
return JSON.stringify(value ?? null);
|
|
48
|
+
}
|
|
49
|
+
function normalizeFeatureGrants(grants) {
|
|
50
|
+
return grants.map((grant) => typeof grant === "string" ? {
|
|
51
|
+
featureKey: grant,
|
|
52
|
+
enabled: true
|
|
53
|
+
} : grant);
|
|
54
|
+
}
|
|
55
|
+
function normalizeSubscriber(input) {
|
|
56
|
+
const rawExternalId = input.subscriberExternalId;
|
|
57
|
+
const externalId = rawExternalId === null || rawExternalId === void 0 ? "" : String(rawExternalId);
|
|
58
|
+
const rawKind = input.subscriberKind;
|
|
59
|
+
const kind = rawKind === null || rawKind === void 0 ? "tenant" : rawKind;
|
|
60
|
+
if (kind !== "tenant" && kind !== "external") throw new Error(`subscriberKind must be "tenant" or "external" (got ${JSON.stringify(rawKind)})`);
|
|
61
|
+
if (kind === "tenant") {
|
|
62
|
+
if (externalId !== "") throw new Error("subscriberExternalId is set but subscriberKind is \"tenant\"; set subscriberKind=\"external\" explicitly or omit subscriberExternalId");
|
|
63
|
+
return {
|
|
64
|
+
kind: "tenant",
|
|
65
|
+
tenantId: input.tenantId
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (externalId === "") throw new Error("subscriberKind=\"external\" requires a non-empty subscriberExternalId");
|
|
69
|
+
return {
|
|
70
|
+
kind: "external",
|
|
71
|
+
tenantId: input.tenantId,
|
|
72
|
+
externalId
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function subscriberToColumns(subscriber) {
|
|
76
|
+
if (subscriber.kind === "tenant") return {
|
|
77
|
+
tenantId: subscriber.tenantId,
|
|
78
|
+
subscriberKind: "tenant",
|
|
79
|
+
subscriberExternalId: ""
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
tenantId: subscriber.tenantId,
|
|
83
|
+
subscriberKind: "external",
|
|
84
|
+
subscriberExternalId: subscriber.externalId
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function assertSubscriberInvariant(modelName, fields) {
|
|
88
|
+
if (fields.subscriberKind !== "tenant" && fields.subscriberKind !== "external") throw new Error(`${modelName}: subscriberKind must be "tenant" or "external" (got ${describe(fields.subscriberKind)})`);
|
|
89
|
+
if (typeof fields.subscriberExternalId !== "string") throw new Error(`${modelName}: subscriberExternalId must be a string (got ${describe(fields.subscriberExternalId)})`);
|
|
90
|
+
if (fields.subscriberKind === "tenant" && fields.subscriberExternalId !== "") throw new Error(`${modelName}: subscriberExternalId must be empty when subscriberKind is "tenant"`);
|
|
91
|
+
if (fields.subscriberKind === "external" && fields.subscriberExternalId === "") throw new Error(`${modelName}: subscriberKind="external" requires a non-empty subscriberExternalId`);
|
|
92
|
+
}
|
|
93
|
+
function describe(value) {
|
|
94
|
+
if (value === null) return "null";
|
|
95
|
+
if (value === void 0) return "undefined";
|
|
96
|
+
if (typeof value === "string") return `"${value}"`;
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
function getWindowForThreshold(thresholdWindow, now = /* @__PURE__ */ new Date()) {
|
|
100
|
+
switch (thresholdWindow) {
|
|
101
|
+
case "day": return utcWindow(now, "day");
|
|
102
|
+
case "week": return utcWindow(now, "week");
|
|
103
|
+
case "month": return utcWindow(now, "month");
|
|
104
|
+
case "year": return utcWindow(now, "year");
|
|
105
|
+
case "rolling": return {
|
|
106
|
+
start: /* @__PURE__ */ new Date(now.getTime() - 720 * 60 * 60 * 1e3),
|
|
107
|
+
end: now
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function getWindowKey(window) {
|
|
112
|
+
return `${window.start.toISOString()}..${window.end.toISOString()}`;
|
|
113
|
+
}
|
|
114
|
+
function isValidThreshold(threshold) {
|
|
115
|
+
return typeof threshold.metricKey === "string" && threshold.metricKey.length > 0 && Number.isFinite(threshold.limit) && threshold.limit >= 0 && isThresholdWindow(threshold.window) && isThresholdEnforcement(threshold.enforcement) && (threshold.warningRatio === void 0 || Number.isFinite(threshold.warningRatio) && threshold.warningRatio >= 0 && threshold.warningRatio <= 1);
|
|
116
|
+
}
|
|
117
|
+
function isThresholdWindow(value) {
|
|
118
|
+
return typeof value === "string" && THRESHOLD_WINDOWS.includes(value);
|
|
119
|
+
}
|
|
120
|
+
function isThresholdEnforcement(value) {
|
|
121
|
+
return typeof value === "string" && THRESHOLD_ENFORCEMENTS.includes(value);
|
|
122
|
+
}
|
|
123
|
+
function utcWindow(now, unit) {
|
|
124
|
+
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
125
|
+
if (unit === "week") {
|
|
126
|
+
const day = start.getUTCDay();
|
|
127
|
+
const mondayOffset = day === 0 ? -6 : 1 - day;
|
|
128
|
+
start.setUTCDate(start.getUTCDate() + mondayOffset);
|
|
129
|
+
}
|
|
130
|
+
if (unit === "month") start.setUTCDate(1);
|
|
131
|
+
if (unit === "year") start.setUTCMonth(0, 1);
|
|
132
|
+
const end = new Date(start);
|
|
133
|
+
switch (unit) {
|
|
134
|
+
case "day":
|
|
135
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
136
|
+
break;
|
|
137
|
+
case "week":
|
|
138
|
+
end.setUTCDate(end.getUTCDate() + 7);
|
|
139
|
+
break;
|
|
140
|
+
case "month":
|
|
141
|
+
end.setUTCMonth(end.getUTCMonth() + 1);
|
|
142
|
+
break;
|
|
143
|
+
case "year":
|
|
144
|
+
end.setUTCFullYear(end.getUTCFullYear() + 1);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
start,
|
|
149
|
+
end
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function tenantKey(value) {
|
|
153
|
+
return String(value ?? "").toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/plan-terms.ts
|
|
157
|
+
var PLAN_TERM_FIELDS = [
|
|
158
|
+
"status",
|
|
159
|
+
"priceAmount",
|
|
160
|
+
"currency",
|
|
161
|
+
"billingInterval",
|
|
162
|
+
"pricingMode",
|
|
163
|
+
"minimumAmount",
|
|
164
|
+
"suggestedAmount",
|
|
165
|
+
"features",
|
|
166
|
+
"thresholds"
|
|
167
|
+
];
|
|
168
|
+
function extractPlanTerms(source) {
|
|
169
|
+
return {
|
|
170
|
+
status: source.status,
|
|
171
|
+
priceAmount: Number(source.priceAmount),
|
|
172
|
+
currency: source.currency,
|
|
173
|
+
billingInterval: source.billingInterval,
|
|
174
|
+
pricingMode: source.pricingMode,
|
|
175
|
+
minimumAmount: Number(source.minimumAmount),
|
|
176
|
+
suggestedAmount: Number(source.suggestedAmount),
|
|
177
|
+
features: canonicalFeatures(source.features),
|
|
178
|
+
thresholds: canonicalThresholds(source.thresholds)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function samePlanTerms(left, right) {
|
|
182
|
+
const a = extractPlanTerms(left);
|
|
183
|
+
const b = extractPlanTerms(right);
|
|
184
|
+
return PLAN_TERM_FIELDS.every((field) => a[field] === b[field]);
|
|
185
|
+
}
|
|
186
|
+
function enabledFeatureKeys(terms) {
|
|
187
|
+
return featureGrants(terms.features).filter((grant) => grant.enabled !== false).map((grant) => grant.featureKey);
|
|
188
|
+
}
|
|
189
|
+
function validThresholds(terms) {
|
|
190
|
+
return parseJsonArray(terms.thresholds).filter((threshold) => Boolean(threshold) && isValidThreshold(threshold));
|
|
191
|
+
}
|
|
192
|
+
var ENFORCEMENT_RANK = {
|
|
193
|
+
observe: 0,
|
|
194
|
+
warn: 1,
|
|
195
|
+
block: 2
|
|
196
|
+
};
|
|
197
|
+
function isEntitlementUpgrade(from, to) {
|
|
198
|
+
if (to.status !== "active") return false;
|
|
199
|
+
const granted = new Set(enabledFeatureKeys(to));
|
|
200
|
+
if (enabledFeatureKeys(from).some((key) => !granted.has(key))) return false;
|
|
201
|
+
const current = validThresholds(from);
|
|
202
|
+
return validThresholds(to).every((next) => {
|
|
203
|
+
const counterpart = current.find((threshold) => threshold.metricKey === next.metricKey && threshold.window === next.window);
|
|
204
|
+
return counterpart !== void 0 && next.limit >= counterpart.limit && ENFORCEMENT_RANK[next.enforcement] <= ENFORCEMENT_RANK[counterpart.enforcement];
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function featureGrants(value) {
|
|
208
|
+
return normalizeFeatureGrants(parseJsonArray(value));
|
|
209
|
+
}
|
|
210
|
+
function canonicalFeatures(value) {
|
|
211
|
+
return stringifyJson(featureGrants(value));
|
|
212
|
+
}
|
|
213
|
+
function canonicalThresholds(value) {
|
|
214
|
+
return stringifyJson(parseJsonArray(value));
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
export { subscriberToColumns as _, samePlanTerms as a, deterministicUuid as c, isValidThreshold as d, normalizeFeatureGrants as f, stringifyJson as g, parseJsonObject as h, isEntitlementUpgrade as i, getWindowForThreshold as l, parseJsonArray as m, enabledFeatureKeys as n, validThresholds as o, normalizeSubscriber as p, extractPlanTerms as r, assertSubscriberInvariant as s, PLAN_TERM_FIELDS as t, getWindowKey as u, tenantKey as v };
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=plan-terms-BoeXvDhs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-terms-BoeXvDhs.js","names":[],"sources":["../../src/utils.ts","../../src/plan-terms.ts"],"sourcesContent":["import type {\n JsonObject,\n PlanFeatureGrant,\n PlanThreshold,\n Subscriber,\n SubscriberKind,\n ThresholdEnforcement,\n ThresholdWindow,\n UsageWindow,\n} from './types.js';\n\nconst THRESHOLD_WINDOWS: ThresholdWindow[] = [\n 'day',\n 'week',\n 'month',\n 'year',\n 'rolling',\n];\nconst THRESHOLD_ENFORCEMENTS: ThresholdEnforcement[] = [\n 'observe',\n 'warn',\n 'block',\n];\n\nexport async function deterministicUuid(parts: string[]): Promise<string> {\n const bytes = new TextEncoder().encode(JSON.stringify(parts));\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));\n const uuid = digest.slice(0, 16);\n uuid[6] = (uuid[6] & 0x0f) | 0x50;\n uuid[8] = (uuid[8] & 0x3f) | 0x80;\n const hex = Array.from(uuid, (byte) =>\n byte.toString(16).padStart(2, '0'),\n ).join('');\n return [\n hex.slice(0, 8),\n hex.slice(8, 12),\n hex.slice(12, 16),\n hex.slice(16, 20),\n hex.slice(20),\n ].join('-');\n}\n\nexport function parseJsonArray<T>(value: string, fallback: T[] = []): T[] {\n if (!value) {\n return fallback;\n }\n\n try {\n const parsed = JSON.parse(value) as unknown;\n return Array.isArray(parsed) ? (parsed as T[]) : fallback;\n } catch {\n return fallback;\n }\n}\n\nexport function parseJsonObject<T extends JsonObject>(\n value: string,\n fallback: T,\n): T {\n if (!value) {\n return fallback;\n }\n\n try {\n const parsed = JSON.parse(value) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as T)\n : fallback;\n } catch {\n return fallback;\n }\n}\n\nexport function stringifyJson(value: unknown): string {\n return JSON.stringify(value ?? null);\n}\n\nexport function normalizeFeatureGrants(\n grants: Array<string | PlanFeatureGrant>,\n): PlanFeatureGrant[] {\n return grants.map((grant) =>\n typeof grant === 'string' ? { featureKey: grant, enabled: true } : grant,\n );\n}\n\n/**\n * Coerce inputs from the legacy tenant-only API into a {@link Subscriber}.\n *\n * Existing callers that pass only `tenantId` (with optional `subscriberKind` /\n * `subscriberExternalId` fields, e.g. on `RecordUsageOptions`) get normalized\n * into the discriminated union exactly once at the boundary. This is the only\n * place that contains \"if kind is omitted, default to tenant\" logic — every\n * other site works with a typed `Subscriber`.\n *\n * Throws when:\n * - `subscriberKind === 'external'` is requested without a non-empty\n * `subscriberExternalId` (the XOR invariant is the whole point), or\n * - the input carries a non-empty `subscriberExternalId` but the kind resolves\n * to `'tenant'` (silent re-scoping would write buyer-scoped usage as tenant\n * usage, which then disappears from external summaries).\n */\nexport function normalizeSubscriber(input: {\n tenantId: string;\n subscriberKind?: SubscriberKind | null;\n subscriberExternalId?: string | null;\n}): Subscriber {\n // Treat null/undefined external ids as \"absent\" — but only after the\n // discriminator-derived branch decides what to do. We never coerce null\n // into the persisted column because the invariant on the model is\n // re-checked at save time via assertSubscriberInvariant.\n const rawExternalId = input.subscriberExternalId;\n const externalIdAbsent =\n rawExternalId === null || rawExternalId === undefined;\n const externalId = externalIdAbsent ? '' : String(rawExternalId);\n\n const rawKind = input.subscriberKind;\n const kind: SubscriberKind =\n rawKind === null || rawKind === undefined ? 'tenant' : rawKind;\n\n if (kind !== 'tenant' && kind !== 'external') {\n throw new Error(\n `subscriberKind must be \"tenant\" or \"external\" (got ${JSON.stringify(rawKind)})`,\n );\n }\n\n if (kind === 'tenant') {\n if (externalId !== '') {\n throw new Error(\n 'subscriberExternalId is set but subscriberKind is \"tenant\"; ' +\n 'set subscriberKind=\"external\" explicitly or omit subscriberExternalId',\n );\n }\n return { kind: 'tenant', tenantId: input.tenantId };\n }\n if (externalId === '') {\n throw new Error(\n 'subscriberKind=\"external\" requires a non-empty subscriberExternalId',\n );\n }\n return {\n kind: 'external',\n tenantId: input.tenantId,\n externalId,\n };\n}\n\n/**\n * Project a {@link Subscriber} back onto column values for persistence or query\n * filters. Counterpart to {@link normalizeSubscriber}.\n */\nexport function subscriberToColumns(subscriber: Subscriber): {\n tenantId: string;\n subscriberKind: SubscriberKind;\n subscriberExternalId: string;\n} {\n if (subscriber.kind === 'tenant') {\n return {\n tenantId: subscriber.tenantId,\n subscriberKind: 'tenant',\n subscriberExternalId: '',\n };\n }\n return {\n tenantId: subscriber.tenantId,\n subscriberKind: 'external',\n subscriberExternalId: subscriber.externalId,\n };\n}\n\n/**\n * Enforce the subscriber XOR invariant on a row's columns. Used by both the\n * model constructors (catches construction-time mistakes) and the\n * `validateBeforeSave` override (catches mutations applied via the\n * generated PUT/PATCH update path that bypass the constructor).\n *\n * Defensive about types because both call sites can receive JSON or untyped\n * fixture data via the generated REST/CLI surface — `null` arriving in place\n * of `''` would otherwise slip past an `=== ''` check, land in the\n * `(tenant_id, subscriber_kind, subscriber_external_id)` conflict key, and\n * (on Postgres) not collide with other NULLs while `findCurrentForSubscriber`\n * keeps querying by a string external id.\n *\n * @param modelName - Prepended to error messages so callers can tell whether\n * the failure originated in `TenantSubscription` or `TenantUsageMetric`.\n */\nexport function assertSubscriberInvariant(\n modelName: string,\n fields: {\n subscriberKind: unknown;\n subscriberExternalId: unknown;\n },\n): void {\n if (\n fields.subscriberKind !== 'tenant' &&\n fields.subscriberKind !== 'external'\n ) {\n throw new Error(\n `${modelName}: subscriberKind must be \"tenant\" or \"external\" (got ${describe(\n fields.subscriberKind,\n )})`,\n );\n }\n if (typeof fields.subscriberExternalId !== 'string') {\n throw new Error(\n `${modelName}: subscriberExternalId must be a string (got ${describe(\n fields.subscriberExternalId,\n )})`,\n );\n }\n\n if (\n fields.subscriberKind === 'tenant' &&\n fields.subscriberExternalId !== ''\n ) {\n throw new Error(\n `${modelName}: subscriberExternalId must be empty when subscriberKind is \"tenant\"`,\n );\n }\n if (\n fields.subscriberKind === 'external' &&\n fields.subscriberExternalId === ''\n ) {\n throw new Error(\n `${modelName}: subscriberKind=\"external\" requires a non-empty subscriberExternalId`,\n );\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return 'null';\n if (value === undefined) return 'undefined';\n if (typeof value === 'string') return `\"${value}\"`;\n return String(value);\n}\n\nexport function getWindowForThreshold(\n thresholdWindow: ThresholdWindow,\n now = new Date(),\n): UsageWindow {\n switch (thresholdWindow) {\n case 'day':\n return utcWindow(now, 'day');\n case 'week':\n return utcWindow(now, 'week');\n case 'month':\n return utcWindow(now, 'month');\n case 'year':\n return utcWindow(now, 'year');\n case 'rolling':\n return {\n start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),\n end: now,\n };\n }\n}\n\nexport function getWindowKey(window: UsageWindow): string {\n return `${window.start.toISOString()}..${window.end.toISOString()}`;\n}\n\nexport function isValidThreshold(threshold: PlanThreshold): boolean {\n return (\n typeof threshold.metricKey === 'string' &&\n threshold.metricKey.length > 0 &&\n Number.isFinite(threshold.limit) &&\n threshold.limit >= 0 &&\n isThresholdWindow(threshold.window) &&\n isThresholdEnforcement(threshold.enforcement) &&\n (threshold.warningRatio === undefined ||\n (Number.isFinite(threshold.warningRatio) &&\n threshold.warningRatio >= 0 &&\n threshold.warningRatio <= 1))\n );\n}\n\nfunction isThresholdWindow(value: unknown): value is ThresholdWindow {\n return (\n typeof value === 'string' &&\n THRESHOLD_WINDOWS.includes(value as ThresholdWindow)\n );\n}\n\nfunction isThresholdEnforcement(value: unknown): value is ThresholdEnforcement {\n return (\n typeof value === 'string' &&\n THRESHOLD_ENFORCEMENTS.includes(value as ThresholdEnforcement)\n );\n}\n\nfunction utcWindow(now: Date, unit: 'day' | 'week' | 'month' | 'year') {\n const start = new Date(\n Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()),\n );\n\n if (unit === 'week') {\n const day = start.getUTCDay();\n const mondayOffset = day === 0 ? -6 : 1 - day;\n start.setUTCDate(start.getUTCDate() + mondayOffset);\n }\n\n if (unit === 'month') {\n start.setUTCDate(1);\n }\n\n if (unit === 'year') {\n start.setUTCMonth(0, 1);\n }\n\n const end = new Date(start);\n switch (unit) {\n case 'day':\n end.setUTCDate(end.getUTCDate() + 1);\n break;\n case 'week':\n end.setUTCDate(end.getUTCDate() + 7);\n break;\n case 'month':\n end.setUTCMonth(end.getUTCMonth() + 1);\n break;\n case 'year':\n end.setUTCFullYear(end.getUTCFullYear() + 1);\n break;\n }\n\n return { start, end };\n}\n\n/** Canonical comparison key for a possibly-null hydrated tenant id. */\nexport function tenantKey(value: string | null | undefined): string {\n return String(value ?? '').toLowerCase();\n}\n","/**\n * Plan terms and the entitlement-upgrade policy for effective-dated plan\n * versions (#4032).\n */\nimport type {\n PlanFeatureGrant,\n PlanTerms,\n PlanThreshold,\n ThresholdEnforcement,\n} from './types.js';\nimport {\n isValidThreshold,\n normalizeFeatureGrants,\n parseJsonArray,\n stringifyJson,\n} from './utils.js';\n\n/** The versioned term fields, in a stable order. */\nexport const PLAN_TERM_FIELDS = [\n 'status',\n 'priceAmount',\n 'currency',\n 'billingInterval',\n 'pricingMode',\n 'minimumAmount',\n 'suggestedAmount',\n 'features',\n 'thresholds',\n] as const satisfies ReadonlyArray<keyof PlanTerms>;\n\n/** Copy the versioned terms off a plan or version. */\nexport function extractPlanTerms(source: PlanTerms): PlanTerms {\n return {\n status: source.status,\n priceAmount: Number(source.priceAmount),\n currency: source.currency,\n billingInterval: source.billingInterval,\n pricingMode: source.pricingMode,\n minimumAmount: Number(source.minimumAmount),\n suggestedAmount: Number(source.suggestedAmount),\n features: canonicalFeatures(source.features),\n thresholds: canonicalThresholds(source.thresholds),\n };\n}\n\n/** True when two term sets would grant and charge the same thing. */\nexport function samePlanTerms(left: PlanTerms, right: PlanTerms): boolean {\n const a = extractPlanTerms(left);\n const b = extractPlanTerms(right);\n return PLAN_TERM_FIELDS.every((field) => a[field] === b[field]);\n}\n\n/** Enabled feature keys of a term set. */\nexport function enabledFeatureKeys(terms: Pick<PlanTerms, 'features'>) {\n return featureGrants(terms.features)\n .filter((grant) => grant.enabled !== false)\n .map((grant) => grant.featureKey);\n}\n\n/** Valid thresholds of a term set. */\nexport function validThresholds(\n terms: Pick<PlanTerms, 'thresholds'>,\n): PlanThreshold[] {\n return parseJsonArray<PlanThreshold>(terms.thresholds).filter(\n (threshold) => Boolean(threshold) && isValidThreshold(threshold),\n );\n}\n\nconst ENFORCEMENT_RANK: Record<ThresholdEnforcement, number> = {\n observe: 0,\n warn: 1,\n block: 2,\n};\n\n/**\n * The in-period upgrade policy: `to` is an upgrade of `from` when it is\n * `active` and takes nothing away. Every feature enabled in `from` stays\n * enabled, and every threshold in `to` has a counterpart in `from` (same\n * metric and window) whose limit is no higher and whose enforcement is no\n * looser. Removing a threshold loosens it; adding one restricts. Price is not\n * an entitlement and is ignored. Anything else is a downgrade, which only\n * takes effect at renewal or through an explicit authorized replacement.\n */\nexport function isEntitlementUpgrade(from: PlanTerms, to: PlanTerms): boolean {\n if (to.status !== 'active') return false;\n const granted = new Set(enabledFeatureKeys(to));\n if (enabledFeatureKeys(from).some((key) => !granted.has(key))) return false;\n const current = validThresholds(from);\n return validThresholds(to).every((next) => {\n const counterpart = current.find(\n (threshold) =>\n threshold.metricKey === next.metricKey &&\n threshold.window === next.window,\n );\n return (\n counterpart !== undefined &&\n next.limit >= counterpart.limit &&\n ENFORCEMENT_RANK[next.enforcement] <=\n ENFORCEMENT_RANK[counterpart.enforcement]\n );\n });\n}\n\nfunction featureGrants(value: string): PlanFeatureGrant[] {\n return normalizeFeatureGrants(\n parseJsonArray<string | PlanFeatureGrant>(value),\n );\n}\n\nfunction canonicalFeatures(value: string): string {\n return stringifyJson(featureGrants(value));\n}\n\nfunction canonicalThresholds(value: string): string {\n return stringifyJson(parseJsonArray<PlanThreshold>(value));\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { a as samePlanTerms, i as isEntitlementUpgrade, r as extractPlanTerms } from "./plan-terms-BoeXvDhs.js";
|
|
3
|
+
import { t as SubscriptionPlanCollection } from "./SubscriptionPlanCollection-CqlYYtPg.js";
|
|
4
|
+
import { r as SubscriptionPlanVersion, t as SubscriptionPlanVersionCollection } from "./SubscriptionPlanVersionCollection-CV0QPmp0.js";
|
|
5
|
+
import { withSystemContext } from "@happyvertical/smrt-tenancy";
|
|
6
|
+
//#region src/plan-versioning.ts
|
|
7
|
+
var plan_versioning_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
periodBoundary: () => periodBoundary,
|
|
9
|
+
pinSubscriptionPlanVersion: () => pinSubscriptionPlanVersion,
|
|
10
|
+
publishPlanVersion: () => publishPlanVersion,
|
|
11
|
+
snapshotLegacyPlanVersion: () => snapshotLegacyPlanVersion
|
|
12
|
+
});
|
|
13
|
+
async function snapshotLegacyPlanVersion(plan) {
|
|
14
|
+
if (!plan.id) return false;
|
|
15
|
+
const versions = await SubscriptionPlanVersionCollection.create({ db: plan.db });
|
|
16
|
+
const owner = plan.tenantId ?? null;
|
|
17
|
+
if (await withSystemContext(() => versions.findLatest(String(plan.id), owner))) return false;
|
|
18
|
+
await createVersion(plan, plan, 1, persistedCreatedAt(plan));
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
async function publishPlanVersion(plan, effectiveFrom, prior) {
|
|
22
|
+
const versions = await SubscriptionPlanVersionCollection.create({ db: plan.db });
|
|
23
|
+
const owner = plan.tenantId ?? null;
|
|
24
|
+
let latest = await withSystemContext(() => versions.findLatest(String(plan.id), owner));
|
|
25
|
+
if (!latest && prior && String(prior.id) === String(plan.id)) latest = await createVersion(plan, prior, 1, persistedCreatedAt(prior));
|
|
26
|
+
if (latest && samePlanTerms(latest, plan)) return latest;
|
|
27
|
+
return createVersion(plan, plan, (latest?.version ?? 0) + 1, effectiveFrom);
|
|
28
|
+
}
|
|
29
|
+
async function createVersion(plan, terms, version, effectiveFrom) {
|
|
30
|
+
const record = new SubscriptionPlanVersion({
|
|
31
|
+
db: plan.db,
|
|
32
|
+
tenantId: plan.tenantId ?? null,
|
|
33
|
+
planId: String(plan.id),
|
|
34
|
+
planKey: plan.planKey,
|
|
35
|
+
name: plan.name,
|
|
36
|
+
version,
|
|
37
|
+
effectiveFrom,
|
|
38
|
+
...extractPlanTerms(terms)
|
|
39
|
+
});
|
|
40
|
+
await record.initialize();
|
|
41
|
+
await record.save();
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
function persistedCreatedAt(plan) {
|
|
45
|
+
const created = plan.created_at ? new Date(plan.created_at) : null;
|
|
46
|
+
return created && !Number.isNaN(created.getTime()) ? created : /* @__PURE__ */ new Date(0);
|
|
47
|
+
}
|
|
48
|
+
async function pinSubscriptionPlanVersion(subscription, persisted, replacement, now = /* @__PURE__ */ new Date()) {
|
|
49
|
+
const versions = await SubscriptionPlanVersionCollection.create({ db: subscription.db });
|
|
50
|
+
const boundary = periodBoundary(subscription);
|
|
51
|
+
const requested = subscription.planVersionId && subscription.planVersionId !== (persisted?.planVersionId ?? null) ? subscription.planVersionId : replacement?.planVersionId;
|
|
52
|
+
const authorizedReason = replacement?.policy === "authorized" ? replacement.reason?.trim() : "";
|
|
53
|
+
if (replacement?.policy === "authorized" && !authorizedReason) throw new Error("An authorized plan version replacement needs a reason");
|
|
54
|
+
const pin = (version, reason, note = "") => {
|
|
55
|
+
subscription.planVersionId = version?.id ? String(version.id) : null;
|
|
56
|
+
subscription.planVersionPinnedAt = version ? now : null;
|
|
57
|
+
subscription.planVersionPinReason = version ? reason : null;
|
|
58
|
+
subscription.planVersionPinNote = version ? note : "";
|
|
59
|
+
};
|
|
60
|
+
const load = async (id) => {
|
|
61
|
+
const version = await loadVersion(versions, id);
|
|
62
|
+
if (!version) throw new Error("Plan version is unavailable");
|
|
63
|
+
if (version.planId !== subscription.planId) throw new Error("Plan version does not belong to the subscription plan");
|
|
64
|
+
return version;
|
|
65
|
+
};
|
|
66
|
+
const atBoundary = async (reason) => {
|
|
67
|
+
const inForce = await versions.findInForce(subscription.planId, boundary);
|
|
68
|
+
if (requested && requested !== inForce?.id) {
|
|
69
|
+
const chosen = await load(requested);
|
|
70
|
+
if (!authorizedReason) throw new Error("A period pins the plan version in force at its start; pinning another version needs replacePlanVersion({ policy: 'authorized', reason }).");
|
|
71
|
+
pin(chosen, "authorized", authorizedReason);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (authorizedReason && inForce) {
|
|
75
|
+
pin(inForce, "authorized", authorizedReason);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
pin(inForce, reason);
|
|
79
|
+
};
|
|
80
|
+
if (!persisted) {
|
|
81
|
+
await atBoundary("period_start");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (isRenewal(persisted, boundary)) {
|
|
85
|
+
await atBoundary("renewal");
|
|
86
|
+
if (boundary.getTime() > now.getTime() && !authorizedReason && persisted.isEntitled(now) && (subscription.planVersionId !== persisted.planVersionId || subscription.planId !== persisted.planId)) {
|
|
87
|
+
const frozen2 = await frozenTerms(versions, persisted, periodBoundary(persisted));
|
|
88
|
+
const target2 = subscription.planVersionId ? await loadVersion(versions, subscription.planVersionId) : await (await SubscriptionPlanCollection.create({ db: subscription.db })).get({ id: subscription.planId }, { cache: false });
|
|
89
|
+
if (frozen2 && (!target2 || !isEntitlementUpgrade(frozen2, target2))) throw new Error("A renewal that changes plan terms can be saved once its period has started; the current paid period keeps its terms until then.");
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!(persisted.planId !== subscription.planId) && !requested && !replacement) {
|
|
94
|
+
if (!persisted.planVersionId) {
|
|
95
|
+
pin(await versions.findInForce(subscription.planId, boundary), "migration");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
subscription.planVersionId = persisted.planVersionId;
|
|
99
|
+
subscription.planVersionPinnedAt = persisted.planVersionPinnedAt;
|
|
100
|
+
subscription.planVersionPinReason = persisted.planVersionPinReason;
|
|
101
|
+
subscription.planVersionPinNote = persisted.planVersionPinNote;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const next = requested ? await load(requested) : await versions.findInForce(subscription.planId, now);
|
|
105
|
+
if (authorizedReason) {
|
|
106
|
+
pin(next, "authorized", authorizedReason);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!persisted.isEntitled(now)) {
|
|
110
|
+
pin(next, "period_start");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const target = next ?? await (await SubscriptionPlanCollection.create({ db: subscription.db })).get({ id: subscription.planId }, { cache: false });
|
|
114
|
+
const frozen = await frozenTerms(versions, persisted, boundary);
|
|
115
|
+
if (frozen && (!target || !isEntitlementUpgrade(frozen, target))) throw new Error("This plan change would remove entitlements from a paid period. It takes effect at renewal (a new period starting at or after currentPeriodEnd), or through replacePlanVersion({ policy: 'authorized', reason }).");
|
|
116
|
+
pin(next, "upgrade", replacement?.reason?.trim() ?? "");
|
|
117
|
+
}
|
|
118
|
+
function isRenewal(persisted, boundary) {
|
|
119
|
+
const previous = periodBoundary(persisted).getTime();
|
|
120
|
+
if (boundary.getTime() === previous) return false;
|
|
121
|
+
const end = persisted.currentPeriodEnd ? new Date(persisted.currentPeriodEnd).getTime() : null;
|
|
122
|
+
return end === null ? boundary.getTime() > previous : boundary.getTime() >= end;
|
|
123
|
+
}
|
|
124
|
+
async function frozenTerms(versions, persisted, boundary) {
|
|
125
|
+
if (persisted.planVersionId) {
|
|
126
|
+
const pinned = await loadVersion(versions, persisted.planVersionId);
|
|
127
|
+
if (pinned) return pinned;
|
|
128
|
+
}
|
|
129
|
+
const inForce = await versions.findInForce(persisted.planId, boundary);
|
|
130
|
+
if (inForce) return inForce;
|
|
131
|
+
return (await SubscriptionPlanCollection.create({ db: persisted.db })).get({ id: persisted.planId }, { cache: false });
|
|
132
|
+
}
|
|
133
|
+
async function loadVersion(versions, id) {
|
|
134
|
+
return versions.findVisibleById(id);
|
|
135
|
+
}
|
|
136
|
+
function periodBoundary(subscription) {
|
|
137
|
+
return new Date(subscription.currentPeriodStart ?? subscription.startedAt);
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { plan_versioning_exports as n, snapshotLegacyPlanVersion as r, pinSubscriptionPlanVersion as t };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=plan-versioning-Bp0NqTJf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-versioning-Bp0NqTJf.js","names":["frozen","target"],"sources":["../../src/plan-versioning.ts"],"sourcesContent":["/**\n * Publishing plan versions and pinning subscriptions to them (#4032).\n *\n * Kept out of the model files so `SubscriptionPlan` and `TenantSubscription`\n * share one implementation of the pin rules without importing each other.\n */\nimport { withSystemContext } from '@happyvertical/smrt-tenancy';\nimport { SubscriptionPlanCollection } from './collections/SubscriptionPlanCollection.js';\nimport { SubscriptionPlanVersionCollection } from './collections/SubscriptionPlanVersionCollection.js';\nimport type { SubscriptionPlan } from './models/SubscriptionPlan.js';\nimport { SubscriptionPlanVersion } from './models/SubscriptionPlanVersion.js';\nimport type { TenantSubscription } from './models/TenantSubscription.js';\nimport {\n extractPlanTerms,\n isEntitlementUpgrade,\n samePlanTerms,\n} from './plan-terms.js';\nimport type { PlanTerms, PlanVersionPinReason } from './types.js';\n\n/** An in-period replacement requested through `replacePlanVersion()`. */\nexport interface PlanVersionReplacement {\n policy: 'upgrade' | 'authorized';\n reason?: string;\n planVersionId?: string;\n}\n\n/**\n * Version 1 for a plan that predates versioning: its current (persisted)\n * terms, effective from the plan's creation. Returns false when the plan\n * already has versions.\n */\nexport async function snapshotLegacyPlanVersion(\n plan: SubscriptionPlan,\n): Promise<boolean> {\n if (!plan.id) return false;\n const versions = await SubscriptionPlanVersionCollection.create({\n db: plan.db,\n });\n const owner = plan.tenantId ?? null;\n const latest = await withSystemContext(() =>\n versions.findLatest(String(plan.id), owner),\n );\n if (latest) return false;\n await createVersion(plan, plan, 1, persistedCreatedAt(plan));\n return true;\n}\n\n/**\n * After a plan save, publish its terms as the next version when they differ\n * from the latest one (or when none exists). `prior` is the row the save\n * overwrote: when the plan had no versions yet, its terms become version 1,\n * effective from the plan's creation.\n */\nexport async function publishPlanVersion(\n plan: SubscriptionPlan,\n effectiveFrom: Date,\n prior?: SubscriptionPlan,\n): Promise<SubscriptionPlanVersion> {\n const versions = await SubscriptionPlanVersionCollection.create({\n db: plan.db,\n });\n const owner = plan.tenantId ?? null;\n let latest = await withSystemContext(() =>\n versions.findLatest(String(plan.id), owner),\n );\n if (!latest && prior && String(prior.id) === String(plan.id)) {\n latest = await createVersion(plan, prior, 1, persistedCreatedAt(prior));\n }\n if (latest && samePlanTerms(latest, plan)) return latest;\n return createVersion(plan, plan, (latest?.version ?? 0) + 1, effectiveFrom);\n}\n\nasync function createVersion(\n plan: SubscriptionPlan,\n terms: PlanTerms,\n version: number,\n effectiveFrom: Date,\n): Promise<SubscriptionPlanVersion> {\n const record = new SubscriptionPlanVersion({\n db: plan.db,\n tenantId: plan.tenantId ?? null,\n planId: String(plan.id),\n planKey: plan.planKey,\n name: plan.name,\n version,\n effectiveFrom,\n ...extractPlanTerms(terms),\n });\n await record.initialize();\n await record.save();\n return record;\n}\n\nfunction persistedCreatedAt(plan: SubscriptionPlan): Date {\n const created = plan.created_at ? new Date(plan.created_at) : null;\n return created && !Number.isNaN(created.getTime()) ? created : new Date(0);\n}\n\n/**\n * Bind a subscription to the plan version its current period paid for.\n * Called from `TenantSubscription.validateBeforeSave()`.\n *\n * - A new subscription, or a legacy row without a pin, pins the version in\n * force at its period start (`currentPeriodStart ?? startedAt`).\n * - A renewal re-pins the version in force at the new boundary, for the\n * (possibly changed) plan. A save is a renewal only when the new period\n * starts at or after the stored `currentPeriodEnd` (or, without one, after\n * the stored start); moving the start within a paid period is not.\n * - At creation and renewal the boundary decides the version: a caller may\n * name only that version, unless the save is an authorized replacement.\n * - Within a paid period, a different plan or version is accepted only when\n * it is an entitlement upgrade of the frozen terms, when the period no\n * longer entitles, or through an explicit authorized replacement.\n *\n * A plan without versions (an unmigrated legacy plan) leaves the pin empty;\n * resolution then falls back to the live plan.\n */\nexport async function pinSubscriptionPlanVersion(\n subscription: TenantSubscription,\n persisted: TenantSubscription | null,\n replacement: PlanVersionReplacement | undefined,\n now = new Date(),\n): Promise<void> {\n const versions = await SubscriptionPlanVersionCollection.create({\n db: subscription.db,\n });\n const boundary = periodBoundary(subscription);\n const requested =\n subscription.planVersionId &&\n subscription.planVersionId !== (persisted?.planVersionId ?? null)\n ? subscription.planVersionId\n : replacement?.planVersionId;\n const authorizedReason =\n replacement?.policy === 'authorized' ? replacement.reason?.trim() : '';\n if (replacement?.policy === 'authorized' && !authorizedReason) {\n throw new Error('An authorized plan version replacement needs a reason');\n }\n const pin = (\n version: SubscriptionPlanVersion | null,\n reason: PlanVersionPinReason,\n note = '',\n ) => {\n subscription.planVersionId = version?.id ? String(version.id) : null;\n subscription.planVersionPinnedAt = version ? now : null;\n subscription.planVersionPinReason = version ? reason : null;\n subscription.planVersionPinNote = version ? note : '';\n };\n const load = async (id: string) => {\n const version = await loadVersion(versions, id);\n if (!version) throw new Error('Plan version is unavailable');\n if (version.planId !== subscription.planId) {\n throw new Error('Plan version does not belong to the subscription plan');\n }\n return version;\n };\n // Creation and renewal: the version in force at the boundary.\n const atBoundary = async (reason: PlanVersionPinReason) => {\n const inForce = await versions.findInForce(subscription.planId, boundary);\n if (requested && requested !== inForce?.id) {\n const chosen = await load(requested);\n if (!authorizedReason) {\n throw new Error(\n 'A period pins the plan version in force at its start; pinning ' +\n \"another version needs replacePlanVersion({ policy: 'authorized', reason }).\",\n );\n }\n pin(chosen, 'authorized', authorizedReason);\n return;\n }\n if (authorizedReason && inForce) {\n pin(inForce, 'authorized', authorizedReason);\n return;\n }\n pin(inForce, reason);\n };\n\n if (!persisted) {\n await atBoundary('period_start');\n return;\n }\n if (isRenewal(persisted, boundary)) {\n await atBoundary('renewal');\n // Saved ahead of its start, a renewal must not change what the still\n // running paid period resolves to: allow it only when the pin is\n // unchanged, the change is an upgrade, or it was authorized.\n if (\n boundary.getTime() > now.getTime() &&\n !authorizedReason &&\n persisted.isEntitled(now) &&\n (subscription.planVersionId !== persisted.planVersionId ||\n subscription.planId !== persisted.planId)\n ) {\n const frozen = await frozenTerms(\n versions,\n persisted,\n periodBoundary(persisted),\n );\n const target: PlanTerms | null = subscription.planVersionId\n ? await loadVersion(versions, subscription.planVersionId)\n : await (\n await SubscriptionPlanCollection.create({ db: subscription.db })\n ).get({ id: subscription.planId }, { cache: false });\n if (frozen && (!target || !isEntitlementUpgrade(frozen, target))) {\n throw new Error(\n 'A renewal that changes plan terms can be saved once its period ' +\n 'has started; the current paid period keeps its terms until then.',\n );\n }\n }\n return;\n }\n const planChanged = persisted.planId !== subscription.planId;\n if (!planChanged && !requested && !replacement) {\n if (!persisted.planVersionId) {\n // A row written before versioning: pin what its period was sold under.\n pin(\n await versions.findInForce(subscription.planId, boundary),\n 'migration',\n );\n return;\n }\n // Nothing moved: keep the pin and its audit trail exactly as stored.\n subscription.planVersionId = persisted.planVersionId;\n subscription.planVersionPinnedAt = persisted.planVersionPinnedAt;\n subscription.planVersionPinReason = persisted.planVersionPinReason;\n subscription.planVersionPinNote = persisted.planVersionPinNote;\n return;\n }\n\n // An in-period change: the target is the named version, else the version\n // of the (new) plan in force now, else that plan's live terms.\n const next = requested\n ? await load(requested)\n : await versions.findInForce(subscription.planId, now);\n if (authorizedReason) {\n pin(next, 'authorized', authorizedReason);\n return;\n }\n if (!persisted.isEntitled(now)) {\n pin(next, 'period_start');\n return;\n }\n const target: PlanTerms | null =\n next ??\n (await (\n await SubscriptionPlanCollection.create({ db: subscription.db })\n ).get({ id: subscription.planId }, { cache: false }));\n const frozen = await frozenTerms(versions, persisted, boundary);\n if (frozen && (!target || !isEntitlementUpgrade(frozen, target))) {\n throw new Error(\n 'This plan change would remove entitlements from a paid period. It ' +\n 'takes effect at renewal (a new period starting at or after ' +\n \"currentPeriodEnd), or through replacePlanVersion({ policy: 'authorized', reason }).\",\n );\n }\n pin(next, 'upgrade', replacement?.reason?.trim() ?? '');\n}\n\n/**\n * Whether moving to `boundary` starts a new period: at or after the stored\n * period end, or — for an open-ended row — after the stored start.\n */\nfunction isRenewal(persisted: TenantSubscription, boundary: Date): boolean {\n const previous = periodBoundary(persisted).getTime();\n if (boundary.getTime() === previous) return false;\n const end = persisted.currentPeriodEnd\n ? new Date(persisted.currentPeriodEnd).getTime()\n : null;\n return end === null\n ? boundary.getTime() > previous\n : boundary.getTime() >= end;\n}\n\n/** The terms the persisted subscription's current period paid for. */\nasync function frozenTerms(\n versions: SubscriptionPlanVersionCollection,\n persisted: TenantSubscription,\n boundary: Date,\n): Promise<PlanTerms | null> {\n if (persisted.planVersionId) {\n const pinned = await loadVersion(versions, persisted.planVersionId);\n if (pinned) return pinned;\n }\n const inForce = await versions.findInForce(persisted.planId, boundary);\n if (inForce) return inForce;\n const plans = await SubscriptionPlanCollection.create({\n db: persisted.db,\n });\n return plans.get({ id: persisted.planId }, { cache: false });\n}\n\nasync function loadVersion(\n versions: SubscriptionPlanVersionCollection,\n id: string,\n): Promise<SubscriptionPlanVersion | null> {\n return versions.findVisibleById(id);\n}\n\n/** Start of a subscription's current period. */\nexport function periodBoundary(\n subscription: Pick<TenantSubscription, 'currentPeriodStart' | 'startedAt'>,\n): Date {\n return new Date(subscription.currentPeriodStart ?? subscription.startedAt);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SmrtCollection } from '@happyvertical/smrt-core';
|
|
2
|
+
import { SubscriptionPlanVersion } from '../models/SubscriptionPlanVersion.js';
|
|
3
|
+
/**
|
|
4
|
+
* Effective-dated plan versions (#4032). Reads follow the caller's tenancy;
|
|
5
|
+
* a tenant caller additionally sees the versions of legacy global plans
|
|
6
|
+
* (`tenantId: null`), the same bounded fallback subscriptions use for plans.
|
|
7
|
+
*/
|
|
8
|
+
export declare class SubscriptionPlanVersionCollection extends SmrtCollection<SubscriptionPlanVersion> {
|
|
9
|
+
static readonly _itemClass: typeof SubscriptionPlanVersion;
|
|
10
|
+
/**
|
|
11
|
+
* One version by id, through the caller's tenancy plus the bounded
|
|
12
|
+
* fallback to legacy global plans' versions.
|
|
13
|
+
*/
|
|
14
|
+
findVisibleById(id: string): Promise<SubscriptionPlanVersion | null>;
|
|
15
|
+
/** Every version of a plan, oldest first. */
|
|
16
|
+
listForPlan(planId: string): Promise<SubscriptionPlanVersion[]>;
|
|
17
|
+
/**
|
|
18
|
+
* The most recently published version of a plan. Pass `tenantId` to name
|
|
19
|
+
* the owner explicitly (`null` for a global plan); omit it to read through
|
|
20
|
+
* the caller's tenancy.
|
|
21
|
+
*/
|
|
22
|
+
findLatest(planId: string, tenantId?: string | null): Promise<SubscriptionPlanVersion | null>;
|
|
23
|
+
/**
|
|
24
|
+
* The version of a plan in force at `at`: the latest version whose
|
|
25
|
+
* `effectiveFrom` is at or before `at`. An instant before the first
|
|
26
|
+
* version resolves to the first version, so subscriptions that predate
|
|
27
|
+
* versioning keep the terms they were sold. Returns null only when the plan
|
|
28
|
+
* has no versions.
|
|
29
|
+
*/
|
|
30
|
+
findInForce(planId: string, at: Date): Promise<SubscriptionPlanVersion | null>;
|
|
31
|
+
private withGlobalFallback;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=SubscriptionPlanVersionCollection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubscriptionPlanVersionCollection.d.ts","sourceRoot":"","sources":["../../src/collections/SubscriptionPlanVersionCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAM1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAE/E;;;;GAIG;AACH,qBAAa,iCAAkC,SAAQ,cAAc,CAAC,uBAAuB,CAAC;IAC5F,MAAM,CAAC,QAAQ,CAAC,UAAU,iCAA2B;IAErD;;;OAGG;IACG,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;IAO1E,6CAA6C;IACvC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC;IAMrE;;;;OAIG;IACG,UAAU,CACd,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;IAc1C;;;;;;OAMG;IACG,WAAW,CACf,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,IAAI,GACP,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;YAU5B,kBAAkB;CAWjC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { SubscriptionPlanCollection } from './SubscriptionPlanCollection.js';
|
|
2
|
+
export { SubscriptionPlanVersionCollection } from './SubscriptionPlanVersionCollection.js';
|
|
2
3
|
export { TenantSubscriptionCollection } from './TenantSubscriptionCollection.js';
|
|
3
4
|
export { TenantUsageMetricCollection } from './TenantUsageMetricCollection.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/collections/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/collections/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,iCAAiC,EAAE,MAAM,wCAAwC,CAAC;AAC3F,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
9
|
import './__smrt-register__.js';
|
|
10
|
-
export { SubscriptionPlanCollection, TenantSubscriptionCollection, TenantUsageMetricCollection, } from './collections/index.js';
|
|
10
|
+
export { SubscriptionPlanCollection, SubscriptionPlanVersionCollection, TenantSubscriptionCollection, TenantUsageMetricCollection, } from './collections/index.js';
|
|
11
11
|
export { migrateSubscriptionsMoneyToMinorUnits, preflightSubscriptionsMoneyMinorUnits, SUBSCRIPTIONS_MONEY_COLUMNS, SUBSCRIPTIONS_MONEY_MINOR_UNITS_BACKFILL, } from './migrations/moneyMinorUnits.js';
|
|
12
|
+
export { migrateSubscriptionPlanVersions, type SubscriptionPlanVersionsMigrationReport, } from './migrations/planVersions.js';
|
|
12
13
|
export type { PriceBookKind, PricingStrategy, SpendingBasis, SpendingPeriod, SpendingPolicyBehavior, } from './models/index.js';
|
|
13
|
-
export { BillingAdjustment, BillingAdjustmentCollection, ClientCharge, ClientChargeCollection, CreditGrant, CreditGrantCollection, isCurrencyCode, isPriceBookKind, PriceBook, PriceBookAssignment, PriceBookCollection, PricingRule, PricingRuleCollection, RetailCharge, RetailChargeCollection, SpendingPolicy, SpendingPolicyCollection, SubscriptionPlan, TenantSubscription, TenantUsageMetric, } from './models/index.js';
|
|
14
|
+
export { BillingAdjustment, BillingAdjustmentCollection, ClientCharge, ClientChargeCollection, CreditGrant, CreditGrantCollection, isCurrencyCode, isPriceBookKind, PriceBook, PriceBookAssignment, PriceBookCollection, PricingRule, PricingRuleCollection, RetailCharge, RetailChargeCollection, SpendingPolicy, SpendingPolicyCollection, SubscriptionPlan, SubscriptionPlanVersion, TenantSubscription, TenantUsageMetric, } from './models/index.js';
|
|
15
|
+
export { enabledFeatureKeys, extractPlanTerms, isEntitlementUpgrade, PLAN_TERM_FIELDS, samePlanTerms, validThresholds, } from './plan-terms.js';
|
|
16
|
+
export type { PlanVersionReplacement } from './plan-versioning.js';
|
|
14
17
|
export type { AssignPriceBooksInput, AutoTopUpGrant, AutoTopUpHook, AutoTopUpRequest, BillingRelationshipReader, CommercialBillingStorage, CommercialUsageServiceOptions, CustomPricingContext, CustomPricingStrategy, DefinePriceBookPriceInput, DelegatedSpendingPolicyInput, GrantChildCreditInput, GrantCreditInput, PriceBookAssignmentView, PriceBookLeg, PriceUsageOptions, RateUsageOptions, ResellerBillingAction, ResellerBillingErrorCode, ResellerBillingServiceOptions, SpendingDecision, SpendingEvaluationInput, SpendingPolicyEvaluatorExtensions, SpendingPolicyEvaluatorOptions, UsageRating, } from './services/index.js';
|
|
15
|
-
export { assertCommercialBillingStorageSupported, CommercialBillingStorageConfigurationError, CommercialUsageService, evaluateThreshold, evaluateThresholds, ResellerBillingError, ResellerBillingService, SpendingPolicyEvaluator, type SubscriptionPlanReader, SubscriptionResolver, type SubscriptionResolverReaders, type TenantSubscriptionReader, TenantUsageMeter, UnsupportedCommercialBillingStorageError, type UsageSummaryReader, } from './services/index.js';
|
|
16
|
-
export type { AiUsageSummary, BillingInterval, EntitlementResolution, EntitlementResolutionContext, JsonObject, PlanFeatureGrant, PlanThreshold, PricingMode, RecordUsageOptions, Subscriber, SubscriberKind, SubscriptionPlanStatus, SubscriptionResolverOptions, SubscriptionStatus, SummarizeAiUsageOptions, SummarizeUsageBatchOptions, SummarizeUsageOptions, ThresholdEnforcement, ThresholdEvaluation, ThresholdWindow, UsageMeterOptions, UsageMetricRecord, UsageSummary, UsageWindow, } from './types.js';
|
|
18
|
+
export { assertCommercialBillingStorageSupported, CommercialBillingStorageConfigurationError, CommercialUsageService, evaluateThreshold, evaluateThresholds, ResellerBillingError, ResellerBillingService, SpendingPolicyEvaluator, type SubscriptionPlanReader, type SubscriptionPlanVersionReader, SubscriptionResolver, type SubscriptionResolverReaders, type TenantSubscriptionReader, TenantUsageMeter, UnsupportedCommercialBillingStorageError, type UsageSummaryReader, } from './services/index.js';
|
|
19
|
+
export type { AiUsageSummary, BillingInterval, EntitlementResolution, EntitlementResolutionContext, JsonObject, PlanFeatureGrant, PlanTerms, PlanThreshold, PlanVersionPinReason, PricingMode, RecordUsageOptions, Subscriber, SubscriberKind, SubscriptionPlanStatus, SubscriptionPlanVersionOptions, SubscriptionResolverOptions, SubscriptionStatus, SummarizeAiUsageOptions, SummarizeUsageBatchOptions, SummarizeUsageOptions, ThresholdEnforcement, ThresholdEvaluation, ThresholdWindow, UsageMeterOptions, UsageMetricRecord, UsageSummary, UsageWindow, } from './types.js';
|
|
17
20
|
export { assertSubscriberInvariant, getWindowForThreshold, getWindowKey, isValidThreshold, normalizeFeatureGrants, normalizeSubscriber, subscriberToColumns, } from './utils.js';
|
|
18
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,qCAAqC,EACrC,qCAAqC,EACrC,2BAA2B,EAC3B,wCAAwC,GACzC,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EACL,0BAA0B,EAC1B,iCAAiC,EACjC,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,qCAAqC,EACrC,qCAAqC,EACrC,2BAA2B,EAC3B,wCAAwC,GACzC,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,+BAA+B,EAC/B,KAAK,uCAAuC,GAC7C,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,aAAa,EACb,eAAe,EACf,aAAa,EACb,cAAc,EACd,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,sBAAsB,EACtB,cAAc,EACd,wBAAwB,EACxB,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnE,YAAY,EACV,qBAAqB,EACrB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,4BAA4B,EAC5B,qBAAqB,EACrB,gBAAgB,EAChB,uBAAuB,EACvB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,gBAAgB,EAChB,uBAAuB,EACvB,iCAAiC,EACjC,8BAA8B,EAC9B,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,uCAAuC,EACvC,0CAA0C,EAC1C,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,gBAAgB,EAChB,wCAAwC,EACxC,KAAK,kBAAkB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,4BAA4B,EAC5B,UAAU,EACV,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,sBAAsB,EACtB,8BAA8B,EAC9B,2BAA2B,EAC3B,kBAAkB,EAClB,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|